From 5ba2a0224531361a84ee3faa59e73f23ff49db6d Mon Sep 17 00:00:00 2001 From: Mathieu Pillard Date: Thu, 21 Jul 2016 15:23:16 +0200 Subject: [PATCH] Add ratings information to the add-on search/detail API --- docs/conf.py | 1 + docs/topics/api/addons.rst | 3 +++ src/olympia/addons/indexers.py | 13 ++++++++++++- src/olympia/addons/serializers.py | 16 +++++++++++++--- src/olympia/addons/tests/test_indexers.py | 6 +++++- src/olympia/addons/tests/test_serializers.py | 6 ++++++ 6 files changed, 40 insertions(+), 5 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 5dbda056dc..5350a4f814 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -233,6 +233,7 @@ nitpick_ignore = [ ('http:obj', 'array'), ('http:obj', 'boolean'), ('http:obj', 'int'), + ('http:obj', 'float'), ('http:obj', 'object'), ('http:obj', 'string'), ('http:obj', 'string|object|null'), diff --git a/docs/topics/api/addons.rst b/docs/topics/api/addons.rst index 4bab478246..71bec84a7b 100644 --- a/docs/topics/api/addons.rst +++ b/docs/topics/api/addons.rst @@ -102,6 +102,9 @@ This endpoint allows you to fetch a specific add-on by id, slug or guid. :>json string previews[].image_url: The URL (including a cachebusting query string) to the preview image. :>json string previews[].thumbnail_url: The URL (including a cachebusting query string) to the preview image thumbnail. :>json boolean public_stats: Boolean indicating whether the add-on stats are public or not. + :>json object ratings: Object holding ratings summary information about the add-on. + :>json int ratings.count: The number of user ratings for the add-on. + :>json float ratings.average: The average user rating for the add-on. :>json string review_url: The URL to the review page for this add-on. :>json string slug: The add-on slug. :>json string status: The :ref:`add-on status `. diff --git a/src/olympia/addons/indexers.py b/src/olympia/addons/indexers.py index 67ff04c589..f6b9be71ec 100644 --- a/src/olympia/addons/indexers.py +++ b/src/olympia/addons/indexers.py @@ -113,7 +113,14 @@ class AddonIndexer(BaseSearchIndexer): 'modified': {'type': 'date', 'index': 'no'}, }, }, - 'public_stats': {'type': 'boolean'}, + 'public_stats': {'type': 'boolean', 'index': 'no'}, + 'ratings': { + 'type': 'object', + 'properties': { + 'count': {'type': 'short', 'index': 'no'}, + 'average': {'type': 'float', 'index': 'no'} + } + }, 'slug': {'type': 'string'}, 'status': {'type': 'byte'}, 'summary': {'type': 'string', 'analyzer': 'snowball'}, @@ -230,6 +237,10 @@ class AddonIndexer(BaseSearchIndexer): # transformer that sets it. data['previews'] = [{'id': preview.id, 'modified': preview.modified} for preview in obj.all_previews] + data['ratings'] = { + 'average': obj.average_rating, + 'count': obj.total_reviews, + } data['tags'] = getattr(obj, 'tag_list', []) # Handle localized fields. diff --git a/src/olympia/addons/serializers.py b/src/olympia/addons/serializers.py index 872f88eb5f..411d40d689 100644 --- a/src/olympia/addons/serializers.py +++ b/src/olympia/addons/serializers.py @@ -119,6 +119,7 @@ class AddonSerializer(serializers.ModelSerializer): icon_url = serializers.SerializerMethodField() name = TranslationSerializerField() previews = PreviewSerializer(many=True, source='all_previews') + ratings = serializers.SerializerMethodField() review_url = serializers.SerializerMethodField() status = ReverseChoiceField(choices=amo.STATUS_CHOICES_API.items()) summary = TranslationSerializerField() @@ -134,9 +135,9 @@ class AddonSerializer(serializers.ModelSerializer): fields = ('id', 'authors', 'current_version', 'default_locale', 'description', 'edit_url', 'guid', 'homepage', 'icon_url', 'is_listed', 'name', 'last_updated', 'previews', - 'public_stats', 'review_url', 'slug', 'status', 'summary', - 'support_email', 'support_url', 'tags', 'theme_data', 'type', - 'url') + 'public_stats', 'ratings', 'review_url', 'slug', 'status', + 'summary', 'support_email', 'support_url', 'tags', + 'theme_data', 'type', 'url') def to_representation(self, obj): data = super(AddonSerializer, self).to_representation(obj) @@ -165,6 +166,12 @@ class AddonSerializer(serializers.ModelSerializer): return absolutify(obj.get_default_icon_url(64)) return absolutify(obj.get_icon_url(64)) + def get_ratings(self, obj): + return { + 'average': obj.average_rating, + 'count': obj.total_reviews, + } + def get_theme_data(self, obj): theme_data = None @@ -276,6 +283,9 @@ class ESAddonSerializer(BaseESSerializer, AddonSerializer): # for us when its to_representation() method is called. obj.all_previews = data.get('previews', []) + obj.average_rating = data.get('ratings', {}).get('average') + obj.total_reviews = data.get('ratings', {}).get('count') + if data['type'] == amo.ADDON_PERSONA: persona_data = data.get('persona') if persona_data: diff --git a/src/olympia/addons/tests/test_indexers.py b/src/olympia/addons/tests/test_indexers.py index 33c4502af3..e2c61d1021 100644 --- a/src/olympia/addons/tests/test_indexers.py +++ b/src/olympia/addons/tests/test_indexers.py @@ -47,7 +47,7 @@ class TestAddonIndexer(TestCase): 'app', 'appversion', 'authors', 'boost', 'category', 'current_version', 'description', 'has_theme_rereview', 'has_version', 'listed_authors', 'name', 'name_sort', 'platforms', - 'previews', 'public_stats', 'summary', 'tags', + 'previews', 'public_stats', 'ratings', 'summary', 'tags', ] # Fields that need to be present in the mapping, but might be skipped @@ -149,6 +149,10 @@ class TestAddonIndexer(TestCase): assert extracted['listed_authors'] == [ {'name': u'55021 التطب', 'id': 55021, 'username': '55021'}] assert extracted['platforms'] == [PLATFORM_ALL.id] + assert extracted['ratings'] == { + 'average': self.addon.average_rating, + 'count': self.addon.total_reviews, + } assert extracted['tags'] == [] def test_extract_version_and_files(self): diff --git a/src/olympia/addons/tests/test_serializers.py b/src/olympia/addons/tests/test_serializers.py index 9addc6d33f..b528c0bf45 100644 --- a/src/olympia/addons/tests/test_serializers.py +++ b/src/olympia/addons/tests/test_serializers.py @@ -22,6 +22,7 @@ class AddonSerializerOutputTestMixin(object): def test_basic(self): self.addon = addon_factory( + average_rating=4.21, description=u'My Addôn description', file_kw={ 'hash': 'fakehash', @@ -38,6 +39,7 @@ class AddonSerializerOutputTestMixin(object): support_email=u'support@example.org', support_url=u'https://support.example.org/support/my-addon/', tags=['some_tag', 'some_other_tag'], + total_reviews=666, ) AddonUser.objects.create(user=user_factory(username='hidden_author'), addon=self.addon, listed=False) @@ -127,6 +129,10 @@ class AddonSerializerOutputTestMixin(object): assert result_preview['thumbnail_url'] == absolutify( second_preview.thumbnail_url) + assert result['ratings'] == { + 'average': self.addon.average_rating, + 'count': self.addon.total_reviews, + } assert result['public_stats'] == self.addon.public_stats assert result['review_url'] == absolutify( reverse('editors.review', args=[self.addon.pk]))