Search code examples
djangodjango-filter

I want to filter the def 'getAmountRating(self):' function


I built the class Restaurant where you can find the 'def getAmountRating(self):' function. This function should be used as a keyword in my django_filter.

class Restaurant(models.Model):
restaurant_picture = models.ImageField(null=True, default='dashboard-BG.jpg')
name = models.CharField(max_length=200, null=True)

address = models.CharField(max_length=128, blank=True)
houseNumber = models.IntegerField(default=1)

city = models.CharField(max_length=64, default="")
state = models.CharField(max_length=64, default="")
zip_code = models.CharField(max_length=5, default="86444")

tags = models.ManyToManyField(Tag)
affordability = models.FloatField(validators=[MaxValueValidator(3), MinValueValidator(1)], null=True)
objects = models.Manager()

def getAverageRating(self):
    comments = Comment.objects.all()
    avg = 0
    count = 0
    for i in comments:
        if i.restaurant == self:
            avg += i.ratings
            if count is 0:
                count += 1
            else:
                avg = avg / 2
    if avg is not 0:
        avg = round(avg)
    return avg

In the filter

class RestaurantFilter(django_filters.FilterSet):

rating = NumberFilter(field_name="getAverageRating", lookup_expr="gte")

class Meta:
    model = Restaurant
    fields = '__all__'
    exclude = ['location', 'restaurant_picture', 'address', 'houseNumber', 'state']

I already had to accept that this is not working as I'd like to.

As a fix I looked up some solutions but those solutions did not work. For example I built an other class containing the def getAverageRating function and referenced this class in Restaurant as a - averageRating = RestaurantRating() - which did not work.

I don't think its relevant but here is the views.py

def restaurants(request):
restaurants = Restaurant.objects.all()

foods = Food.objects.all()
comments = Comment.objects.all()

myFilter = RestaurantFilter(request.GET, queryset=restaurants)
restaurants = myFilter.qs

context = {'restaurants': restaurants, 'myFilter': myFilter, 'foods': foods, 'comments': comments}
return render(request, 'accounts/restaurants.html', context)

I hope someone finds the time to help me with my issue. Thx in advande.


Solution

  • You can not filter, annotate or aggregate on properties or methods. Properties and methods are defined on the Python/Django model, but the database does not know anything about these. Since Querysets are translated into database queries, using a method, property does not make much sense.

    It would not be efficient anyway if it was possible. It would mean you fetch all the Restaurants in memory and then you filter on each restaurant at the Python/Django level. Python is not ideal to filter huge amounts of data, databases are designed to do this.

    We however do not need this. A database can determine the average of a related field itself. It has aggregates and it can group by to obtain the average per restaurant. In SQL you would construct a query like:

    SELECT restaurant.*, AVG(comment.rating)
    FROM restaurant
    LEFT OUTER JOIN comment ON comment.restaurant_id = restaurant.id
    GROUP BY restaurant.id

    you can do this with Django as well. Indeed, we can .annotate(…) [Django-doc] the restaurants with the average rating of the restaurants with:

    from django.db.models import Avg
    
    Restaurant.objects.annotate(
        avg_rating=Avg('comment__ratings')
    )

    We can use this in our FilterSet with:

    class RestaurantFilter(django_filters.FilterSet):
        rating = NumberFilter(field_name='avg_rating', lookup_expr='gte')
        class Meta:
            model = Restaurant
            exclude = [
                'location'
              , 'restaurant_picture'
              , 'address'
              , 'houseNumber'
              , 'state'
              ]

    and in the view we thus can filter with:

    from django.db.models import Avg
    
    def restaurants(request):
        restaurants = Restaurant.objects.annotate(
            avg_rating=Avg('comment__ratings')
        )
    
        foods = Food.objects.all()
        comments = Comment.objects.all()
    
        myFilter = RestaurantFilter(request.GET, queryset=restaurants)
        restaurants = myFilter.qs
    
        context = {'restaurants': restaurants, 'myFilter': myFilter, 'foods': foods, 'comments': comments}
        return render(request, 'accounts/restaurants.html', context)