Django: override RelatedFieldWidgetWrapper - django

I want to change the way that the "+" icon for the foreign key in the admin site is shown.
I found that the widget that prints the code is RelatedFieldWidgetWrapper that is in django/contrib/admin/widgets.py.
So I wrote my version of this class and I changed its render function.
But now how can I use it? I mean... in the definition of my model do I have to use the formfield_overrides in this way?
formfield_overrides = {
models.ForeignKey: {'widget': customRelatedFieldWidgetWrapper},
}
I think that this is not the right way, because that widget is not the one that manage the whole foreign key, but only the "+" icon.
Am I wrong?
Thanks a lot.

You would need to create custom ModelForm for ModelAdmin and override widget there.
Example code:
#forms.py
class CustomForm(forms.ModelForm):
user = forms.ModelChoiceField(queryset=User.objects.all(), widget=yourCustomWidget)
class Meta:
model = MyModel
#admin.py
class MyModelAdmin(admin.ModelAdmin):
form = CustomForm

I approached this slightly differently by monkey-patching the widget - that way the change is reflected in all forms and you're not monkeying around with django's source code.
I ran into this as I was working on customizing yawd admin, a very nice Twitter-Bootstrap skin for admin interface. Now all my icons are jazzed up.
import django.contrib.admin.widgets
class MyRelatedFieldWidgetWrapper(django.contrib.admin.widgets.RelatedFieldWidgetWrapper):
"""
This class is a wrapper to a given widget to add the add icon for the
admin interface.
"""
def render(self, name, value, *args, **kwargs):
rel_to = self.rel.to
info = (rel_to._meta.app_label, rel_to._meta.model_name)
self.widget.choices = self.choices
output = [self.widget.render(name, value, *args, **kwargs)]
if self.can_add_related:
related_url = reverse(
'admin:%s_%s_add'
% info, current_app=self.admin_site.name
)
output.append(
"""
<a href="%s"
onclick="return showAddAnotherPopup(this);
alt="%s">
<i class="help icon-large icon-plus-sign"
id="add_id_%s"
data-original-title>
</i>
</a>""" % (related_url, _('Add Another'), name))
return mark_safe(''.join(output))
# Monkeypatch it
django.contrib.admin.widgets.RelatedFieldWidgetWrapper = MyRelatedFieldWidgetWrapper

Related

Django MultiValueField

I'm struggling with some Django, where I want to make a custom MultiValueField combined with MultiWidget. I've read misc. tutorials, but it seem I'm missing something - most of them were quite old, which I suspect could be the reason.
I'm using Django 1.10.
Goal: Make a custom field that provides three dropdowns for a form. So far, no requirements to the contents of the dropdowns - first I just want to see them in my form :-)
I have a fields.py file, containing:
from django import forms
from widgets import MyCustomWidget
class MyCustomField(forms.MultiValueField):
widget = MyCustomWidget
def __init__(self, *args, **kwargs):
fields = (
forms.CharField(max_length=31),
forms.CharField(max_length=31),
forms.CharField(max_length=31),
)
super(MyCustomField, self).__init__(fields, *args, **kwargs)
def compress(self, data_list):
return "-".join(data_list)
And then there's a widgets.py, containing:
import re
from django import forms
class MyCustomWidget(forms.MultiWidget):
def __init__(self, attrs=None):
widgets = (
forms.widgets.Select(attrs=attrs, choices=[("1", "1")]),
forms.widgets.Select(attrs=attrs, choices=[("2", "2")]),
forms.widgets.Select(attrs=attrs, choices=[("3", "3")]),
)
super(MyCustomWidget, self).__init__(widgets, attrs)
def decompress(self, value):
if value:
return re.split(r"\-", value)
return [None, None, None]
forms.py:
from django.forms import ModelForm
from django import forms
class MyCustomForm(forms.ModelForm):
class Meta:
model = MyCustomModel
fields = ("name")
name = forms.CharField(widget=MyCustomField)
This works fine when migrating, but I try to view the form, I get this error:
'MyCustomField' object has no attribute 'is_hidden'
I have tried to implement this attribute in MyCustomField, but then I get another error:
'MyCustomField' object has no attribute 'attrs'
These attributes should be provided by forms.MultiValueField, as far as I understand - thus I shouldn't need to write them myself.
In the template, I'm just using "{{ form }}" as I wan't to use Django's default layout.
I'm going nuts here and hope someone is able to help to the right path :-)
Kind regards,
Kasper
I believe your mistake is on the line where you set the name
name = forms.CharField(widget=MyCustomField). I haven't tested the codes thou.
from django.forms import ModelForm
from django import forms
class MyCustomForm(forms.ModelForm):
class Meta:
model = MyCustomModel
fields = ("name")
name = MyCustomField # This I believe is will fix your error

django-filter: extend filter query with request.user

I'm need to add an additional filter property (in the background) to a django-filter request.
My Model:
class Event(models.Model):
name=models.CharField(max_length=254)
location=models.ForeignKey(Place)
invited_user=models.ManyToManyField(User,null=True, blank=True)
With a filter those entries with the same location can be filtered. This is working.
Further on I have to exclude all those entries where the invited_user is not the request.user (choosing this filter property is only possible if the user has permissions).
Is this possible with django-filter, and if yes how?
My filter Class:
import django_filters
from models import Event
class EventFilter(django_filters.FilterSet):
class Meta:
model = Event
fields = ['location']
My work is based on: How do I filter tables with Django generic views?
you can access the request object in FilterSet.qs property.
class EventFilter(django_filters.FilterSet):
class Meta:
model = Event
fields = ['location']
#property
def qs(self):
queryset=super(EventFilter, self).qs
if request.user.has_perm("app_label.has_permission"):
return queryset.exclude(invited_user!=self.request.user)
return queryset
docs https://rpkilby.github.io/django-filter/guide/usage.html#filtering-the-primary-qs
I think in your case you could do it by modifying the queryset in the view, where you should be able to access request.user. Therefore you wouldn't need to dig deep into django-filter,
In my case, when using dango_filters FilterView along with crispy forms to render the form, I wanted to hide fields from the form, along with additional filtering as you described, so I overrode get() for the FilterView, restricted the queryset to the user, and used crispy form's layout editing to pop the unwanted fields from the filter form:
def get(self, request, *args, **kwargs):
"""
original code from django-filters.views.BaseFilterView - added admin check
"""
filterset_class = self.get_filterset_class()
self.filterset = self.get_filterset(filterset_class)
self.object_list = self.filterset.qs
# If not admin, restrict to assessor's own centre and clients
if not request.user.get_profile().is_admin:
self.object_list = self.object_list.filter(attendee__assessor=request.user)
self.filterset.form.helper.layout[0].pop(2) # centres filter
self.filterset.form.helper.layout[0].pop(1) # assessors filter
context = self.get_context_data(filter=self.filterset,
object_list=self.object_list)
return self.render_to_response(context)
Try this:
class EventListView(BaseFilterView):
...
def get_filterset(self, *args, **kwargs):
fs = super().get_filterset(*args, **kwargs)
fs.filters['location'].field.queryset = fs.filters['location'].field.queryset.filter(user=self.request.user)
return fs

django admin tinymce for part of textareas

I'm using django admin + grappelli + tinymce from grappelli.
It works perfect, but I can't figure out how to make an admin form with textareas and apply tinymce only for part of them.
For example, I've two fields: description and meta-description. I need tinymce only for description and I need textarea without tinymce for meta-descrption.
tinymce in admin is enabled via form like this:
class AdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
"""Sets the list of tags to be a string"""
instance = kwargs.get('instance', None)
if instance:
init = kwargs.get('initial', {})
kwargs['initial'] = init
super(AdminForm, self).__init__(*args, **kwargs)
class Media:
js = (
settings.STATIC_URL + 'grappelli/tinymce/jscripts/tiny_mce/tiny_mce.js',
settings.STATIC_URL + 'grappelli/tinymce_setup/tinymce_setup.js',
)
and enabled in admin.py:
class ModelAdmin(admin.ModelAdmin):
form = AdminForm
It looks like behaviour is described in the beginning of tinymce init:
tinyMCE.init({
mode: 'textareas',
theme: 'advanced',
skin: 'grappelli',
...
Is there a way to solve my issue?
By setting the mode to textareas, it won't give you any sort of selector control as to which one it applies the editor to. You'll most likely need to drop the mode setting and go with selector: http://www.tinymce.com/wiki.php/Configuration:selector
The django-tinymce config options are just a mirror for TinyMCE's settings.
On a field by field basis you can use this technique providing a custom ModelForm:
class XAdminForm(forms.ModelForm):
name = forms.CharField(label='Name', max_length=100,
widget=forms.TextInput(attrs={'size': '100'}))
something = forms.CharField(label='Something', max_length=SOME_MAX_LENGTH,
widget=forms.Textarea(attrs={'rows': '10', 'cols': '100'}))
note = forms.CharField(label='Note', max_length=NOTE_MAX_LENGTH,
widget=forms.Textarea(attrs={'class': 'ckeditor'}))
class Meta:
model = x
class Meta:
model = x
class XAdmin(admin.ModelAdmin):
model = X
form = XAdminForm
class Media:
js = ('/static/js/ckeditor/ckeditor.js',)
admin.site.register(X, XAdmin)

Adding links to full change forms for inline items in django admin?

I have a standard admin change form for an object, with the usual StackedInline forms for a ForeignKey relationship. I would like to be able to link each inline item to its corresponding full-sized change form, as the inline item has inlined items of its own, and I can't nest them.
I've tried everything from custom widgets to custom templates, and can't make anything work. So far, the "solutions" I've seen in the form of snippets just plain don't seem to work for inlines. I'm getting ready to try some DOM hacking with jQuery just to get it working and move on.
I hope I must be missing something very simple, as this seems like such a simple task!
Using Django 1.2.
There is a property called show_change_link since Django 1.8.
I did something like the following in my admin.py:
from django.utils.html import format_html
from django.core.urlresolvers import reverse
class MyModelInline(admin.TabularInline):
model = MyModel
def admin_link(self, instance):
url = reverse('admin:%s_%s_change' % (instance._meta.app_label,
instance._meta.module_name),
args=(instance.id,))
return format_html(u'Edit', url)
# … or if you want to include other fields:
return format_html(u'Edit: {}', url, instance.title)
readonly_fields = ('admin_link',)
The currently accepted solution here is good work, but it's out of date.
Since Django 1.3, there is a built-in property called show_change_link = True that addresses this issue.
This can be added to any StackedInline or TabularInline object. For example:
class ContactListInline(admin.TabularInline):
model = ContactList
fields = ('name', 'description', 'total_contacts',)
readonly_fields = ('name', 'description', 'total_contacts',)
show_change_link = True
The result will be something line this:
I had similar problem and I came up with custom widget plus some tweaks to model form. Here is the widget:
from django.utils.safestring import mark_safe
class ModelLinkWidget(forms.Widget):
def __init__(self, obj, attrs=None):
self.object = obj
super(ModelLinkWidget, self).__init__(attrs)
def render(self, name, value, attrs=None):
if self.object.pk:
return mark_safe(
u'<a target="_blank" href="../../../%s/%s/%s/">%s</a>' %\
(
self.object._meta.app_label,
self.object._meta.object_name.lower(),
self.object.pk, self.object
)
)
else:
return mark_safe(u'')
Now since widget for each inline need to get different object in constructor you can't just set it in standard way, but in Form's init method:
class TheForm(forms.ModelForm):
...
# required=False is essential cause we don't
# render input tag so there will be no value submitted.
link = forms.CharField(label='link', required=False)
def __init__(self, *args, **kwargs):
super(TheForm, self).__init__(*args, **kwargs)
# instance is always available, it just does or doesn't have pk.
self.fields['link'].widget = ModelLinkWidget(self.instance)
Quentin's answer above works, but you also need to specify fields = ('admin_link',)
There is a module for this purpose. Check out:
django-relatives
I think: args=[instance.id] should be args=[instance.pk]. It worked for me!

Readonly fields in the django admin/inline

I use this snippet to show several fields in my admin backend as readonly, but as noticed in the comments, it does not work on stackedinline/tabularinline. Is there any other way to achieve this? I have a list of objects attached to a model and just want to show it in the model's details view without the possibility to change values.
If you are running Django 1.3 or later; there's an attribute named ModelAdmin.readonly_fields which you could use.
InlineModelAdmin inherits from ModelAdmin, so you should be able to use it from your inline subclass.
I've encountered the same problem today. Here is my solution. This is example of read-only field for the ForeignKey value:
class MySelect(forms.Select):
def render(self, name, value, attrs=None, choices=()):
s = Site.objects.get(id=value)
return s.name
class UserProfileInlineForm(forms.ModelForm):
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=MySelect)
class UserProfileInline(admin.StackedInline):
model = UserProfile
form = UserProfileInlineForm
As is the case with JQuery, it seems you can achieve this by changing an attr called "disabled" (works in my Safari, OK we're now in 2013 :-) ).
Example below:
def get_form(self, request, obj=None, **kwargs):
result = super(<your ModelAdmin class here>, self).get_form(request, obj=obj, **kwargs)
result.base_fields[<the select field you want to disable>].widget.attrs['disabled'] = 'disabled'
return result