Administrador de Django: cómo ordenar por uno de los campos list_display personalizados que no tiene un campo de base de datos

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

¿Cómo puedo clasificar a los Clientes, dependiendo de number_of_ordersque tengan?

admin_order_fieldLa propiedad no se puede usar aquí, ya que requiere un campo de base de datos para ordenar. ¿Es posible en absoluto, ya que Django se basa en la base de datos subyacente para realizar la clasificación? Crear un campo agregado para contener la cantidad de pedidos parece exagerado aquí.

Lo divertido: si cambia la URL a mano en el navegador para ordenar en esta columna, ¡funciona como se esperaba!

mike_k
fuente
"Lo divertido: si cambia la URL a mano en el navegador para ordenar en esta columna, ¡funciona como se esperaba!" Te refieres a: / admin / myapp / customer /? Ot = asc & o = 2 ¿Estás seguro?
Andy Baker
sí, tanto asc como dsc. Quizás solo funcione con decimales.
mike_k
No creo que funcione con varias páginas.
Chase Seibert

Respuestas:

159

Me encantó la solución de Greg a este problema, pero me gustaría señalar que puede hacer lo mismo directamente en el administrador:

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'

De esta manera, solo realiza anotaciones dentro de la interfaz de administración. No con cada consulta que haces.

bbrik
fuente
5
Sí, esta es una forma mucho mejor. :)
Greg
2
Hay una edición sugerida en esta respuesta. Voté para rechazarlo porque eliminó demasiado texto. No conozco a Django, no tengo idea de si vale la pena mencionar el cambio de código propuesto.
Gilles 'SO- deja de ser malvado'
1
@Gilles, la edición sugerida es correcta sobre una definición más simple de number_of_orders. Esto funciona: def number_of_orders(self, obj): return obj.order__count
Nils
1
¿No debería ser en get_queryset()lugar de queryset()?
Mariusz Jamro
2
debe ser get_queryset (self, request): ... para Django 1.6+
michael
50

No lo he probado (me interesaría saber si funciona), pero ¿qué pasa con la definición de un administrador personalizado para el Customerque se incluye la cantidad de pedidos agregados y luego configurar admin_order_fieldese agregado, es decir

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'

EDITAR: Acabo de probar esta idea y funciona perfectamente, ¡no se requiere una subclase de administrador de django!

Greg
fuente
1
Esta es una mejor respuesta en comparación con la aceptada. El problema que encontré al aplicar el aceptado es cuando busca algo junto con ese conjunto de consultas actualizado en el nivel de administrador, lleva demasiado tiempo y también obtiene un recuento incorrecto de los resultados que encontró.
Mutant
0

La única forma que se me ocurre es desnormalizar el campo. Es decir, cree un campo real que se actualice para mantenerse sincronizado con los campos de los que se deriva. Por lo general, hago esto anulando el guardado en el modelo con los campos desnormalizados o el modelo del que se deriva:

# 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
fuente
1
Esto ciertamente debería funcionar, pero no puede marcarlo como aceptado debido al campo DB adicional involucrado. También tenga en cuenta que falta .count () al final de la línea de conjunto de consultas.
mike_k
fijo el recuento (). La única otra solución (salvo subclasificar grandes trozos de contrib.admin) sería un truco de Jquery / Ajaxy.
Andy Baker