142
# admin.py
class CustomerAdmin(admin.ModelAdmin):  
    list_display = ('foo', 'number_of_orders')


# models.py
class Order(models.Model):
    bar = models.CharField[...]
    customer = models.ForeignKey(Customer)

class Customer(models.Model):
    foo = models.CharField[...]

    def number_of_orders(self):
        return u'%s' % Order.objects.filter(customer=self).count()  

How could I sort Customers, depending on number_of_orders they have?

admin_order_field property can't be used here, as it requires a database field to sort on. Is it possible at all, as Django relies on the underlying DB to perform sorting? Creating an aggregate field to contain the number of orders seems like an overkill here.

The fun thing: if you change url by hand in the browser to sort on this column - it works as expected!

nik_m
  • 11,825
  • 4
  • 43
  • 57
mike_k
  • 1,831
  • 2
  • 14
  • 12

3 Answers3

185

I loved Greg's solution to this problem, but I'd like to point that you can do the same thing directly in the admin:

from django.db import models

class CustomerAdmin(admin.ModelAdmin):
    list_display = ('number_of_orders',)

    def get_queryset(self, request):
    # def queryset(self, request): # For Django <1.6
        qs = super(CustomerAdmin, self).get_queryset(request)
        # qs = super(CustomerAdmin, self).queryset(request) # For Django <1.6
        qs = qs.annotate(models.Count('order'))
        return qs

    def number_of_orders(self, obj):
        return obj.order__count
    number_of_orders.admin_order_field = 'order__count'

This way you only annotate inside the admin interface. Not with every query that you do.

charleschenster
  • 252
  • 3
  • 8
bbrik
  • 2,936
  • 1
  • 19
  • 7
  • 6
    Yes, this is a much better way. :) – Greg Jan 24 '12 at 04:01
  • 2
    There's a [suggested edit](http://stackoverflow.com/suggested-edits/240451) on this answer. I voted to reject it because it removed too much text. I don't know Django, I have no idea whether the proposed code change is worth mentioning. – Gilles 'SO- stop being evil' Apr 14 '12 at 20:32
  • You can annotate with a name: `qs = qs.annotate(number_of_orders=models.Count('order'))`, and then just write: `number_of_orders.admin_order_field = 'number_of_orders'`, to avoid django's double underscore automatic field name. – Tomasz Gandor Sep 02 '13 at 07:01
  • 1
    @Gilles the suggested edit is correct about a simpler number_of_orders definition. This works: `def number_of_orders(self, obj): return obj.order__count` – Nils Nov 15 '13 at 18:43
  • In addition to Eric's comment (2 arguments self and obj) later on there should be ... Order.objects.filter(customer=obj)... not customer=self. Anyway thanks, just figured it out thanks to this magnificent question. – gwaramadze Dec 22 '13 at 18:34
  • Hi, I've made the edits you suggested in the comments. Great collaboration! :) – Emil Stenström May 11 '14 at 23:44
  • How to do this with a GenericForeignKey? http://stackoverflow.com/questions/34488496/django-admin-sort-by-genericforeignkeys-field – Aakash Rayate Dec 28 '15 at 06:11
  • 1
    Shoudn't that be `get_queryset()` instead of `queryset()` ? – Mariusz Jamro Feb 22 '16 at 13:42
  • 2
    should be get_queryset(self, request):... for Django 1.6+ – michael Mar 02 '16 at 16:18
  • Greg's answer has the added advantage (or disadvantage, from a performance POV) that it adds the annotation for all query_set calls (objects.filter call) – pranshus Dec 04 '17 at 14:03
  • Works fine. but when I have search option enabled and try to search based on some other field on this list display, it gets the wrong count and also takes a long time to load. I am assuming its order of operation in which it tries to perform search and calc the count on the custom field is causing it. Anyone else has run into it? – Mutant Mar 21 '18 at 18:54
  • Beautiful! So `get_queryset` is one of the many [hooks](https://docs.djangoproject.com/en/2.2/ref/contrib/admin/#modeladmin-methods) I can override whenever I want to customize a ModelAdmin? Really need a tutorial on when to override what about there hooks. – John Wang Apr 17 '19 at 23:22
  • This should work for fields that can be retrieved by aggregation but not sure if we have a way for a bit complicated custom fields. e.g. `def theme_names(self, obj) -> str: return ','.join(obj.theme.all().values_list('name', flat=True))` – JM217 Dec 14 '21 at 11:54
52

I haven't tested this out (I'd be interested to know if it works) but what about defining a custom manager for Customer which includes the number of orders aggregated, and then setting admin_order_field to that aggregate, ie

from django.db import models 


class CustomerManager(models.Manager):
    def get_query_set(self):
        return super(CustomerManager, self).get_query_set().annotate(models.Count('order'))

class Customer(models.Model):
    foo = models.CharField[...]

    objects = CustomerManager()

    def number_of_orders(self):
        return u'%s' % Order.objects.filter(customer=self).count()
    number_of_orders.admin_order_field = 'order__count'

EDIT: I've just tested this idea and it works perfectly - no django admin subclassing required!

Greg
  • 9,963
  • 5
  • 43
  • 46
  • 1
    This is better answer compared to the accepted one. The issue I ran into applying the accepted one is when you search something along with that updated queryset at the admin level, it takes too much time and also comes up with wrong count for the results it found. – Mutant Mar 22 '18 at 11:30
0

The only way I can think of is to denormalize the field. That is - create a real field that get's updated to stay in sync with the fields it is derived from. I usually do this by overriding save on eith the model with the denormalized fields or the model it derives from:

# models.py
class Order(models.Model):
    bar = models.CharField[...]
    customer = models.ForeignKey(Customer)
    def save(self):
        super(Order, self).save()
        self.customer.number_of_orders = Order.objects.filter(customer=self.customer).count()
        self.customer.save()

class Customer(models.Model):
    foo = models.CharField[...]
    number_of_orders = models.IntegerField[...]
Andy Baker
  • 21,158
  • 12
  • 58
  • 71
  • 1
    This certainly should work, but can't mark it as accepted due to extra DB field involved. Also note missing .count() at the end of query-set line. – mike_k Feb 02 '10 at 17:08
  • fixed the count(). The only other solution (short of subclassing large chunks of contrib.admin) would be a Jquery/Ajaxy hack. – Andy Baker Feb 03 '10 at 08:21