0

I am trying to make a Movie project in django. I would like to have one view for film's ratings that can take single rate_value but by GET returns mean of rating values for a film.

I have no idea where should I start. I tried some changes in view and serializer but didnt work. Here are:

views.py:

class RateListView(generics.ListCreateAPIView):
    permission_classes = [AllowAny]
    serializer_class = RateSerializer
    lookup_url_kwarg = 'rateid'

    def get_queryset(self):
        rateid = self.kwargs.get(self.lookup_url_kwarg)
        queryset = Rate.objects.filter(film = rateid)
        # .aggregate(Avg('rate_value'))

        if queryset.exists():
            return queryset
        else:
            raise Http404

    def post(self, request, *args, **kwargs):
        serializer = RateSerializer(data = request.data)

        if serializer.is_valid():
            if Film.objects.filter(pk = self.kwargs.get(self.lookup_url_kwarg)).exists():
            serializer.save(film_id = self.kwargs.get(self.lookup_url_kwarg))
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        else:
            raise Http404

    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

serializer.py

class RateSerializer(serializers.ModelSerializer):
    class Meta:
        model = Rate
        fields = ('rate_value',)

and model.py

class Rate(models.Model):
    film = models.ForeignKey(Film, on_delete = models.CASCADE)
    rate_value = models.IntegerField(validators = [
                        MinValueValidator(1),
                        MaxValueValidator(10)
                        ]
    )

    def __str__(self):
        return str(self.film.title) + str(self.rate_value)

Right now it returns single values correctly.

  • 1
    can you fix your indents and explain what your problem is in a less confusing way please? (e.g. : What is your desired result? What is the result you are getting now? What are the errors you got? ) – engin_ipek Dec 23 '19 at 16:25
  • Thanks for giving me a valuable lesson. I need to be more precise. When I send GET on /movies/4/rating/ (4 is film ID) I am getting a list of ratings in reponse. It works fine but is not correct according to requirements. I should have one parameter in reponse, containing an average value of all raitings. POST is working corretly. What I am trying to figure out is how to redesign code above to get an average raiting in reponse, not the whole list of raitings. – Marek Rybka Dec 27 '19 at 16:15

2 Answers2

0
class Film(models.Model:
    ...

    @property
    def mean_rating(self):
        ratings = self.ratings.all().values_list('rate_value', flat=True)
        if ratings:
            return sum(ratings)/len(ratings)
        # return a separate default value here if no ratings

class Rate(models.Model):
    film = models.ForeignKey(Film, on_delete = models.CASCADE, related_name='ratings')
    rate_value = models.IntegerField(validators = [
                        MinValueValidator(1),
                        MaxValueValidator(10)
                        ]
    )

...

class FilmSerializer(serializers.ModelSerializer):
    class Meta:
        model = Film
        fields = ('id','name','mean_rating',)

Without getting into the architecture of your app, here's a pattern that can be used. The Film object has a computed property that calls ratings, the related_name supplied to a film rating, to get all related ratings and returns the mean.

Properties of models can be used as serializer fields as long as then provide serializable values.

For more on related_name for models in Django, see here - What is `related_name` used for in Django?

Ian Price
  • 7,416
  • 2
  • 23
  • 34
  • Yeah. I used this concept in very similar way and it works fine. Only bug I found was "sum(ratings)". It is hard to use sum for tuples that way so I used 'sum(sum(x) for x in ratings)' – Marek Rybka Dec 27 '19 at 20:44
  • Ahh, my bad on the oversight there! You could alternately flatten the list in place with `ratings = self.ratings.all().values_list('rate_value', flat=True)` – Ian Price Dec 27 '19 at 22:02
0

Try something like this:

serializers.py

class RateListSerializer(serializers.Serializer):
    avg_rate_value = serializers.FloatField()    


class RateSerializer(serializers.ModelSerializer):
    class Meta:
        model = Rate
        fields = ("rate_value", "film")

views.py

class RateListView(generics.ListCreateAPIView):
    permission_classes = [AllowAny]

    def get_queryset(self):
        if self.action == "list":
            queryset = Film.objects.all()
            film_id = self.request.query_params.get("film_id", None)
            if film_id is not None:
                queryset = queryset.filter(id=film_id)
            return queryset.annotate(avg_rate_value=Avg("rate__rate_value"))
        return Rate.objects.all()

    def get_seializer_class(self):
        if self.action == "list":
            return RateListSerializer
        return RateSerializer
ar7n
  • 171
  • 1
  • 6
  • I am getting an error: 'RateListView' object has no attribute 'action' – Marek Rybka Dec 27 '19 at 14:53
  • U misunderstood my idea a little. I dont have a rate parameter for every film. Ratings are different table releated by ForeignKey to films. I created serializer like your and trying to return in queryset "queryset = Rate.objects.filter(film = rateid).aggregate(avg_rate_value=Avg('rate_value'))". I have an error: AttributeError at /movies/4/rating/ Got AttributeError when attempting to get a value for field `avg_rate_value` on serializer `RateMeanSerializer`. The serializer field might be named incorrectly and not match any attribute or key on the `str` instance. – Marek Rybka Dec 27 '19 at 16:09
  • What is funny, annotate works. It return list of raitings, but works. Problem is with aggregate. – Marek Rybka Dec 27 '19 at 16:26