Question

I'm having an issue where I want to sort based on the aggregate of a related field in the admin and not sure of the best way to go about it. Here are my models:

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

In the waiter admin, I want to display the average service rating for each waiter for breakfast, lunch and dinner and want to be able to order by each of these as a separate column.

This question explained how to order by the average of all service ratings for each waiter (code below), but I'd really like to order by the appropriate service ratings for each meal type. What would be the best way to do this?

# 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'

Basically, instead of having the admin_order_field be meal__service_rating__avg, I want to have something like breakfast__service_rating__avg, lunch__service_rating__avg and dinner__service_rating__avg to order on.

Was it helpful?

Solution

I don't think you can do this purely with the ORM (at least, I couldn't find a way). However, there is a somewhat straightforward solution if you are willing to use extra() and some custom SQL.

Specifics may vary based on database, but this appears to work with 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'

OTHER TIPS

I would add these fields to 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)

Then the save() method of Meal would call the save() method of the Waiter instance where you calculate the three averages for *_service_rating_avg before saving the Waiter instance.

Then you always have the average on the waiter instance and you can comfortably sort in the admin. Also to annotate is quite an expensive operation, I'd avoid it if possible and provide for the mentioned additional fields.

I hope this helps you out!

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top