Admin Django: comment trier par l'un des champs list_display personnalisés qui n'a pas de champ de base de données

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

Comment pourrais-je trier les clients, selon number_of_ordersqu'ils ont?

admin_order_fieldLa propriété ne peut pas être utilisée ici, car elle nécessite un champ de base de données pour effectuer le tri. Est-ce possible du tout, car Django s'appuie sur la base de données sous-jacente pour effectuer le tri? Créer un champ agrégé pour contenir le nombre de commandes semble ici exagéré.

La chose amusante: si vous changez l'url à la main dans le navigateur pour trier cette colonne - cela fonctionne comme prévu!

mike_k
la source
"La chose amusante: si vous changez l'url à la main dans le navigateur pour trier sur cette colonne - cela fonctionne comme prévu!" Vous voulez dire comme: / admin / myapp / customer /? Ot = asc & o = 2 Êtes-vous sûr?
Andy Baker
ouais, à la fois asc et dsc. Peut-être que cela fonctionne uniquement avec des décimales.
mike_k
Je ne pense pas que cela fonctionnerait avec plusieurs pages.
Chase Seibert le

Réponses:

159

J'ai adoré la solution de Greg à ce problème, mais j'aimerais souligner que vous pouvez faire la même chose directement dans l'administrateur:

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 cette façon, vous n'annotez que dans l'interface d'administration. Pas avec chaque requête que vous faites.

bbrik
la source
5
Oui, c'est une bien meilleure façon. :)
Greg
2
Il y a une modification suggérée pour cette réponse. J'ai voté pour le rejeter car il supprimait trop de texte. Je ne connais pas Django, je ne sais pas si le changement de code proposé mérite d'être mentionné.
Gilles 'SO- arrête d'être diabolique'
1
@Gilles la modification suggérée est correcte à propos d'une définition plus simple de number_of_orders. Cela fonctionne: def number_of_orders(self, obj): return obj.order__count
Nils
1
Cela ne devrait-il pas être à la get_queryset()place queryset()?
Mariusz Jamro
2
devrait être get_queryset (self, request): ... pour Django 1.6+
michael
50

Je n'ai pas testé cela (je serais intéressé de savoir si cela fonctionne) mais qu'en est-il de la définition d'un gestionnaire personnalisé pour Customerlequel inclut le nombre de commandes agrégées, puis de la définition admin_order_fieldde cet agrégat, c'est-à-dire

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: Je viens de tester cette idée et cela fonctionne parfaitement - aucune sous-classification d'administrateur de Django n'est requise!

Greg
la source
1
C'est une meilleure réponse par rapport à celle acceptée. Le problème que j'ai rencontré lors de l'application de celui accepté est que lorsque vous recherchez quelque chose avec cet ensemble de requêtes mis à jour au niveau de l'administrateur, cela prend trop de temps et génère également un mauvais décompte des résultats trouvés.
Mutant
0

La seule façon dont je peux penser est de dénormaliser le champ. Autrement dit, créez un champ réel qui est mis à jour pour rester synchronisé avec les champs dont il est dérivé. Je fais généralement cela en remplaçant save sur le modèle avec les champs dénormalisés ou le modèle dont il dérive:

# 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
la source
1
Cela devrait certainement fonctionner, mais ne peut pas le marquer comme accepté en raison du champ DB supplémentaire impliqué. Notez également .count () manquant à la fin de la ligne du jeu de requêtes.
mike_k
a corrigé le nombre (). La seule autre solution (à moins de sous-classer de gros morceaux de contrib.admin) serait un hack Jquery / Ajaxy.
Andy Baker