How to calculate average of some field in Django models and send it to rest API?

Viewed 1255

I want to count the average of ratings ( in Reviews model ) and send it to my API.

Models.py

from django.db import models
from adminuser.models import Categories
from accounts.models import UserAccount as User
from django.core.validators import MaxValueValidator, MinValueValidator

# Create your models here.
class Gigs(models.Model):
    title = models.CharField(max_length=255)
    category = models.ForeignKey(Categories , on_delete=models.CASCADE)
    price = models.DecimalField(max_digits=6, decimal_places=2)
    details = models.TextField()
    seller = models.ForeignKey(User,default=None, on_delete=models.CASCADE)

class Reviews(models.Model):
    rating = models.SmallIntegerField( default=0,validators=[MaxValueValidator(5),MinValueValidator(1)])
    comment = models.CharField(max_length=500)
    item =  models.ForeignKey(Gigs , on_delete=models.CASCADE)
    buyer = models.ForeignKey(User ,default=None, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)

Views.py

from django.shortcuts import render
from .models import Gigs,Reviews
from .serializers import GigsSerializer,ReviewsSerializer
from rest_framework.generics import GenericAPIView
from rest_framework.mixins import ListModelMixin, CreateModelMixin , RetrieveModelMixin , DestroyModelMixin, UpdateModelMixin
from rest_framework.permissions import AllowAny
# Create your views here.

#List and create (pk not required)
class GigsListAPI(GenericAPIView, ListModelMixin ):
    def get_queryset(self):
       username = self.kwargs['user']
       return Gigs.objects.filter(seller=username)
    serializer_class = GigsSerializer
    permission_classes = (AllowAny,)

    def get(self, request , *args, **kwargs):
        return self.list(request, *args, **kwargs)
class GigsListCategorywise(GenericAPIView, ListModelMixin ):
    def get_queryset(self):
       SearchedCategory = self.kwargs['category']
       return Gigs.objects.filter(category=SearchedCategory)
    serializer_class = GigsSerializer
    permission_classes = (AllowAny,)

    def get(self, request , *args, **kwargs):
        return self.list(request, *args, **kwargs)

class GigsListAll(GenericAPIView, ListModelMixin ):
    queryset = Gigs.objects.all()
    serializer_class = GigsSerializer
    permission_classes = (AllowAny,)

    def get(self, request , *args, **kwargs):
        return self.list(request, *args, **kwargs)

class GigsCreateAPI(GenericAPIView, CreateModelMixin):
    queryset = Gigs.objects.all()
    serializer_class = GigsSerializer
    permission_classes = (AllowAny,)

    def post(self, request , *args, **kwargs):
        return self.create(request, *args, **kwargs)

# Retrieve, update and delete (pk required)
class RUDGigsAPI(GenericAPIView, RetrieveModelMixin, UpdateModelMixin,  DestroyModelMixin):
    queryset = Gigs.objects.all()
    serializer_class = GigsSerializer
    permission_classes = (AllowAny,)

    def get(self, request , *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)
    
    def put(self, request , *args, **kwargs):
        return self.update(request, *args, **kwargs)

    def put(self, request , *args, **kwargs):
        return self.partial_update(request, *args, **kwargs)
    
    def delete(self, request , *args, **kwargs): 
        pk = kwargs.get('pk')
        p = Gigs.objects.get(id=pk)
        if p.images:
            p.images.delete() 
        return self.destroy(request, *args, **kwargs)



# VIEWS FOR REVIEWS MODEL
class ReviewsListAPI(GenericAPIView, ListModelMixin ):
    def get_queryset(self):
       item = self.kwargs['item']
       return Reviews.objects.filter(item=item)
    serializer_class = ReviewsSerializer
    permission_classes = (AllowAny,)

    def get(self, request , *args, **kwargs):
        return self.list(request, *args, **kwargs)

class ReviewsCreateAPI(GenericAPIView, CreateModelMixin):
    queryset = Reviews.objects.all()
    serializer_class = ReviewsSerializer
    permission_classes = (AllowAny,)

    def post(self, request , *args, **kwargs):
        return self.create(request, *args, **kwargs)

Serializers.py

from rest_framework import serializers
from .models import Gigs, Reviews

class GigsSerializer (serializers.ModelSerializer):
    class Meta:
        model = Gigs
        fields = ['id','title','category','price','details','seller','images']

class ReviewsSerializer (serializers.ModelSerializer):
    class Meta:
        model = Reviews
        fields = ['id','rating','comment','item','buyer','created_at']

I want to calculate average of the ratings of some gigs or item in reviews table and then send it to API. but I am confused where to calculate it (models.py or views.py) and then how to send it to my API.

3 Answers

Well I am gonna explain this in details, average rating could be considered as a virtual field in Gigs, so it make sense to put it in there, so lets try that:

class Gigs(models.Model):
    ...

    @property
    def average_rating(self):
        return self.reviews.aggregate(Avg('rating'))['rating_avg']

so when you gonna retrieve a single Gigs, this is good and everything, but the problem is if you need the average in the list api, this is gonna make alot of extra queries(1 for each Gig). in that case it is better to do it in bulk and in the view, so:

class GigsListAll(ListModelMixin, GenericAPIView): # you should put the mixin before the main class :D
    serializer_class = GigsSerializer
    permission_classes = (AllowAny,)

    def get_queryset(self):

        return Gigs.objects.all().annotate(_average_rating=Avg('reviews__rating') # pay attention, it was annotated as _average_rating

and now we gonna change the virtual field in the model, and check if we have it precalculated, so:

class Gigs(models.Model):
...

    @property
    def average_rating(self):
        if hasattr(self, '_average_rating'):
            return self._average_rating
        return self.reviews.aggregate(Avg('rating'))

finally to use it in your serializer:

class GigsSerializer (serializers.ModelSerializer):
average_rating = serializers.SerializerMethodField()
def get_average_rating(self, obj):
    return obj.average_rating
class Meta:
    model = Gigs
    fields = ['id','title','category','price','details','seller','images','average_rating']

p.s. It is a best practice to set the related name for foreign keys, so change your reviews model like this:

class Reviews(models.Model):
    ...
    item = models.ForeignKey(Gigs , on_delete=models.CASCADE, related_name='reviews')

First give your foreign key a name so you can reverse it:

class Reviews(models.Model):
    ...
    item =  models.ForeignKey(Gigs, on_delete=models.CASCADE, related_name='reviews')
    ...

Then you can do this in your serializer:

from rest_framework import serializers
from .models import Gigs, Reviews
from django.db.models import Avg

class GigsSerializer (serializers.ModelSerializer):
    class Meta:
        model = Gigs
        fields = ['id','title','category','price','details','seller','images','avg_rating']

    avg_rating = serializers.SerializerMethodField()

    def get_avg_rating(self, ob):
        # reverse lookup on Reviews using item field
        return ob.reviews.all().aggregate(Avg('rating'))['rating__avg']

class ReviewsSerializer (serializers.ModelSerializer):
    class Meta:
        model = Reviews
        fields = ['id','rating','comment','item','buyer','created_at']

It's redundant but I post it also here.

Objects properties have to be serialized using SerializerMethodField. To get values for these type of fields serializers look for methodsnamed get_ and raises errors if they are not defined. The get_ method must accept an object as a parameter that the serializer use to pass in the current serialized object, so you can access its properties. In this case it have to be:

class GigsSerializer(serializers.MethodSerializer):
    average_rating = serializers.SerializerMethodField()

    def get_average_rating(self, obj):
        return obj.average_rating

    class Meta:
        model = Gigs
        fields = ["""your fields here"""]
Related