web-dev-qa-db-fra.com

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

# 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 puis-je trier les clients en fonction de number_of_orders Qu'ils ont?

La propriété admin_order_field 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? La création d'un champ agrégé pour contenir le nombre de commandes semble être une surpuissance ici.

Le plus amusant: si vous modifiez l'URL à la main dans le navigateur pour trier sur cette colonne - cela fonctionne comme prévu!

110
mike_k

J'ai adoré la solution de Greg à ce problème, mais je voudrais souligner que vous pouvez faire la même chose directement dans l'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'

De cette façon, vous annotez uniquement à l'intérieur de l'interface d'administration. Pas avec chaque requête que vous faites.

144
bbrik

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 Customer qui inclut le nombre de commandes agrégées, puis de définir admin_order_field à 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 - pas de sous-classement admin Django requis!

47
Greg

La seule façon dont je peux penser est de dénormaliser le champ. C'est-à-dire - créer un vrai champ qui est mis à jour pour rester synchronisé avec les champs dont il est dérivé. Je le fais généralement en remplaçant l'enregistrement sur le modèle par 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[...]
0
Andy Baker