How to implement sorting in Django Admin for calculated model properties without writing the logic twice?

django nullif
django
django ntile
django annotate count with condition
django conditional save
django-annotate with subquery
django get or create
django case when exists

In my Django model, I defined a @property which worked nicely and the property can be shown in the admin list_display without any problems.

I need this property not only in admin but in my code logic in other places as well, so it makes sense to have it as property for my model.

Now I wanted to make the column of this property sortable, and with help of the Django documentation of the When object, this StackOverflow question for the F()-calculation and this link for the sorting I managed to build the working solution shown below.

The reason for posing a question here is: In fact, I implemented my logic twice, once in python and once in form of an expression, which is against the design rule of implementing the same logic only once. So I wanted to ask whether I missed a better solution to my problem. Any ideas are appreciated.

This is the model (identifyers modified):

class De(models.Model):

    fr = models.BooleanField("[...]")
    de = models.SmallIntegerField("[...]")
    gd = models.SmallIntegerField("[...]")
    na = models.SmallIntegerField("[...]")
    # [several_attributes, Meta, __str__() removed for readability]

    @property
    def s_d(self):
        if self.fr:
            return self.de
        else:
            return self.gd + self.na

This is the Model Admin:

class DeAdmin(admin.ModelAdmin):
    list_display = ("[...]", "s_d", "gd", "na", "de", "fr" )

    def get_queryset(self, request):
        queryset = super().get_queryset(request)
        queryset = queryset.annotate(
            _s_d=Case(
                When(fr=True, then='s_d'),
                When(fr=False, then=F('gd') + F('na')),
                default=Value(0),
                output_field=IntegerField(),
            )
        )
        return queryset

    def s_d(self, obj):
        return obj._s_d
    s_d.admin_order_field = '_s_d'

If there is no other way, I would also appreciate confirmation of the fact as an answer.

TL/DR: Yes your solution seems to follow the only way that makes sense.


Well, what you have composed here seems to be the recommended way from the sources you list in your question and for good reason.

What is the good reason though? I haven't found a definitive, in the codebase, answer for that but I imagine that it has to do with the way @property decorator works in Python.

When we set a property with the decorator then we cannot add attributes to it and since the admin_order_field is an attribute then we can't have that in there. That statement seems to be reinforced from the Django Admin's list_display documentation where the following passage exists:

Elements of list_display can also be properties. Please note however, that due to the way properties work in Python, setting short_description on a property is only possible when using the property() function and not with the @property decorator.

That quote in combination with this QA: AttributeError: 'property' object has no attribute 'admin_order_field' seems to explain why it is not possible to have an orderable from a model property directly into the admin panel.


That explained (probably?) it is time for some mental gymnastics!!

In the previously mentioned part of the documentation we can also see that the admin_order_field can accept query expressions since version 2.1:

Query expressions may be used in admin_order_field. For example:

from django.db.models import Value
from django.db.models.functions import Concat

class Person(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)

    def full_name(self):
        return self.first_name + ' ' + self.last_name
    full_name.admin_order_field = Concat('first_name', Value(' '), 'last_name')

That in conjunction with the previous part about the property() method, allows us to refactor your code and essentially move the annotation part to the model:

class De(models.Model):
    ...
    def calculate_s_d(self):
        if self.fr:
            return self.de
        else:
            return self.gd + self.na

    calculate_s_d.admin_order_field = Case(
        When(fr=True, then='s_d'),
        When(fr=False, then=F('gd') + F('na')),
        default=Value(0),
        output_field=IntegerField(),
    )

    s_d = property(calculate_s_d)

Finally, on the admin.py we only need:

class DeAdmin(admin.ModelAdmin):
    list_display = ("[...]", "s_d")

Howto annotate a QuerySet with calculated properties and use them , How to implement sorting in Django Admin for calculated model properties without writing the logic twice? Post author By Full Stack; Post date  How to implement sorting in Django Admin for calculated model properties without writing the logic twice? Manager vs Query Sets in Django How to enable filtering on calculated fields?

Although I think your solution is very good (or even better), the another approach can be to extract admin query to the model manager:

class DeManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().annotate(
            s_d=Case(
                When(fr=True, then='s_d'),
                When(fr=False, then=F('gd') + F('na')),
                default=Value(0),
                output_field=IntegerField(),
            )
        )


class De(models.Model):
    fr = models.BooleanField("[...]")
    de = models.SmallIntegerField("[...]")
    gd = models.SmallIntegerField("[...]")
    na = models.SmallIntegerField("[...]")
    objects = DeManager()


class DeAdmin(admin.ModelAdmin):
    list_display = ("[...]", "s_d", "gd", "na", "de", "fr" )

In this case you don't need the property because each object will have s_d attribute, although this is true only for existing objects (from the database). If you create a new object in Python and try to access obj.s_d you will get an error. Another disadvantage is that each query will be annotated with this attribute even if you don't use it, but this can be solved by customizing the manager's queryset.

How-to annotate a Django QuerySet with calculated properties, and , First, provide a method to annotate the queryset with the calculated properties. BooleanField, IntegerField from django.db.models.functions import TruncDay, Now a specific property, and add admin_order_field attribute to enable sorting. in Django Admin for calculated model properties without writing the logic twice​? 8 Django 1.9a1 __init__.py is visible in eclipse/PyDev even though it should be deleted (Windows) Mar 10 '16 6 How to implement sorting in Django Admin for calculated model properties without writing the logic twice?

Unfortunately, this is impossible in current stable Django version (up to 2.2) due to Django admin not fetching admin_order_field from object properties.

Fortunately, it will be possible in upcoming Django version (3.0 and up) which should be released on 2nd of December.

The way to achieve it:

class De(models.Model):

    fr = models.BooleanField("[...]")
    de = models.SmallIntegerField("[...]")
    gd = models.SmallIntegerField("[...]")
    na = models.SmallIntegerField("[...]")
    # [several_attributes, Meta, __str__() removed for readability]

    def s_d(self):
        if self.fr:
            return self.de
        else:
            return self.gd + self.na
    s_d.admin_order_field = '_s_d'
    s_d = property(s_d)

Alternatively, you can create some decorator that will add any attribute to function, before converting it to property:

def decorate(**kwargs):
    def wrap(function):
        for name, value in kwargs.iteritems():
            setattr(function, name, value)

        return function
    return wrap

class De(models.Model):

    fr = models.BooleanField("[...]")
    de = models.SmallIntegerField("[...]")
    gd = models.SmallIntegerField("[...]")
    na = models.SmallIntegerField("[...]")
    # [several_attributes, Meta, __str__() removed for readability]

    @property
    @decorate(admin_order_field='_s_d')
    def s_d(self):
        if self.fr:
            return self.de
        else:
            return self.gd + self.na

Query Expressions | Django documentation, avantages: the calculated properties are transparently added to any from django.db.models import Sum, Case, Q, F, When, Value as V, and add admin_order_field attribute to enable sorting. How to implement sorting in Django Admin for calculated model properties without writing the logic twice? DurationField is used for input of a particular duration in the database. One can input date of submission, birthdate, etc. Till now we have discussed how to implement DurationField but how to use it in the view for performing the logical part. To perform some logic we would need to get the value entered into field into a python string instance.

Another possible solution might be to convert the s_d property to a model field and override the model save method to keep it up to date.

# models.py

class De(models.Model):

    fr = models.BooleanField("[...]")
    de = models.SmallIntegerField("[...]")
    gd = models.SmallIntegerField("[...]")
    na = models.SmallIntegerField("[...]")
    s_d = models.SmallIntegerField("[...]", blank=True)

    # [several_attributes, Meta, __str__() removed for readability]

    def save(self, *args, **kwargs):
        if self.fr:
            self.s_d = self.de
        else:
            self.s_d = self.gd + self.na
        super().save(*args, **kwargs)

# admin.py

class DeAdmin(admin.ModelAdmin):
    list_display = ("[...]", "s_d", "gd", "na", "de", "fr" )

The default sorting in admin.py will be applied and the value of s_d will be updated every time the model is saved.

There is a caveat to this method if you plan to do a lot of bulk operations, such as bulk_create, update, or delete.

Overridden model methods are not called on bulk operations

Note that the delete() method for an object is not necessarily called when deleting objects in bulk using a QuerySet or as a result of a cascading delete. To ensure customized delete logic gets executed, you can use pre_delete and/or post_delete signals.

Unfortunately, there isn’t a workaround when creating or updating objects in bulk, since none of save(), pre_save, and post_save are called.

3. How to enable sorting on calculated fields?, It is our responsibility and our honour to use our platform to amplify the voices of the expressions (documented below) that can be used to help you write queries. or using the double underscore lookup syntax. from django.db.models import A class attribute determining whether or not this aggregate function allows  Check it in the django admin interface Now lets’ customize django admin according to available options. Customize Django Admin Interface. 1. Change model name: If you want to change name of model which is States here so open model.py file and add verbose_name attribute in meta section. state/model.py

Advanced Models - Python Django Tutorials, Django adds sorting capabilities on fields which are attributes on the models. you wrote in the previous chapter (How to optimize queries in Django admin?).:. FilePathField – Django Models FilePathField is a CharField whose choices are limited to the filenames in a certain directory on the filesystem. FilePathField instances are created in your database as varchar columns with a default max length of 100 characters.

[PDF] Django Admin Cookbook, Dig much deeper into Django's models and comprehensively explore the defer​(), Do not retrieve the named fields from the database. Function parameters are the model fields to use to order the QuerySet. If the model is unordered, the sort order of the returned QuerySet will be e.manager <User: admin> >>>  Now we will apply a custom validation so that the above field is validated for google mail IDs only. Create a function which accepts an argument called value.One can apply any type of operation on value now. so let us check if our function value contains @gmail.com to be validated for google mail IDs only.

Django model property queryset, The usual way to create admin pages is to put all models in a single admin. Django adds sorting capabilities on fields which are attributes on the models. If you want to add sorting on a calculated field, you have to tell Django what to You start from the admin you wrote in the previous chapter (How to  Django queries help to create, retrieve, update and delete objects. But sometimes we need to get summered values from the objects. Then a Simple solution is to use Django aggregate feature Here are simple examples of how to use aggregation.

Comments
  • Very interesting discussion, thank you. I guess there's no way to share the code here, since in one case your working on the database level, as required for sorting and populating the display_list, while in the Model's property you manipulate the object attributes. Since Python and SQL are two very different beasts, I do believe you already did the best. Sometime the technology involved forces us to violate the DRY principle.
  • I stumbled on this problem again, and ended up with a DRY solution with sorting, filtering and more ... by mixing all suggestions found in this discussion. Thank you for great question. My final solution is summarised here: medium.com/@marioorlandi/…
  • It does not work, in the UI, I still can't sort by s_d
  • @rvernica is it possible that you may have some error in your case? I cannot really tell without seeing some code... You can try creating a question here on SO linked to this one if you are having difficulties...
  • Unfortunately, it is hard to recreate it. If you tested your solution and confirm that it works, I'll remove my comments.
  • Thank you very much for this preview to Django 3!
  • I just tested this solution - it does not seem to work when you have to calculate the value but only if you can refer to an existing (sortable) attribute (in your example _s_d (which does not exist).