通过相关模型上的过滤器对 list_display 字段进行排序

Posted

技术标签:

【中文标题】通过相关模型上的过滤器对 list_display 字段进行排序【英文标题】:Ordering list_display fields by a filter on a related model 【发布时间】:2014-05-17 14:24:29 【问题描述】:

我遇到了一个问题,我想根据管理员中相关字段的聚合进行排序,但不确定最佳解决方法。这是我的模型:

# models.py   
from django.db import models

class Waiter(models.Model):
    pass

class Customer(models.Model):
    pass

class Meal(models.Model):
    BREAKFAST = 1
    LUNCH = 2
    DINNER = 3
    MEAL_TYPE_CHOICES = (
            (BREAKFAST, 'Breakfast'),
            (LUNCH, 'Lunch'),
            (DINNER, 'Dinner'),
            )
    meal_type = models.IntegerField(choices=MEAL_TYPE_CHOICES)
    customer = models.ForeignKey(Customer)
    waiter = models.ForeignKey(Waiter)
    service_rating = models.IntegerField()

在服务员管理中,我想显示早餐、午餐和晚餐每个服务员的平均服务评分,并希望能够按每个服务员作为单独的列进行订购。

This question 解释了如何按每位服务员的所有服务评分的平均值进行订购(代码如下),但我真的很想按每种餐食类型的相应服务评分进行订购。最好的方法是什么?

# admin.py
from .models import Customer, Meal, Waiter

from django.contrib import admin
from django.db.models import Avg

class WaiterAdmin(admin.ModelAdmin):
    list_display('avg_breakfast_rating', 'avg_lunch_rating', 'avg_dinner_rating')

    def queryset(self, request):
        qs = super(WaiterAdmin, self).queryset(request)
        qs = qs.annotate(Avg('meal__service_rating'))
        return qs

    def avg_breakfast_rating(self, obj):
        breakfasts = Meal.objects.filter(waiter=obj, meal_type=Meal.BREAKFAST)
        return breakfasts.aggregate(avg_rating=Avg('service_rating'))['avg_rating']
    avg_breakfast_rating.short_description = 'Average Breakfast Rating'
    avg_breakfast_rating.admin_order_field = 'meal__service_rating__avg'

    def avg_lunch_rating(self, obj):
        lunches = Meal.objects.filter(waiter=obj, meal_type=Meal.LUNCH)
        return lunches.aggregate(avg_rating=Avg('service_rating'))['avg_rating']
    avg_lunch_rating.short_description = 'Average Lunch Rating'
    avg_lunch_rating.admin_order_field = 'meal__service_rating__avg'

    def avg_dinner_rating(self, obj):
        dinners = Meal.objects.filter(waiter=obj, meal_type=Meal.DINNER)
        return dinners.aggregate(avg_rating=Avg('service_rating'))['avg_rating']
    avg_dinner_rating.short_description = 'Average Dinner Rating'
    avg_dinner_rating.admin_order_field = 'meal__service_rating__avg'

基本上,我不想让 admin_order_field 成为 meal__service_rating__avg,而是希望有类似 breakfast__service_rating__avglunch__service_rating__avgdinner__service_rating__avg 的东西来订购。

【问题讨论】:

【参考方案1】:

我认为您不能仅使用 ORM 来做到这一点(至少,我找不到方法)。但是,如果您愿意使用 extra() 和一些自定义 SQL,则有一个比较简单的解决方案。

具体情况可能因数据库而异,但这似乎适用于 sqlite3:

class WaiterAdmin(admin.ModelAdmin):
    list_display = ('name', 'breakfast_avg', 'lunch_avg', 'dinner_avg')

    def _avg_query(self, meal_type):
        return """
               SELECT AVG(service_rating) FROM meal_table
               WHERE meal_type=meal_type AND waiter_id=waiter_table.id
               """.format(meal_table=Meal._meta.db_table,
                          waiter_table=Waiter._meta.db_table,
                          meal_type=meal_type)


    def queryset(self, request):
        qs = super(WaiterAdmin, self).queryset(request)
        qs = qs.extra('breakfast_avg': self._avg_query(Meal.BREAKFAST),
                       'lunch_avg': self._avg_query(Meal.LUNCH),
                       'dinner_avg': self._avg_query(Meal.DINNER),
                      )
        return qs

    def breakfast_avg(self, obj):
        return obj.breakfast_avg
    breakfast_avg.short_description = 'Average Breakfast Rating'
    breakfast_avg.admin_order_field = 'breakfast_avg'

    def lunch_avg(self, obj):
        return obj.lunch_avg
    lunch_avg.short_description = 'Average Lunch Rating'
    lunch_avg.admin_order_field = 'lunch_avg'

    def dinner_avg(self, obj):
        return obj.dinner_avg
    dinner_avg.short_description = 'Average Dinner Rating'
    dinner_avg.admin_order_field = 'dinner_avg'

【讨论】:

谢谢!我通常使用postgres,所以我会调整语法并尝试一下。目前正在旅行,所以可能需要在我回来时进行测试。【参考方案2】:

我会将这些字段添加到 Waiter:

breakfast_service_rating_avg = models.FloatField(null=True,blank=True)
lunch_service_rating_avg = models.FloatField(null=True,blank=True)
dinner_service_rating_avg = models.FloatField(null=True,blank=True)

然后,Meal 的 save() 方法将调用 Waiter 实例的 save() 方法,您可以在其中计算 *_service_rating_avg 的三个平均值,然后再保存 Waiter 实例。

然后你总是有服务员实例的平均值,你可以在管理中轻松排序。注释也是一项相当昂贵的操作,如果可能的话,我会避免它并提供提到的附加字段。

希望对你有帮助!

【讨论】:

这是一个很好的建议!另一个答案更适合我在不向数据库添加其他字段的情况下尝试做的事情。我不确定注释实际上有多昂贵。我很确定所有工作都在数据库中完成,所以它应该比在 Python 中做一些事情快,尽管比简单的数据库操作慢。我会看看我是否可以运行一些基准测试来获取一些数据。

以上是关于通过相关模型上的过滤器对 list_display 字段进行排序的主要内容,如果未能解决你的问题,请参考以下文章

Django Admin list_display 添加列[统计相关ForeignKey的记录]

更改列表页面上的 django admin 附加字段

在 list_display 中显示内联模型字段

Django:如何在 list_display 中包含内联模型字段?

Django admin list_display 不显示模型方法返回项

Django admin list_display模型方法