list_display - boolean icons for methods - django

When defining the list_display array for a ModelAdmin class, if a BooleanField or NullBooleanField is given the UI will use nice looking icons instead of True/False text in the column. If a method that returns a boolean is given, however, it simply prints out True/False.
Is there a way to make it use the pretty icons for a boolean method?

This is documented, although it's a bit hard to find - go a couple of screens down from here, and you'll find this:
If the string given is a method of the model, ModelAdmin or a callable that returns True or False Django will display a pretty "on" or "off" icon if you give the method a boolean attribute whose value is True.
and the example given is:
def born_in_fifties(self):
return self.birthday.strftime('%Y')[:3] == '195'
born_in_fifties.boolean = True

Thanks to #daniel-roseman (rtfm)
Since Django 3.2 there is a decorator #admin.display(boolean=True):
If the string (in list_display) given is a method of the model,
ModelAdmin or a callable that returns True, False, or None, Django
will display a pretty “yes”, “no”, or “unknown” icon if you wrap the
method with the display() decorator passing the boolean argument with
the value set to True:
class Person(models.Model):
birthday = models.DateField()
#admin.display(boolean=True)
def born_in_fifties(self):
return 1950 <= self.birthday.year < 1960

I got this to work for me (Django 3.1.10)
class MyAdmin(MyModel):
list_display = ("field_as_boolean", )
def field_as_boolean(self, obj):
return True if obj.field else False
field_as_boolean.boolean = True
field_as_boolean.short_description = "field_name"

Related

django suit admin filter - simplest way to override lookups

When using Django suit's admin filter, as I select a value, the field name disappears.
For example looking at:
http://djangosuit.com/admin/examples/kitchensink/
As shown in the image, when someone selects the "Choices" field, they can only see the value that is selected (Tall).
This can sometimes be bad; for example for a boolean field that is "in stock" you'd only see 'Yes' but I'd prefer to see 'In Stock: Yes'.
I know I can make a custom filter and specify the lookup tuples, but I'm wondering whether there is a cleaner more sustainable way to do this.
To illustrate how much redundant code there is:
class InventoryFilter(admin.SimpleListFilter):
title = 'is_in_stock'
parameter_name = 'is_in_stock'
def lookups(self, request, model_admin):
return (('Yes', 'in stock:Yes'),('No', 'in stock:No'))
def queryset(self, request, queryset):
return queryset.filter(is_in_stock=True)
#admin.register(Inventory)
class InventoryAdmin(admin.ModelAdmin):
list_display = ('is_in_stock',)
list_filter = ('is_in_stock',)
and I have to do this for every variable!
I'm looking for a more intelligent way. Thanks
Another solution, extend the template and show the field name before the dropdown (if the field has a value).
It's work for me , Django-Suit V2
list_filter = ('Model_ForeignKey', )

How to determine if a field has changed in a Django modelform

I was surprised that this was difficult to do. However I came up with this, which seems to work at least for my simple case. Can anyone recommend a better approach?
def field_changed(self, fieldname):
"""Tests if the value of the field changed from the original data"""
orig_value = self.fields[fieldname].initial or getattr(self.instance, field, None)
orig_value = getattr(orig_value, 'pk', orig_value)
if type(orig_value) is bool:
# because None and False can be interchangeable
return bool(self.data.get(fieldname)) != bool(orig_value)
else:
return unicode(self.data.get(fieldname)) != unicode(orig_value)
Form contains a property changed_data which holds a list of all the fields whose values have changed.
Try:
'fieldname' in myforminstance.changed_data
It seems that you have reinvented .has_changed() method.
Extending on Airs's answer, for multiple fields:
In a scenario where you'd like to track changes for a list of fields ['field_a', 'field_b', 'field_c']
If you'd like to check if any of those fields has changed:
any(x in myforminstance.changed_data for x in ['field_a', 'field_b', 'field_c'])
If you'd like to check if all of those fields have changed:
all(x in myforminstance.changed_data for x in ['field_a', 'field_b', 'field_c'])
You must override the method post_save. Overriding methods is a good practice in Django: https://docs.djangoproject.com/en/dev/ref/signals/#post-save
I suggest you adding something like this into your model class:
def post_save(self, sender, instance, created, raw, using, update_fields):
if 'the_field' in update_fields:
# Do whatever...

Django get_next_by_FIELD using complex Q lookups

While creating a front end for a Django module I faced the following problem inside Django core:
In order to display a link to the next/previous object from a model query, we can use the extra-instance-methods of a model instance: get_next_by_FIELD() or get_previous_by_FIELD(). Where FIELD is a model field of type DateField or DateTimeField.
Lets explain it with an example
from django.db import models
class Shoe(models.Model):
created = models.DateTimeField(auto_now_add=True, null=False)
size = models.IntegerField()
A view to display a list of shoes, excluding those where size equals 4:
def list_shoes(request):
shoes = Shoe.objects.exclude(size=4)
return render_to_response(request, {
'shoes': shoes
})
And let the following be a view to display one shoe and the corresponding
link to the previous and next shoe.
def show_shoe(request, shoe_id):
shoe = Shoe.objects.get(pk=shoe_id)
prev_shoe = shoe.get_previous_by_created()
next_shoe = shoe.get_next_by_created()
return render_to_response('show_shoe.html', {
'shoe': shoe,
'prev_shoe': prev_shoe,
'next_shoe': next_shoe
})
Now I have the situation that the show_shoe view displays the link to the previous/next regardless of the shoes size. But I actually wanted just shoes whose size is not 4.
Therefore I tried to use the **kwargs argument of the get_(previous|next)_by_created() methods to filter out the unwanted shoes, as stated by the documentation:
Both of these methods will perform their queries using the default manager for the model. If you need to emulate filtering used by a custom manager, or want to perform one-off custom filtering, both methods also accept
optional keyword arguments, which should be in the format described in Field lookups.
Edit: Keep an eye on the word "should", because then also (size_ne=4) should work, but it doesn't.
The actual problem
Filtering using the lookup size__ne ...
def show_shoe(request, shoe_id):
...
prev_shoe = shoe.get_previous_by_created(size__ne=4)
next_shoe = shoe.get_next_by_created(size__ne=4)
...
... didn't work, it throws FieldError: Cannot resolve keyword 'size_ne' into field.
Then I tried to use a negated complex lookup using Q objects:
from django.db.models import Q
def show_shoe(request, shoe_id):
...
prev_shoe = shoe.get_previous_by_created(~Q(size=4))
next_shoe = shoe.get_next_by_created(~Q(size=4))
...
... didn't work either, throws TypeError: _get_next_or_previous_by_FIELD() got multiple values for argument 'field'
Because the get_(previous|next)_by_created methods only accept **kwargs.
The actual solution
Since these instance methods use the _get_next_or_previous_by_FIELD(self, field, is_next, **kwargs) I changed it to accept positional arguments using *args and passed them to the filter, like the **kwargs.
def my_get_next_or_previous_by_FIELD(self, field, is_next, *args, **kwargs):
"""
Workaround to call get_next_or_previous_by_FIELD by using complext lookup queries using
Djangos Q Class. The only difference between this version and original version is that
positional arguments are also passed to the filter function.
"""
if not self.pk:
raise ValueError("get_next/get_previous cannot be used on unsaved objects.")
op = 'gt' if is_next else 'lt'
order = '' if is_next else '-'
param = force_text(getattr(self, field.attname))
q = Q(**{'%s__%s' % (field.name, op): param})
q = q | Q(**{field.name: param, 'pk__%s' % op: self.pk})
qs = self.__class__._default_manager.using(self._state.db).filter(*args, **kwargs).filter(q).order_by('%s%s' % (order, field.name), '%spk' % order)
try:
return qs[0]
except IndexError:
raise self.DoesNotExist("%s matching query does not exist." % self.__class__._meta.object_name)
And calling it like:
...
prev_shoe = shoe.my_get_next_or_previous_by_FIELD(Shoe._meta.get_field('created'), False, ~Q(state=4))
next_shoe = shoe.my_get_next_or_previous_by_FIELD(Shoe._meta.get_field('created'), True, ~Q(state=4))
...
finally did it.
Now the question to you
Is there an easier way to handle this? Should shoe.get_previous_by_created(size__ne=4) work as expected or should I report this issue to the Django guys, in the hope they'll accept my _get_next_or_previous_by_FIELD() fix?
Environment: Django 1.7, haven't tested it on 1.9 yet, but the code for _get_next_or_previous_by_FIELD() stayed the same.
Edit: It is true that complex lookups using Q object is not part of "field lookups", it's more part of the filter() and exclude() functions instead. And I am probably wrong when I suppose that get_next_by_FIELD should accept Q objects too. But since the changes involved are minimal and the advantage to use Q object is high, I think these changes should get upstream.
tags: django, complex-lookup, query, get_next_by_FIELD, get_previous_by_FIELD
(listing tags here, because I don't have enough reputations.)
You can create custom lookup ne and use it:
.get_next_by_created(size__ne=4)
I suspect the method you've tried first only takes lookup arg for the field you're basing the get_next on. Meaning you won't be able to access the size field from the get_next_by_created() method, for example.
Edit : your method is by far more efficient, but to answer your question on the Django issue, I think everything is working the way it is supposed to. You could offer an additional method such as yours but the existing get_next_by_FIELD is working as described in the docs.
You've managed to work around this with a working method, which is OK I guess, but if you wanted to reduce the overhead, you could try a simple loop :
def get_next_by_field_filtered(obj, field=None, **kwargs):
next_obj = getattr(obj, 'get_next_by_{}'.format(field))()
for key in kwargs:
if not getattr(next_obj, str(key)) == kwargs[str(key)]:
return get_next_by_field_filtered(next_obj, field=field, **kwargs)
return next_obj
This isn't very efficient but it's one way to do what you want.
Hope this helps !
Regards,

SimpleListFIlter Default

I have a boolean field on my model that represents whether someone has canceled their membership or not. I am trying to create a custom SimpleListFilter that allows this field to be filtered on.
However, I really want to show only those who are not canceled by default. Is there someway to select the "No" option by default? This is my filter so far:
class CanceledFilter(SimpleListFilter):
title = 'Canceled'
# Parameter for the filter that will be used in the URL query.
parameter_name = 'canceled'
def lookups(self, request, model_admin):
return (
(True, 'Yes'),
(False, 'No'),
)
def queryset(self, request, queryset):
if self.value() is True or self.value() is None:
return queryset.filter(canceled=True)
if self.value() is False:
return queryset.filter(canceled=False)
EDIT:
I should have been a bit clearer. I am specifically trying to do this in the Admin interface. When I add the above filter as a list_filter in admin. I get a filter on the side of the admin page with 3 choices: All, Yes and No.
I would like the "No" choice or none of the choices to be set by default. Instead the "All" choice is always set by default. Is there some none hacky way to set the default filter choice or something like that.
Basiclly in Admin when they view the Members, I only want to show the active (not canceled) by default. If they click "All" or "Yes" then I want to show the canceled ones.
Update:
Note this is the same as question Default filter in Django admin, but I that question is now 6 years old. The accepted answer is marked as requiring Django 1.4. I am not sure if that answer will still work with newer Django versions or is still the best answer.
Given the age of the answers on the other question, I am not sure how we should proceed. I don't think there is any way to merge the two.
Had to do the same and stumbled upon your question. This is how I fixed it in my code (adapted to your example):
class CanceledFilter(SimpleListFilter):
title = 'Canceled'
# Parameter for the filter that will be used in the URL query.
parameter_name = 'canceled'
def lookups(self, request, model_admin):
return (
(2, 'All'),
(1, 'Yes'),
(0, 'No'),
)
def queryset(self, request, queryset):
if self.value() is None:
self.used_parameters[self.parameter_name] = 0
else:
self.used_parameters[self.parameter_name] = int(self.value())
if self.value() == 2:
return queryset
return queryset.filter(cancelled=self.value())
Some explanation is required. The querystring is just part of the URL, and exactly what the name implies: a query string. Your values come in as strings, not as booleans or integers. So when you call self.value(), it returns a string.
If you examine the URL you get when you click on the Yes/No, when not using a custom list filter, you'll see it encodes it as 1/0, not True/False. I went with the same scheme.
For completeness and our future readers, I also added 2 for All. Without verifying, I assume that was None before. But None is also used when nothing is selected, which defaults to All. Except, in our case it needs to default to False, so I had to pick a different value. If you don't need the All option, just remove the final if-block in the queryset method, and the first tuple in the lookups method.
With that out of the way, how does it work? The trick is in realising that self.value() just returns:
self.used_parameters.get(self.parameter_name, None)
which is either a string, or None, depending on whether the key is found in the dictionary or not. So that's the central idea: we make sure it contains integers and not strings, so that self.value() can be used in the call to queryset.filter(). Special treatment for the value for All, which is 2: in this case, just return queryset rather than a filtered queryset. Another special value is None, which means there is no key parameter_name in the dictionary. In that case, we create one with value 0, so that False becomes the default value.
Note: your logic was incorrect there; you want the non-cancelled by default, but you treat None the same as True. My version corrects this.
ps: yes, you could check for 'True' and 'False' rather than True and False in your querystring method, but then you'd notice the correct selection would not be highlighted because the first elements in your tuple don't match up (you're comparing strings to booleans then). I tried making the first elements in the tuples strings too, but then I'd have to do string comparison or eval to match up 'True' to True, which is kind of ugly/unsafe. So best stick to integers, like in my example.
If anyone is still interested in a solution for this, I used a different and IMHO much cleaner approach. As I'm fine with a default choice and the handling of it, I decided I just want to rename the default display label. This is IMHO much cleaner and you don't need any "hacks" to handle the default value.
class CompleteFilter(admin.SimpleListFilter):
'''
Model admin filter to filter orders for their completion state.
'''
title = _('completion')
parameter_name = 'complete'
def choices(self, changelist):
'''
Return the available choices, while setting a new default.
:return: Available choices
:rtype: list
'''
choices = list(super().choices(changelist))
choices[0]['display'] = _('Only complete')
return choices
def lookups(self, request, model_admin):
'''
Return the optionally available lookup items.
:param django.http.HttpRequest request: The Django request instance
:param django.contrib.admin.ModelAdmin model_admin: The model admin instance
:return: Optional lookup states
:rtype: tuple
'''
return (
('incomplete', _('Only incomplete')),
('all', _('All')),
)
def queryset(self, request, queryset):
'''
Filter the retreived queryset.
:param django.http.HttpRequest request: The Django request instance
:param django.db.models.query.QuerySet: The Django database query set
:return: The filtered queryset
:rtype: django.db.models.query.QuerySet
'''
value = self.value()
if value is None:
return queryset.filter(state__complete=True)
elif value == 'incomplete':
return queryset.filter(state__complete=False)
return queryset
In the choices() method, I just rename the display label from All to Only complete. Thus, the new default (which has a value of None is now renamed).
Then I've added all additional lookups as usual in the lookups() method. Because I still want an All choice, I add it again. However, you can also skip that part if you don't need it.
That's basically it! However, if you want to display the All choice on top again, you might want to reorder the choices list in the choices() method before returning it. For example:
# Reorder choices so that our custom "All" choice is on top again.
return [choices[2], choices[0], choices[1]]
Look at the section called "Adding Extra Manager Methods" in the link below:
http://www.djangobook.com/en/2.0/chapter10.html
You can add an additional models.Manager to your model to only return people that have not cancelled their membership. A rough implementation of the additional models.Manager would look like this:
class MemberManager(models.Manager):
def get_query_set(self):
return super(MemberManager, self).get_query_set().filter(membership=True)
class Customer(models.Model):
# fields in your model
membership = BooleanField() # here you can set to default=True or default=False for when they sign up inside the brackets
objects = models.Manager # default Manager
members = MemberManager() # Manager to filter for members only
Anytime you need to get a list of you current members only, you would then just call:
Customer.members.all()

Override get_FIELD_display method in Django model

Let's say I have a field called field in my model, with a choices parameter defining the values to be returned by the get_field_display method.
I need the get_field_display method to return a different value based on another field. Is there any way to override get_field_display?
This doesn't work:
def get_field_display(self):
if self.other_field == 1:
return 'Other value'
return super.get_field_display(self)
You can't call super because the function is defined not by the parent class but by the ModelBase metaclass. Try with this:
def get_field_display(self):
if self.other_field == 1:
value = 'Other value'
else:
field_object = self._meta.get_field('field')
value = self._get_FIELD_display(field_object)
return value
What you can do is to create a different function in the same model, then monkey patch it. For example, in admin.py you may do something like:
ClassName.get_field_display = ClassName.get_patched_field_display
It's not very 'nice' but it works for me.