Администратор Django: как сортировать по одному из настраиваемых полей list_display, у которого нет поля базы данных

122
# 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()  

Как я могу отсортировать клиентов в зависимости от number_of_ordersих наличия?

admin_order_fieldСвойство здесь использовать нельзя, так как для сортировки требуется поле базы данных. Возможно ли это вообще, поскольку Django полагается на базовую БД для выполнения сортировки? Создание агрегированного поля, содержащего количество заказов, здесь кажется излишним.

Самое интересное: если вы вручную измените URL-адрес в браузере для сортировки по этому столбцу - все будет работать, как ожидалось!

mike_k
источник
«Самое интересное: если вы вручную измените URL-адрес в браузере для сортировки по этому столбцу - все будет работать, как ожидалось!» Вы имеете в виду: / admin / myapp / customer /? Ot = asc & o = 2 Вы уверены?
Энди Бейкер
да, и asc, и dsc. Может, он просто работает с десятичными знаками.
mike_k
Я не думаю, что это сработает с несколькими страницами.
Чейз Зайберт,

Ответы:

159

Мне понравилось решение этой проблемы Грегом, но я хотел бы отметить, что вы можете сделать то же самое прямо в админке:

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'

Таким образом, вы делаете аннотации только в интерфейсе администратора. Не с каждым вашим запросом.

bbrik
источник
5
Да, это намного лучший способ. :)
Грег
2
Там это предложено изменить на этот ответ. Я проголосовал за его отклонение, потому что он удалил слишком много текста. Я не знаю Django, понятия не имею, стоит ли упоминать предлагаемое изменение кода.
Жиль 'SO- перестань быть злым'
1
@Gilles: предложенная редакция верна в отношении более простого определения number_of_orders. Это работает: def number_of_orders(self, obj): return obj.order__count
Nils
1
Разве это не должно быть get_queryset()вместо queryset()?
Мариуш Ямро,
2
должно быть get_queryset (self, request): ... для Django 1.6+
Майкл
50

Я не тестировал это (мне было бы интересно узнать, работает ли он), но как насчет определения настраиваемого менеджера, для Customerкоторого включается количество агрегированных заказов, а затем установки admin_order_fieldэтого агрегата, т.е.

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'

РЕДАКТИРОВАТЬ: Я только что протестировал эту идею, и она отлично работает - подкласс администратора django не требуется!

Greg
источник
1
Это лучший ответ по сравнению с принятым. Проблема, с которой я столкнулся при применении принятого, заключается в том, что когда вы ищете что-то вместе с этим обновленным набором запросов на уровне администратора, это занимает слишком много времени, а также дает неправильный счет для найденных результатов.
Mutant
0

Единственный способ, который я могу придумать, - это денормализовать поле. То есть - создать реальное поле, которое будет обновляться для синхронизации с полями, из которых оно получено. Я обычно делаю это, отменяя save на модели с денормализованными полями или моделью, из которой она получена:

# 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[...]
Энди Бейкер
источник
1
Это, безусловно, должно работать, но не может быть помечено как принятое из-за задействованного дополнительного поля БД. Также обратите внимание на отсутствие .count () в конце строки запроса.
mike_k 02
исправлен count (). Единственным другим решением (если не считать подкласса больших кусков contrib.admin) будет взлом JQuery / Ajaxy.
Энди Бейкер