From dd050597d3b1311e71d54c3c91b8b7d689d5d736 Mon Sep 17 00:00:00 2001 From: Andrew Williamson Date: Mon, 8 Feb 2021 15:12:50 +0000 Subject: [PATCH] return grouped ratings with addon detail api (#16468) --- docs/topics/api/addons.rst | 7 +++++++ docs/topics/api/overview.rst | 1 + src/olympia/addons/serializers.py | 16 +++++++++++++++- src/olympia/addons/tests/test_serializers.py | 19 +++++++++++++++++++ src/olympia/addons/tests/test_views.py | 18 ++++++++++++++++++ src/olympia/ratings/utils.py | 16 ++++++++++++++++ src/olympia/ratings/views.py | 16 ++++------------ 7 files changed, 80 insertions(+), 13 deletions(-) create mode 100644 src/olympia/ratings/utils.py diff --git a/docs/topics/api/addons.rst b/docs/topics/api/addons.rst index 876433839a..7954616815 100644 --- a/docs/topics/api/addons.rst +++ b/docs/topics/api/addons.rst @@ -136,6 +136,7 @@ This endpoint allows you to fetch a specific add-on by id, slug or guid. :query string app: Used in conjunction with ``appversion`` below to alter ``current_version`` behaviour. Need to be a valid :ref:`add-on application `. :query string appversion: Make ``current_version`` return the latest public version of the add-on compatible with the given application version, if possible, otherwise fall back on the generic implementation. Pass the full version as a string, e.g. ``46.0``. Only valid when the ``app`` parameter is also present. Currently only compatible with language packs through the add-on detail API, ignored for other types of add-ons and APIs. :query string lang: Activate translations in the specific language for that query. (See :ref:`Translated Fields `) + :query boolean show_grouped_ratings: Whether or not to show ratings aggregates in the ``ratings`` object (Use "true"/"1" as truthy values, "0"/"false" as falsy ones). :>json int id: The add-on id on AMO. :>json array authors: Array holding information about the authors for the add-on. :>json int authors[].id: The id for an author. @@ -181,6 +182,12 @@ This endpoint allows you to fetch a specific add-on by id, slug or guid. :>json string ratings_url: The URL to the user ratings list page for the add-on. :>json float ratings.average: The average user rating for the add-on. :>json float ratings.bayesian_average: The bayesian average user rating for the add-on. + :>json object ratings.grouped_counts: Object with aggregate counts for ratings. Only included when ``show_grouped_ratings`` is present in the request. + :>json int ratings.grouped_counts.1: the count of ratings with a score of 1. + :>json int ratings.grouped_counts.2: the count of ratings with a score of 2. + :>json int ratings.grouped_counts.3: the count of ratings with a score of 3. + :>json int ratings.grouped_counts.4: the count of ratings with a score of 4. + :>json int ratings.grouped_counts.5: the count of ratings with a score of 5. :>json boolean requires_payment: Does the add-on require payment, non-free services or software, or additional hardware. :>json string review_url: The URL to the reviewer review page for the add-on. :>json string slug: The add-on slug. diff --git a/docs/topics/api/overview.rst b/docs/topics/api/overview.rst index a5cf7058ef..2d0d00d0bb 100644 --- a/docs/topics/api/overview.rst +++ b/docs/topics/api/overview.rst @@ -410,6 +410,7 @@ These are `v5` specific changes - `v4` changes apply also. * 2021-01-28: made ``description_text`` in discovery endpoint a translated field in the response. (It was always localized, we just didn't return it as such). https://github.com/mozilla/addons-server/issues/8712 * 2021-02-04: dropped /shelves/sponsored endpoint https://github.com/mozilla/addons-server/issues/16390 * 2021-02-11: removed Stripe webhook endpoint https://github.com/mozilla/addons-server/issues/16391 +* 2021-02-11: added ``show_grouped_ratings`` to addon detail endpoint. https://github.com/mozilla/addons-server/issues/16459 .. _`#11380`: https://github.com/mozilla/addons-server/issues/11380/ .. _`#11379`: https://github.com/mozilla/addons-server/issues/11379/ diff --git a/src/olympia/addons/serializers.py b/src/olympia/addons/serializers.py index 92c4537679..5225d5902b 100644 --- a/src/olympia/addons/serializers.py +++ b/src/olympia/addons/serializers.py @@ -31,6 +31,7 @@ from olympia.constants.promoted import PROMOTED_GROUPS, RECOMMENDED from olympia.files.models import File from olympia.promoted.models import PromotedAddon from olympia.search.filters import AddonAppVersionQueryParam +from olympia.ratings.utils import get_grouped_ratings from olympia.users.models import UserProfile from olympia.versions.models import ( ApplicationsVersions, @@ -505,12 +506,17 @@ class AddonSerializer(serializers.ModelSerializer): return {str(size): absolutify(get_icon(size)) for size in amo.ADDON_ICON_SIZES} def get_ratings(self, obj): - return { + ratings = { 'average': obj.average_rating, 'bayesian_average': obj.bayesian_rating, 'count': obj.total_ratings, 'text_count': obj.text_ratings_count, } + if (request := self.context.get('request', None)) and ( + grouped := get_grouped_ratings(request, obj) + ): + ratings['grouped_counts'] = grouped + return ratings def get_is_source_public(self, obj): return False @@ -742,6 +748,14 @@ class ESAddonSerializer(BaseESSerializer, AddonSerializer): # to_representation() is called, so it's present on all objects. return obj._es_meta['score'] + def get_ratings(self, obj): + return { + 'average': obj.average_rating, + 'bayesian_average': obj.bayesian_rating, + 'count': obj.total_ratings, + 'text_count': obj.text_ratings_count, + } + def to_representation(self, obj): data = super(ESAddonSerializer, self).to_representation(obj) request = self.context.get('request') diff --git a/src/olympia/addons/tests/test_serializers.py b/src/olympia/addons/tests/test_serializers.py index c2701a4aa2..dd5c8aea1c 100644 --- a/src/olympia/addons/tests/test_serializers.py +++ b/src/olympia/addons/tests/test_serializers.py @@ -49,6 +49,7 @@ from olympia.constants.categories import CATEGORIES from olympia.constants.licenses import LICENSES_BY_BUILTIN from olympia.constants.promoted import RECOMMENDED from olympia.files.models import WebextPermission +from olympia.ratings.models import Rating from olympia.versions.models import ( ApplicationsVersions, AppVersion, @@ -735,6 +736,17 @@ class AddonSerializerOutputTestMixin(object): result = self.serialize() assert 'created' not in result + def test_grouped_ratings(self): + self.addon = addon_factory() + self.request = self.get_request('/', {'show_grouped_ratings': 1}) + result = self.serialize() + assert result['ratings']['grouped_counts'] == {1: 0, 2: 0, 3: 0, 4: 0, 5: 0} + Rating.objects.create(addon=self.addon, rating=2, user=user_factory()) + Rating.objects.create(addon=self.addon, rating=2, user=user_factory()) + Rating.objects.create(addon=self.addon, rating=5, user=user_factory()) + result = self.serialize() + assert result['ratings']['grouped_counts'] == {1: 0, 2: 2, 3: 0, 4: 0, 5: 1} + class TestAddonSerializerOutput(AddonSerializerOutputTestMixin, TestCase): serializer_class = AddonSerializer @@ -1000,6 +1012,13 @@ class TestESAddonSerializerOutput(AddonSerializerOutputTestMixin, ESTestCase): result = self.serialize() assert '_score' not in result + def test_grouped_ratings(self): + # as grouped ratings aren't stored in ES, we don't support this + self.addon = addon_factory() + self.request = self.get_request('/', {'show_grouped_ratings': 1}) + result = self.serialize() + assert 'grouped_counts' not in result['ratings'] + class TestVersionSerializerOutput(TestCase): def setUp(self): diff --git a/src/olympia/addons/tests/test_views.py b/src/olympia/addons/tests/test_views.py index 00457f1caf..b5c9382bf4 100644 --- a/src/olympia/addons/tests/test_views.py +++ b/src/olympia/addons/tests/test_views.py @@ -644,6 +644,24 @@ class TestAddonViewSetDetail(AddonAndVersionViewSetDetailMixin, TestCase): data = json.loads(force_str(response.content)) assert data == {'detail': 'Invalid "app" parameter.'} + def test_with_grouped_ratings(self): + assert 'grouped_counts' not in self.client.get(self.url).json()['ratings'] + + response = self.client.get(self.url, {'show_grouped_ratings': 'true'}) + assert 'grouped_counts' in response.json()['ratings'] + assert response.json()['ratings']['grouped_counts'] == { + '1': 0, + '2': 0, + '3': 0, + '4': 0, + '5': 0, + } + + response = self.client.get(self.url, {'show_grouped_ratings': '58.0'}) + assert response.status_code == 400 + data = json.loads(force_str(response.content)) + assert data == {'detail': 'show_grouped_ratings parameter should be a boolean'} + class TestVersionViewSetDetail(AddonAndVersionViewSetDetailMixin, TestCase): client_class = APITestClient diff --git a/src/olympia/ratings/utils.py b/src/olympia/ratings/utils.py new file mode 100644 index 0000000000..949ebab3d5 --- /dev/null +++ b/src/olympia/ratings/utils.py @@ -0,0 +1,16 @@ +from rest_framework import serializers +from rest_framework.exceptions import ParseError + +from .models import GroupedRating + + +def get_grouped_ratings(request, addon): + if 'show_grouped_ratings' in request.GET: + try: + show_grouped_ratings = serializers.BooleanField().to_internal_value( + request.GET['show_grouped_ratings'] + ) + except serializers.ValidationError: + raise ParseError('show_grouped_ratings parameter should be a boolean') + if show_grouped_ratings and addon: + return dict(GroupedRating.get(addon.id)) diff --git a/src/olympia/ratings/views.py b/src/olympia/ratings/views.py index 6cb34c6fb8..0d42b82e9b 100644 --- a/src/olympia/ratings/views.py +++ b/src/olympia/ratings/views.py @@ -26,9 +26,10 @@ from olympia.api.permissions import ( from olympia.api.throttling import GranularUserRateThrottle from olympia.api.utils import is_gate_active -from .models import GroupedRating, Rating, RatingFlag +from .models import Rating, RatingFlag from .permissions import CanDeleteRatingPermission from .serializers import RatingFlagSerializer, RatingSerializer, RatingSerializerReply +from .utils import get_grouped_ratings class RatingThrottle(GranularUserRateThrottle): @@ -226,17 +227,8 @@ class RatingViewSet(AddonChildMixin, ModelViewSet): def get_paginated_response(self, data): request = self.request extra_data = {} - if 'show_grouped_ratings' in request.GET: - try: - show_grouped_ratings = serializers.BooleanField().to_internal_value( - request.GET['show_grouped_ratings'] - ) - except serializers.ValidationError: - raise ParseError('show_grouped_ratings parameter should be a boolean') - if show_grouped_ratings and self.get_addon_object(): - extra_data['grouped_ratings'] = dict( - GroupedRating.get(self.addon_object.id) - ) + if grouped_rating := get_grouped_ratings(request, self.get_addon_object()): + extra_data['grouped_ratings'] = grouped_rating if 'show_permissions_for' in request.GET and is_gate_active( self.request, 'ratings-can_reply' ):