53

Suppose I have a Book model containing a foreign key to a Publisher model.

How can I display in the Django admin a column with the number of books published by each publisher, in a way that I can use the built-in sorting?

GJ.
  • 5,226
  • 13
  • 59
  • 82
  • https://books.agiliq.com/projects/django-admin-cookbook/en/latest/sorting_calculated_fields.html should be the right place to look at – phy25 Aug 26 '19 at 00:49

5 Answers5

87

I had the same issue (I cannot change my model's manager to add slow annotations or joins). A combination of two of the answers here works. @Andre is really close, the Django Admin supports modifying the queryset for just the admin, so apply the same logic here and then user the admin_order_field attribute. You still need to add the new admin field to list_display, of course.

from django.db.models import Count

class EventAdmin(admin.ModelAdmin)
    list_display = (..., 'show_artist_count')

    def queryset(self, request):
    # def get_queryset(self, request):    for Django 1.6+
        qs = super(EventAdmin, self).queryset(request)
        return qs.annotate(artist_count=Count('artists'))

    def show_artist_count(self, inst):
        return inst.artist_count
    show_artist_count.admin_order_field = 'artist_count'
Andy Baker
  • 21,158
  • 12
  • 58
  • 71
Lincoln B
  • 2,184
  • 1
  • 13
  • 12
  • 4
    Is there anyway to set this as the default ordering method? ordering = ('artist_count') for example. Fails for me with field doesn't exist. – Nathan Keller Jun 10 '13 at 04:16
  • 5
    don't forget to add this line: from django.db.models import Count – Ali Aug 18 '13 at 18:19
  • Simplest and leanest way IMO if you only need ordering for the admin page (as the OP stated). There should be no need to alter the Event model's Manager when only a change in admin is required – Patrick Jun 14 '14 at 14:10
  • 15
    [Since Django 1.6](https://docs.djangoproject.com/en/1.6/ref/contrib/admin/#django.contrib.admin.ModelAdmin.get_queryset) the ModelAdmin method is named **get_queryset** and not **queryset**. – Jocelyn delalande Apr 17 '15 at 14:30
  • 1
    It works for the initial load, but if I search on the object, on matching results it gives wrong counts. Not sure how it comes up with some strange number. – Mutant Aug 15 '17 at 19:06
  • 3
    `queryset` becomes `get_queryset` also on call to `super()` for Django 1.6+: `qs = super(EventAdmin, self).get_queryset(request)` – smilebomb May 31 '18 at 18:43
  • @Mutant To solve this problem, just add `distinct=True` to the annotation i.e. `qs.annotate(artist_count=Count('artists', distinct=True))`. I found the answer here: http://books.agiliq.com/projects/django-admin-cookbook/en/latest/sorting_calculated_fields.html – CapedHero Mar 12 '19 at 17:04
  • 1
    @smilebomb you can even remove the contents of the `super` function and just wirte `qs = super().get_queryset(request)` now. – Brachamul Dec 08 '19 at 20:07
  • how do you add that to the default `ordering` attribute of the admin model? – Tjorriemorrie Oct 20 '20 at 10:25
8

Try this:

make a new Manager (and aggregate with count on the book relation field):

class PublisherManager(models.Manager):
    def get_query_set(self):
        return super(PublisherManager,self).get_query_set().annotate(pubcount=Count('book'))

sort it on pubcount:

class Publisher(models.Model):
    ......
    objects = PublisherManager()

    class Meta:
        ordering = ('pubcount',)
Andre Bossard
  • 6,191
  • 34
  • 52
  • 1
    Oooh - that is pretty neat trick, but bear in mind that it'll add an annotation to everything you look up using that extended objects manager. If that's a problem, you could always create a second, standard manager without the aggregate and attach that as 'standard_objects', too. But that does increase complexity, obv – Steve Jalim Aug 16 '10 at 09:34
  • 2
    @stevejalim, that's right. If the counter doesn't have to be "live", you could also have a book_count Field on the publisher and updated it with a signal, whenever a book is saved..... – Andre Bossard Aug 16 '10 at 10:30
  • 1
    when I try this I'm getting an error: "ordering" refers to "pubcount", a field that doesn't exist. – GJ. Aug 16 '10 at 17:13
  • 1
    also, how do you integrate this as a sortable column in the admin? – GJ. Aug 16 '10 at 18:01
  • 1
    @GJ. have you fixed this problem? I am having the same problem right now. – est Apr 11 '12 at 06:25
  • @est did you see the solution below? – GJ. Apr 11 '12 at 13:01
  • @GJ Sorry I didn't mean that. How do you specify default order of a model according to an `annotate()` value? – est Apr 13 '12 at 06:40
1

You should indeed start off with adding:

class PublisherManager(models.Manager):
    def get_query_set(self):
        return super(PublisherManager,self).get_query_set().annotate(pubcount=Count('book'))

But the correct way to add it as a sortable field is:

class Publisher(models.Model):
    ......
    objects = PublisherManager()

    def count(self):
        return self.pubcount
    count.admin_order_field = 'pubcount'

And then you can just add 'count' to the list_display attribute of model admin in admin.py

GJ.
  • 5,226
  • 13
  • 59
  • 82
0

Lincoln B's answer was the right way for me.

At first I wanted to just comment on his solution, but I actually found myself solving a slightly different problem. I had an admin class, which I wanted to "customize" to my needs - namely the django-taggit admin. In one of my application's admin.py, I added:

# sort tags by name in admin (count items also possible)
from taggit.admin import TagAdmin
TagAdmin.ordering = ["name"]
#   make sortable on item_count:
# 1. function for lookup
def item_count(obj):
    """This takes the item_count from object: didn't work as model field."""
    return obj.item_count # not needed: obj.taggit_taggeditem_items.count()
# 2. property in function - admin field name
item_count.admin_order_field = 'item_count'
# 3. queryset override, with count annotation
from django.db.models import Count
TagAdmin.queryset = lambda self, request: super(TagAdmin, self).queryset(request).annotate(item_count=Count('taggit_taggeditem_items'))
# 4. add to list display
TagAdmin.list_display = ["name", item_count]

The interesting observation for me was, that I could not just annotate the queryset, and add "item_count" to list_display - because there was no item_count method in TagAdmin, nor a method or field in the Tag model class (only in the queryset).

Tomasz Gandor
  • 8,235
  • 2
  • 60
  • 55
-2

Try something like this:

class PublisherAdminWithCount(Admin):

    def book_count(self, obj):
        return obj.book_set.count()

    list_display = ('user_count',)

admin.site.register(Group, PublisherAdminWithCount)
gruszczy
  • 40,948
  • 31
  • 128
  • 181