From 28187a50c3cb7db084ed0e3afed265269ff76090 Mon Sep 17 00:00:00 2001 From: Mathieu Pillard Date: Mon, 17 Jul 2017 22:21:04 +0200 Subject: [PATCH 1/3] Add language tools API This returns all valid, public dictionaries and language packs on AMO in a single endpoint. Clients should then sort through the results to display them as they see fit. --- docs/topics/api/addons.rst | 24 +++++++++ src/olympia/addons/api_urls.py | 4 +- src/olympia/addons/serializers.py | 11 +++++ src/olympia/addons/tests/test_serializers.py | 31 +++++++++++- src/olympia/addons/tests/test_views.py | 52 ++++++++++++++++++++ src/olympia/addons/views.py | 25 +++++++++- 6 files changed, 143 insertions(+), 4 deletions(-) diff --git a/docs/topics/api/addons.rst b/docs/topics/api/addons.rst index bc38cde113..089a9bb7ec 100644 --- a/docs/topics/api/addons.rst +++ b/docs/topics/api/addons.rst @@ -131,6 +131,7 @@ This endpoint allows you to fetch a specific add-on by id, slug or guid. :>json string|object|null name: The add-on name (See :ref:`translated fields `). :>json string last_updated: The date of the last time the add-on was updated by its developer(s). :>json object|null latest_unlisted_version: Object holding the latest unlisted :ref:`version ` of the add-on. This field is only present if the user has unlisted reviewer permissions, or is listed as a developer of the add-on. + :>json string locale_disambiguation: Free text field allowing clients to distinguish between multiple dictionaries in the same locale but different spellings. Only present when using the Language Tools endpoint. :>json array previews: Array holding information about the previews for the add-on. :>json int previews[].id: The id for a preview. :>json string|object|null previews[].caption: The caption describing a preview (See :ref:`translated fields `). @@ -149,6 +150,7 @@ This endpoint allows you to fetch a specific add-on by id, slug or guid. :>json string|object|null support_email: The add-on support email (See :ref:`translated fields `). :>json string|object|null support_url: The add-on support URL (See :ref:`translated fields `). :>json array tags: List containing the text of the tags set on the add-on. + :>json string target_locale: For dictionaries and language packs, the locale the add-on is meant for. Only present when using the Language Tools endpoint. :>json object theme_data: Object holding `lightweight theme (Persona) `_ data. Only present for themes (Persona). :>json string type: The :ref:`add-on type `. :>json string url: The (absolute) add-on detail URL. @@ -358,3 +360,25 @@ This endpoint allows you to fetch an add-on EULA and privacy policy. :>json string|object|null eula: The text of the EULA, if present (See :ref:`translated fields `). :>json string|object|null privacy_policy: The text of the Privacy Policy, if present (See :ref:`translated fields `). + + +-------------- +Language Tools +-------------- + +.. _addon-language-tools: + +This endpoint allows you to list all public language tools add-ons available +on AMO. + +.. http:get:: /api/v3/addons/language-tools/ + + .. note:: + Because this endpoint is intended to be used to feed a page that + displays all available language tools in a single page, it is not + paginated as normal, and instead will return all results without + obeying regular pagination parameters. + + :query string app: Mandatory. Filter by :ref:`add-on application ` availability. + :query string lang: Activate translations in the specific language for that query. (See :ref:`translated fields `) + :>json array results: An array of :ref:`add-ons `, but with only the following fields present: ``id``, ``current_version``, ``default_locale``, ``locale_disambiguation``, ``name``, ``target_locale``, ``type`` and ``url``. diff --git a/src/olympia/addons/api_urls.py b/src/olympia/addons/api_urls.py index 0f0d60e0ef..1e7401abfa 100644 --- a/src/olympia/addons/api_urls.py +++ b/src/olympia/addons/api_urls.py @@ -7,7 +7,7 @@ from olympia.activity.views import VersionReviewNotesViewSet from .views import ( AddonFeaturedView, AddonSearchView, AddonVersionViewSet, AddonViewSet, - StaticCategoryView) + LanguageToolsView, StaticCategoryView) addons = SimpleRouter() @@ -27,4 +27,6 @@ urlpatterns = [ url(r'^search/$', AddonSearchView.as_view(), name='addon-search'), url(r'^featured/$', AddonFeaturedView.as_view(), name='addon-featured'), url(r'^categories/$', StaticCategoryView.as_view(), name='category-list'), + url(r'^language-tools/$', LanguageToolsView.as_view(), + name='addon-language-tools'), ] diff --git a/src/olympia/addons/serializers.py b/src/olympia/addons/serializers.py index 8c3880d946..2d0c5ad63c 100644 --- a/src/olympia/addons/serializers.py +++ b/src/olympia/addons/serializers.py @@ -474,3 +474,14 @@ class StaticCategorySerializer(serializers.Serializer): def get_type(self, obj): return ADDON_TYPE_CHOICES_API[obj.type] + + +class LanguageToolsSerializer(AddonSerializer): + target_locale = serializers.CharField() + locale_disambiguation = serializers.CharField() + + class Meta: + model = Addon + fields = ('id', 'current_version', 'default_locale', + 'locale_disambiguation', 'name', 'target_locale', 'type', + 'url', ) diff --git a/src/olympia/addons/tests/test_serializers.py b/src/olympia/addons/tests/test_serializers.py index a05d741c68..3c3c043f25 100644 --- a/src/olympia/addons/tests/test_serializers.py +++ b/src/olympia/addons/tests/test_serializers.py @@ -15,8 +15,8 @@ from olympia.addons.models import ( Addon, AddonCategory, AddonUser, Category, Persona, Preview) from olympia.addons.serializers import ( AddonSerializer, AddonSerializerWithUnlistedData, ESAddonSerializer, - ESAddonSerializerWithUnlistedData, SimpleVersionSerializer, - VersionSerializer) + ESAddonSerializerWithUnlistedData, LanguageToolsSerializer, + SimpleVersionSerializer, VersionSerializer) from olympia.addons.utils import generate_addon_guid from olympia.constants.categories import CATEGORIES from olympia.files.models import WebextPermission @@ -675,3 +675,30 @@ class TestSimpleVersionSerializerOutput(TestCase): assert result['license']['name']['fr'] == u'Mä Licence' assert result['license']['url'] == 'http://license.example.com/' assert 'text' not in result['license'] + + +class TestLanguageToolsSerializerOutput(TestCase): + def setUp(self): + self.request = APIRequestFactory().get('/') + + def serialize(self): + serializer = LanguageToolsSerializer(context={'request': self.request}) + return serializer.to_representation(self.addon) + + def test_basic(self): + self.addon = addon_factory( + type=amo.ADDON_DICT, target_locale='fr', + locale_disambiguation=u'lolé') + result = self.serialize() + assert result['id'] == self.addon.pk + assert result['default_locale'] == self.addon.default_locale + assert result['locale_disambiguation'] == ( + self.addon.locale_disambiguation) + assert result['name'] == {'en-US': self.addon.name} + assert result['target_locale'] == self.addon.target_locale + assert result['url'] == absolutify(self.addon.get_url_path()) + + addon_testcase = AddonSerializerOutputTestMixin() + addon_testcase.addon = self.addon + addon_testcase._test_version( + self.addon.current_version, result['current_version']) diff --git a/src/olympia/addons/tests/test_views.py b/src/olympia/addons/tests/test_views.py index 7587dade3e..44f722b25a 100644 --- a/src/olympia/addons/tests/test_views.py +++ b/src/olympia/addons/tests/test_views.py @@ -2858,3 +2858,55 @@ class TestStaticCategoryView(TestCase): response = self.client.get(self.url) assert response.status_code == 200 assert response['cache-control'] == 'max-age=21600' + + +class TestLanguageToolsView(TestCase): + client_class = APITestClient + + def setUp(self): + super(TestLanguageToolsView, self).setUp() + self.url = reverse('addon-language-tools') + + def test_wrong_app(self): + response = self.client.get(self.url) + assert response.status_code == 400 + + response = self.client.get(self.url, {'app': 'foo'}) + assert response.status_code == 400 + + def test_basic(self): + dictionary = addon_factory(type=amo.ADDON_DICT, target_locale='fr') + dictionary_spelling_variant = addon_factory( + type=amo.ADDON_DICT, target_locale='fr', + locale_disambiguation='For spelling reform') + language_pack = addon_factory(type=amo.ADDON_DICT, target_locale='es') + + # These add-ons below should be ignored: they are either not public or + # of the wrong type, not supporting the app we care about, or their + # target locale is empty. + addon_factory( + type=amo.ADDON_DICT, target_locale='de', + version_kw={'application': amo.THUNDERBIRD.id}) + addon_factory( + type=amo.ADDON_DICT, target_locale='fr', + version_kw={'channel': amo.RELEASE_CHANNEL_UNLISTED}) + addon_factory( + type=amo.ADDON_LPAPP, target_locale='es', + file_kw={'status': amo.STATUS_AWAITING_REVIEW}, + status=amo.STATUS_NOMINATED) + addon_factory(type=amo.ADDON_DICT, target_locale='') + addon_factory(type=amo.ADDON_LPAPP, target_locale=None) + addon_factory(target_locale='fr') + + response = self.client.get(self.url, {'app': 'firefox'}) + assert response.status_code == 200 + data = json.loads(response.content) + assert len(data['results']) == 3 + expected = [dictionary, dictionary_spelling_variant, language_pack] + + assert ( + set(item['id'] for item in data['results']) == + set(item.pk for item in expected)) + + assert 'locale_disambiguation' in data['results'][0] + assert 'target_locale' in data['results'][0] diff --git a/src/olympia/addons/views.py b/src/olympia/addons/views.py index a739fa7271..3542a53ac0 100644 --- a/src/olympia/addons/views.py +++ b/src/olympia/addons/views.py @@ -63,7 +63,7 @@ from .models import Addon, Persona, FrozenAddon, ReplacementAddon from .serializers import ( AddonEulaPolicySerializer, AddonFeatureCompatibilitySerializer, AddonSerializer, AddonSerializerWithUnlistedData, ESAddonSerializer, - VersionSerializer, StaticCategorySerializer) + LanguageToolsSerializer, VersionSerializer, StaticCategorySerializer) from .utils import get_creatured_ids, get_featured_ids @@ -859,3 +859,26 @@ class StaticCategoryView(ListAPIView): request, response, *args, **kwargs) patch_cache_control(response, max_age=60 * 60 * 6) return response + + +class LanguageToolsView(ListAPIView): + authentication_classes = [] + pagination_class = None + permission_classes = [] + serializer_class = LanguageToolsSerializer + + def get_queryset(self): + try: + application_id = AddonAppFilterParam(self.request).get_value() + except ValueError: + raise ParseError('Invalid app parameter.') + + types = (amo.ADDON_DICT, amo.ADDON_LPAPP) + return Addon.objects.public().filter( + appsupport__app=application_id, type__in=types, + target_locale__isnull=False).exclude(target_locale='') + + def list(self, request, *args, **kwargs): + queryset = self.filter_queryset(self.get_queryset()) + serializer = self.get_serializer(queryset, many=True) + return Response({'results': serializer.data}) From da7c987d48af03085a1648d2e3167101f6f97979 Mon Sep 17 00:00:00 2001 From: Mathieu Pillard Date: Tue, 18 Jul 2017 17:55:03 +0200 Subject: [PATCH 2/3] post review documentation tweaks --- docs/topics/api/addons.rst | 14 ++++++++++++-- src/olympia/addons/views.py | 3 +++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/topics/api/addons.rst b/docs/topics/api/addons.rst index 089a9bb7ec..4069d4b154 100644 --- a/docs/topics/api/addons.rst +++ b/docs/topics/api/addons.rst @@ -377,8 +377,18 @@ on AMO. Because this endpoint is intended to be used to feed a page that displays all available language tools in a single page, it is not paginated as normal, and instead will return all results without - obeying regular pagination parameters. + obeying regular pagination parameters. The ordering is left undefined, + it's up to the clients to re-order results as needed before displaying + the add-ons to the end-users. :query string app: Mandatory. Filter by :ref:`add-on application ` availability. :query string lang: Activate translations in the specific language for that query. (See :ref:`translated fields `) - :>json array results: An array of :ref:`add-ons `, but with only the following fields present: ``id``, ``current_version``, ``default_locale``, ``locale_disambiguation``, ``name``, ``target_locale``, ``type`` and ``url``. + :>json array results: An array of language tools. + :>json int results[].id: The add-on id on AMO. + :>json object results[].current_version: Object holding the current :ref:`version ` of the add-on. For performance reasons the ``release_notes`` field is omitted and the ``license`` field omits the ``text`` property. + :>json string results[].default_locale: The add-on default locale for translations. + :>json string|object|null results[].name: The add-on name (See :ref:`translated fields `). + :>json string results[].locale_disambiguation: Free text field allowing clients to distinguish between multiple dictionaries in the same locale but different spellings. Only present when using the Language Tools endpoint. + :>json string results[].target_locale: For dictionaries and language packs, the locale the add-on is meant for. Only present when using the Language Tools endpoint. + :>json string results[].type: The :ref:`add-on type `. + :>json string results[].url: The (absolute) add-on detail URL. diff --git a/src/olympia/addons/views.py b/src/olympia/addons/views.py index 3542a53ac0..71f37d6e3b 100644 --- a/src/olympia/addons/views.py +++ b/src/olympia/addons/views.py @@ -879,6 +879,9 @@ class LanguageToolsView(ListAPIView): target_locale__isnull=False).exclude(target_locale='') def list(self, request, *args, **kwargs): + # Ignore pagination (return everything) but do wrap the data in a + # 'results' property to mimic what the default implementation of list() + # does in DRF. queryset = self.filter_queryset(self.get_queryset()) serializer = self.get_serializer(queryset, many=True) return Response({'results': serializer.data}) From 2cf22e7705c9bcaddf12ef809c4b53e5b1fa6b55 Mon Sep 17 00:00:00 2001 From: Mathieu Pillard Date: Tue, 18 Jul 2017 18:12:28 +0200 Subject: [PATCH 3/3] Remove extra docs from add-on object now that language-tools doc has them --- docs/topics/api/addons.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/topics/api/addons.rst b/docs/topics/api/addons.rst index 4069d4b154..2b52ecc258 100644 --- a/docs/topics/api/addons.rst +++ b/docs/topics/api/addons.rst @@ -131,7 +131,6 @@ This endpoint allows you to fetch a specific add-on by id, slug or guid. :>json string|object|null name: The add-on name (See :ref:`translated fields `). :>json string last_updated: The date of the last time the add-on was updated by its developer(s). :>json object|null latest_unlisted_version: Object holding the latest unlisted :ref:`version ` of the add-on. This field is only present if the user has unlisted reviewer permissions, or is listed as a developer of the add-on. - :>json string locale_disambiguation: Free text field allowing clients to distinguish between multiple dictionaries in the same locale but different spellings. Only present when using the Language Tools endpoint. :>json array previews: Array holding information about the previews for the add-on. :>json int previews[].id: The id for a preview. :>json string|object|null previews[].caption: The caption describing a preview (See :ref:`translated fields `). @@ -150,7 +149,6 @@ This endpoint allows you to fetch a specific add-on by id, slug or guid. :>json string|object|null support_email: The add-on support email (See :ref:`translated fields `). :>json string|object|null support_url: The add-on support URL (See :ref:`translated fields `). :>json array tags: List containing the text of the tags set on the add-on. - :>json string target_locale: For dictionaries and language packs, the locale the add-on is meant for. Only present when using the Language Tools endpoint. :>json object theme_data: Object holding `lightweight theme (Persona) `_ data. Only present for themes (Persona). :>json string type: The :ref:`add-on type `. :>json string url: The (absolute) add-on detail URL.