Add snippet in ModelAdmin - django

I have a ModelAdmin where I need to insert some html-snippet that is not part of a model (it's a java-applet). Is there any way to do this?

You have a couple options. If the applet is related to one of the form fields then you could create a custom widget which includes the applet. Another way would be to override the template used by the model change form and include the applet. The template should be in admin/app_name/model_name/change_form.html in your templates directory where app_name and model_name are replaced by the appropriate values for your model.

I tend to do a lot of this sort of thing, which is pretty much what you seem to want:
class SomeModelAdmin(admin.ModelAdmin):
...
list_display = (
'visible',
'thumbnail',
'size',
'url',
)
...
def thumbnail(self, obj):
return u'<img src="%s" />' % obj.url
thumbnail.allow_tags = True
... et voila, ad-hoc HTML snippets. obj is the model instance in question. Personally I find this more flexible than endlessly subclassing Widgets, ModelForms et al -- your mileage may vary depending on what you do with the admin site, or if your're of the more orthodox object-oriented persuasion; it's helpful to know how to do it in any case.

Related

How can I limit ImageField to a few choices in Django Admin

I have a model like with a file defined like
models.ImageField(upload_to='folder_icons', null=True)
I want to be able to limit the choice of this icon to a few pre created choices.
I there as way I can show the user (staff member) the choices in the django admin perhaps in a dropdown ?
This is similar to where I want a field where you choose between a few different avatars. Is there a custom field somewhere that can do this ?
Thanks
Just as a starting point, you would need to override the ModelAdmin.get_form() method, which will allow you to change the type of input field that Django uses by default for your image field. Here's what it should look like:
from django.forms import Select
class YourModelAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
# 1. Get the form from the parent class:
form = super(YourModelAdmin, self).get_form(request, obj, **kwargs)
# 2. Change the widget:
form.base_fields['your_image_field'].widget = Select(choices=(
('', 'No Image'),
('path/to/image1.jpg', 'Image 1'),
('path/to/image2.jpg, 'Image 2'),
))
# 3. Return the form!
return form
You'll still have some other considerations - for instance, the path/location of the images themselves (placing them in the settings.MEDIA_ROOT would probably be easiest, or at least the first step in trying to make this work). I can also imagine that you might want a more sophisticated presentation of this field, so that it shows a thumbnail of the actual images (see #Cheche's answer where he suggests select2 - that gets a bit more complicated though, as you'll need to craft a custom widget).
All of that said, in terms of just altering the form field that the admin uses so that it offers a dropdown/select field, rather than a file upload field, this is how you would achieve that.
What you need is a widget to render your choices. You could try with select2 or any django adapt to this, like django-select2. Check this.

How to add custom css class to django-filters field

I tried to make a way that worked with forms, but here the class did not apply
class TrainerFilter(django_filters.FilterSet):
price_lt = django_filters.NumberFilter(field_name="prise", lookup_expr='lt')
class Meta:
model = Profile
fields = ['direction', 'group', 'child', 'price_lt']
widgets = {
'direction': SelectMultiple(attrs={'class': 'custom-select'}),
}
In case somebody stumbles upon this, one solution might be to add the class to the field's widget attrs, like so:
class TrainerFilter(django_filters.FilterSet):
...
def __init__(self, data=None, queryset=None, *, request=None, prefix=None):
super(TrainerFilter, self).__init__(data=data, queryset=queryset, request=request, prefix=prefix)
self.filters['direction'].field.widget.attrs.update({'class': 'custom-select'})
You were pretty close. This should work:
class TrainerFilter(django_filters.FilterSet):
price_lt = django_filters.NumberFilter(field_name="prise", lookup_expr='lt')
direction = django_filters.CharFilter(widgets = SelectMultiple(attrs={'class': 'custom-select'}))
class Meta:
model = Profile
fields = ['group', 'child', 'price_lt']
class UsersFilter(django_filters.FilterSet):
class Meta:
model = UsersModels
fields = ['username', 'email', 'first_name', 'phone']
filter_overrides = {
models.CharField: {
'filter_class': django_filters.CharFilter,
'extra': lambda f: {
'lookup_expr': 'icontains',
'widget': forms.TextInput(attrs={'class': 'form-control'})
},
},
models.EmailField: {
'filter_class': django_filters.CharFilter,
'extra': lambda f: {
'lookup_expr': 'icontains',
'widget': forms.EmailInput(attrs={'class': 'form-control'})
},
},
}
like this
The easiest way I found is using django widget tweaks. And this module can be used with different kinds of forms, not just with filters. After installations add to template
{% load widget_tweaks %}
And now you can add class for your forms like this
{{ filter.form.direction.label_tag }}
{% render_field filter.form.direction class="custom-select" %}
insread
{{ filter.form.direction.label_tag }}
{{ filter.form.direction }}
IMPORTANT: read the bottom paragraph as I discovered an issue doing it this way and have linked to a source that explains how to use this in tandem with widget tweaks to add custom css to the filters.
From the official documentation.
Simply put, add a field into filters meta class named "form" that uses a forms.py class as its value, like so. This effectively extends your forms.py class.
Customize Field Types:
From there you can do customize the fields types like you normally for forms.py like so.
Actually styling the page using CSS
From here you got three options. My least favorite (leads to alot more code). Use widget tweaks to style it on the page. Use boot strap (or really any css files class). Or the most effective way use both boot strap and widget_tweaks while creating templates for your forms.
However, unless your doing really complicated filtering or querying stuff for 90% of use cases you could do something like the following in your forms.py class to make it pretty easy.
Note: I have since found this method unreliable read the bottom paragraph for a solution
IMPORTANT: Django filters unreliably applies the CSS to these fields extending the forms class but it does allow you to still customize the help text, the widgets used, and etc. so I'm leaving it in the answer. It worked for me initially but after expanding the filter to do more complicated things I realized some fields where getting arbitrarily left out. Below is an article that details how to use Django-filters WITH a custom widget tweaks file that does this for you.
Django-filters how to and widget tweaks with css styling
I found a pretty simple solution:
Open your dev tools and look at the element generated by django-filters
django-filters have assigned the element an id like 'id_field'
so just use that id in your CSS to style that element.

How to add a custom button or link beside a form field

I am using Django with crispy_forms third party library. I want to add a link beside a form field like some forms in Django admin app. How can I do this?
You've picked quite a complicated example that uses a method I wouldn't even recommend. But I'll try to explain how you see what you're seeing, and keep it short.
That is a AdminTimeWidget(forms.TimeInput) and a AdminDateWidget both nested in a AdminSplitDateTime(SplitDateTimeWidget(MultiWidget)). The MultiWidget part of this isn't really important, that's just how you bind two widgets together to provide one value (a datetime.datetime).
Here's what AdminTimeWidget looks like:
class AdminTimeWidget(forms.TimeInput):
#property
def media(self):
extra = '' if settings.DEBUG else '.min'
js = [
'vendor/jquery/jquery%s.js' % extra,
'jquery.init.js',
'calendar.js',
'admin/DateTimeShortcuts.js',
]
return forms.Media(js=["admin/js/%s" % path for path in js])
def __init__(self, attrs=None, format=None):
final_attrs = {'class': 'vTimeField', 'size': '8'}
if attrs is not None:
final_attrs.update(attrs)
super().__init__(attrs=final_attrs, format=format)
That adds a DateTimeShortcuts.js script to the page (in the way that Admin Widgets can, via the form media property) and it's that script that iterates input tags looking for date and time inputs.
There's a LOT of machinery involved to get that happening but again, in effect, it's just a bit of javascript that looks for a date/time input and adds the HTML client-side.
But you probably don't want to do that.
As I said, that's a very complicated widget, and in Admin where it's harder to alter things on the fly. If you want to write an Admin widget, you probably do want to go that way.
But if you already control the template, or a crispy layout, you could just bung in some HTML. Crispy has an HTML element that you can throw into layouts. This is well documented.
Or if you want a reusable widget, you could use a custom template. Since Django 1.11, Widgets use templates to render.
Create a widget, borrowing from an existing one to save time
from django.forms import widgets
class DateWithButtonWidget(widgets.DateInput):
template_name = 'widgets/date_with_button.html'
Customise the template with the HTML you want:
{% include "django/forms/widgets/input.html" %} <button>MY BUTTON</button>
Use that widget in your form:
class MyForm(forms.ModelForm):
fancydate = forms.DateField(widget=DateWithButtonWidget)
Of course, wiring that button to do something is all up to you. Using a fully-scripted option might be what you need after all.

Altering the listview of the django admin portal

I've read, extensively, how to change the admin site of Django. I have it mostly figured out -- I think. However there are still a few things that elude me in my understanding. I am using the default registered admin urls; so they are not customized, only what is exposed automatically.
The easiest way to explain this is through imagery...
Here's what I have:
Here's what I want:
I'm fairly certain the changes should be fairly simple. But I don't know exactly which model to alter and template to adjust to get it to look how I want. The [number] -- [name] are fields in my model.
I have extended other pieces of the admin interface to get customized forms for editing particular elements -- by registering my model and customizing the field for it.
#admin.register(Course)
class CourseAdmin(admin.ModelAdmin):
form = CourseAdminForm
fieldsets = (
('Course Info:', {'fields': ('course_number', 'name', 'description', 'units')}),
('Load Info:', {'fields': ('lecture_hours', 'lab_hours', 'discussion_hours', 'work_hours')})
)
in my app/admin.py file.
I'm a bit confused because there technically isn't a model to register here. So I'm not 100% sure how to do this. Do I wrap each one of my modifications inside the CourseAdmin class as different classes/methods with registered URLs or is there some other way I need to be doing this?
You need edit your Course model class:
# models.py
class Course(models.Model):
# fields here
name = ...
# ...
# add a unicode method
# __str__ method if you are using python 3.x
def unicode(self):
return '%s - %s' % (self.pk, self.name)

Django ModelForms: Display ManyToMany field as single-select

In a Django app, I'm having a model Bet which contains a ManyToMany relation with the User model of Django:
class Bet(models.Model):
...
participants = models.ManyToManyField(User)
User should be able to start new bets using a form. Until now, bets have exactly two participants, one of which is the user who creates the bet himself. That means in the form for the new bet you have to chose exactly one participant. The bet creator is added as participant upon saving of the form data.
I'm using a ModelForm for my NewBetForm:
class NewBetForm(forms.ModelForm):
class Meta:
model = Bet
widgets = {
'participants': forms.Select()
}
def save(self, user):
... # save user as participant
Notice the redefined widget for the participants field which makes sure you can only choose one participant.
However, this gives me a validation error:
Enter a list of values.
I'm not really sure where this comes from. If I look at the POST data in the developer tools, it seems to be exactly the same as if I use the default widget and choose only one participant. However, it seems like the to_python() method of the ManyToManyField has its problems with this data. At least there is no User object created if I enable the Select widget.
I know I could work around this problem by excluding the participants field from the form and define it myself but it would be a lot nicer if the ModelForm's capacities could still be used (after all, it's only a widget change). Maybe I could manipulate the passed data in some way if I knew how.
Can anyone tell me what the problem is exactly and if there is a good way to solve it?
Thanks in advance!
Edit
As suggested in the comments: the (relevant) code of the view.
def new_bet(request):
if request.method == 'POST':
form = NewBetForm(request.POST)
if form.is_valid():
form.save(request.user)
... # success message and redirect
else:
form = NewBetForm()
return render(request, 'bets/new.html', {'form': form})
After digging in the Django code, I can answer my own question.
The problem is that Django's ModelForm maps ManyToManyFields in the model to ModelMultipleChoiceFields of the form. This kind of form field expects the widget object to return a sequence from its value_from_datadict() method. The default widget for ModelMultipleChoiceField (which is SelectMultiple) overrides value_from_datadict() to return a list from the user supplied data. But if I use the Select widget, the default value_from_datadict() method of the superclass is used, which simply returns a string. ModelMultipleChoiceField doesn't like that at all, hence the validation error.
To solutions I could think of:
Overriding the value_from_datadict() of Select either via inheritance or some class decorator.
Handling the m2m field manually by creating a new form field and adjusting the save() method of the ModelForm to save its data in the m2m relation.
The seconds solution seems to be less verbose, so that's what I will be going with.
I don't mean to revive a resolved question but I was working a solution like this and thought I would share my code to help others.
In j0ker's answer he lists two methods to get this to work. I used method 1. In which I borrowed the 'value_from_datadict' method from the SelectMultiple widget.
forms.py
from django.utils.datastructures import MultiValueDict, MergeDict
class M2MSelect(forms.Select):
def value_from_datadict(self, data, files, name):
if isinstance(data, (MultiValueDict, MergeDict)):
return data.getlist(name)
return data.get(name, None)
class WindowsSubnetForm(forms.ModelForm):
port_group = forms.ModelMultipleChoiceField(widget=M2MSelect, required=True, queryset=PortGroup.objects.all())
class Meta:
model = Subnet
The problem is that ManyToMany is the wrong data type for this relationship.
In a sense, the bet itself is the many-to-many relationship. It makes no sense to have the participants as a manytomanyfield. What you need is two ForeignKeys, both to User: one for the creator, one for the other user ('acceptor'?)
You can modify the submitted value before (during) validation in Form.clean_field_name. You could use this method to wrap the select's single value in a list.
class NewBetForm(forms.ModelForm):
class Meta:
model = Bet
widgets = {
'participants': forms.Select()
}
def save(self, user):
... # save user as participant
def clean_participants(self):
data = self.cleaned_data['participants']
return [data]
I'm actually just guessing what the value proivded by the select looks like, so this might need a bit of tweaking, but I think it will work.
Here are the docs.
Inspired by #Ryan Currah I found this to be working out of the box:
class M2MSelect(forms.SelectMultiple):
def render(self, name, value, attrs=None, choices=()):
rendered = super(M2MSelect, self).render(name, value=value, attrs=attrs, choices=choices)
return rendered.replace(u'multiple="multiple"', u'')
The first one of the many to many is displayed and when saved only the selected value is left.
I found an easyer way to do this inspired by #Ryan Currah:
You just have to override "allow_multiple_selected" attribut from SelectMultiple class
class M2MSelect(forms.SelectMultiple):
allow_multiple_selected = False
class NewBetForm(forms.ModelForm):
class Meta:
model = Bet
participants = forms.ModelMultipleChoiceField(widget=M2MSelect, required=True, queryset=User.objects.all())