Add an API endpoint for the browser mapping (#20800)

This commit is contained in:
William Durand 2023-06-12 14:19:03 +02:00 коммит произвёл GitHub
Родитель 04c983890c
Коммит 22be8626a5
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 169 добавлений и 3 удалений

Просмотреть файл

@ -974,3 +974,23 @@ Four recommendations are fetched, but only valid, publicly available addons are
:>json string next: The URL of the next page of results.
:>json string previous: The URL of the previous page of results.
:>json array results: An array of :ref:`add-ons <addon-detail-object>`. The following fields are omitted for performance reasons: ``release_notes`` and ``license`` fields on ``current_version`` and ``current_beta_version``, as well as ``picture_url`` from ``authors``.
----------------
Browser Mappings
----------------
.. _addon-browser-mappings:
This endpoint provides browser mappings of non-Firefox and Firefox extensions. Added to support the extensions import feature in Firefox.
.. http:get:: /api/v5/addons/browser-mappings/
.. note::
This endpoint uses a larger ``page_size`` than most other API endpoints.
:query string browser: The browser identifier for this query (required). Must be one of these: ``chrome``.
:query int page_size: Maximum number of results to return for the requested page. Defaults to 100.
:>json array results: An array containing a mapping of non-Firefox and Firefox extension IDs for a given browser.
:>json string results[].extension_id: A non-Firefox extension ID.
:>json string results[].addon_guid: The corresponding Firefox add-on ``guid``.

Просмотреть файл

@ -461,6 +461,7 @@ These are `v5` specific changes - `v4` changes apply also.
* 2023-03-09: added ``host_permissions`` to the response of the version detail endpoint. https://github.com/mozilla/addons-server/issues/20418
* 2023-04-13: removed signing api from api/v5+ in favor of addon submission api. https://github.com/mozilla/addons-server/issues/20560
* 2023-06-01: renamed add-ons search endpoint sort by ratings parameter to ``sort=ratings``, ``sort=rating`` is still supported for backwards-compatibility. https://github.com/mozilla/addons-server/issues/20763
* 2023-06-06: added the /addons/browser-mappings/ endpoint. https://github.com/mozilla/addons-server/issues/20798
.. _`#11380`: https://github.com/mozilla/addons-server/issues/11380/
.. _`#11379`: https://github.com/mozilla/addons-server/issues/11379/

Просмотреть файл

@ -10,6 +10,7 @@ from olympia.tags.views import TagListView
from .views import (
AddonAuthorViewSet,
AddonAutoCompleteSearchView,
AddonBrowserMappingView,
AddonFeaturedView,
AddonPendingAuthorViewSet,
AddonPreviewViewSet,
@ -67,6 +68,11 @@ urls = [
AddonRecommendationView.as_view(),
name='addon-recommendations',
),
re_path(
r'^browser-mappings/$',
AddonBrowserMappingView.as_view(),
name='addon-browser-mappings',
),
]
addons_v3 = urls + [

Просмотреть файл

@ -65,6 +65,7 @@ from .fields import (
from .models import (
Addon,
AddonApprovalsCounter,
AddonBrowserMapping,
AddonUser,
AddonUserPendingConfirmation,
DeniedSlug,
@ -1648,3 +1649,15 @@ class ReplacementAddonSerializer(AMOModelSerializer):
coll_match.group('user_id'), coll_match.group('coll_slug')
)
return []
class AddonBrowserMappingSerializer(AMOModelSerializer):
# The source is an annotated field defined in `AddonBrowserMappingView`.
addon_guid = serializers.CharField(source='addon__guid')
class Meta:
model = AddonBrowserMapping
fields = (
'addon_guid',
'extension_id',
)

Просмотреть файл

@ -46,6 +46,7 @@ from olympia.amo.tests.test_helpers import get_image_path
from olympia.amo.urlresolvers import get_outgoing_url
from olympia.bandwagon.models import CollectionAddon
from olympia.blocklist.models import Block
from olympia.constants.browsers import CHROME
from olympia.constants.categories import CATEGORIES, CATEGORIES_BY_ID
from olympia.constants.licenses import LICENSE_GPL3
from olympia.constants.promoted import (
@ -75,6 +76,7 @@ from olympia.versions.models import (
from ..models import (
Addon,
AddonApprovalsCounter,
AddonBrowserMapping,
AddonCategory,
AddonRegionalRestrictions,
AddonReviewerFlags,
@ -7272,3 +7274,71 @@ class TestAddonPendingAuthorViewSet(TestCase):
# and not delete either
response = self.client.delete(self.detail_url)
assert response.status_code == 403, response.content
class TestBrowserMapping(TestCase):
def setUp(self):
super().setUp()
self.url = reverse_ns('addon-browser-mappings', api_version='v5')
def assert_json_results(self, response, expected_results):
json = response.json()
assert 'results' in json
assert 'count' in json
assert 'page_size' in json
assert json['count'] == expected_results
assert json['page_size'] == 100 # We use `LargePageNumberPagination`.
return json['results']
def test_invalid_params(self):
for query_string in ['', '?invalid=param', '?browser=not-a-browser']:
res = self.client.get(f'{self.url}{query_string}')
assert res.status_code == 400
assert res['cache-control'] == 's-maxage=0'
assert res.json() == {'detail': 'Invalid browser parameter'}
def test_get(self):
addon_1 = addon_factory()
extension_id_1 = 'an-extension-id'
AddonBrowserMapping.objects.create(
addon=addon_1,
extension_id=extension_id_1,
browser=CHROME,
)
addon_2 = addon_factory()
extension_id_2 = 'another-extension-id'
AddonBrowserMapping.objects.create(
addon=addon_2,
extension_id=extension_id_2,
browser=CHROME,
)
# Shouldn't show up because of the add-on status.
AddonBrowserMapping.objects.create(
addon=addon_factory(status=amo.STATUS_NOMINATED),
extension_id='some-other-extension-id-1',
browser=CHROME,
)
# Shouldn't show up because the browser is unrelated.
AddonBrowserMapping.objects.create(
addon=addon_1,
extension_id='some-other-extension-id-2',
browser=0,
)
# - 1 for counting the number of results
# - 1 for fetching the results of the first (and only) page
with self.assertNumQueries(2):
res = self.client.get(f'{self.url}?browser=chrome')
assert res.status_code == 200
assert res['cache-control'] == 'max-age=86400'
results = self.assert_json_results(res, 2)
assert results[0] == {
'extension_id': extension_id_1,
'addon_guid': addon_1.guid,
}
assert results[1] == {
'extension_id': extension_id_2,
'addon_guid': addon_2.guid,
}

Просмотреть файл

@ -1,7 +1,7 @@
from collections import OrderedDict
from django import http
from django.db.models import Max, Prefetch
from django.db.models import F, Max, Prefetch
from django.db.transaction import non_atomic_requests
from django.shortcuts import redirect
from django.utils.cache import patch_cache_control
@ -35,7 +35,7 @@ from olympia.api.authentication import (
SessionIDAuthentication,
)
from olympia.api.exceptions import UnavailableForLegalReasons
from olympia.api.pagination import ESPageNumberPagination
from olympia.api.pagination import ESPageNumberPagination, LargePageNumberPagination
from olympia.api.permissions import (
AllowAddonAuthor,
AllowAddonOwner,
@ -51,6 +51,7 @@ from olympia.api.permissions import (
)
from olympia.api.throttling import addon_submission_throttles
from olympia.api.utils import is_gate_active
from olympia.constants.browsers import BROWSERS
from olympia.constants.categories import CATEGORIES_BY_ID
from olympia.devhub.permissions import IsSubmissionAllowedFor
from olympia.search.filters import (
@ -75,9 +76,16 @@ from olympia.versions.models import Version
from .decorators import addon_view_factory
from .indexers import AddonIndexer
from .models import Addon, AddonUser, AddonUserPendingConfirmation, ReplacementAddon
from .models import (
Addon,
AddonBrowserMapping,
AddonUser,
AddonUserPendingConfirmation,
ReplacementAddon,
)
from .serializers import (
AddonAuthorSerializer,
AddonBrowserMappingSerializer,
AddonEulaPolicySerializer,
AddonPendingAuthorSerializer,
AddonSerializer,
@ -1239,3 +1247,41 @@ class AddonRecommendationView(AddonSearchView):
def paginate_queryset(self, queryset):
# We don't need pagination for the fixed number of results.
return queryset
class AddonBrowserMappingView(ListAPIView):
authentication_classes = []
serializer_class = AddonBrowserMappingSerializer
pagination_class = LargePageNumberPagination
def get(self, request, format=None):
browser = self.request.query_params.get('browser', None)
if browser not in list(BROWSERS.values()):
raise exceptions.ParseError('Invalid browser parameter')
response = super().get(request, format)
# Cache for 24 hours.
patch_cache_control(response, max_age=60 * 60 * 24)
return response
def get_queryset(self):
browser = next(
(
key
for key in BROWSERS
if BROWSERS.get(key) == self.request.query_params.get('browser')
),
None,
)
return AddonBrowserMapping.objects.filter(
browser=browser, addon__status__in=amo.APPROVED_STATUSES
).annotate(
# This is used in `AddonBrowserMappingSerializer` in order to avoid
# unnecessary queries since we only need the add-on GUID.
addon__guid=F('addon__guid')
)
@classmethod
def as_view(cls, **initkwargs):
"""The API is read-only so we can turn off atomic requests."""
return non_atomic_requests(super().as_view(**initkwargs))

Просмотреть файл

@ -59,3 +59,13 @@ class OneOrZeroPageNumberPagination(CustomPageNumberPagination):
]
)
)
class LargePageNumberPagination(CustomPageNumberPagination):
"""
A pagination implementation that allows large page sizes. You should
probably not use it.
"""
max_page_size = 100
page_size = 100