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.
If you want to add sorting on a calculated field, you have to tell Django what to pass to order_by . You can do this by setting the admin_order_field attribute on the calculated field method.
The Django framework comes with a powerful administrative tool called admin. You can use it out of the box to quickly add, delete, or edit any database model from a web interface. But with a little extra code, you can customize the Django admin to take your admin capabilities to the next level.
It is used to create the form presented on both the add/change pages. You can easily provide your own ModelForm to override any default form behavior on the add/change pages. Alternatively, you can customize the default form rather than specifying an entirely new one by using the ModelAdmin. get_form() method.
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, settingshort_description
on a property is only possible when using theproperty()
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")
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With