Add an API endpoint for the browser mapping (#20800)
This commit is contained in:
Родитель
04c983890c
Коммит
22be8626a5
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче