rm featured in the v4 api/shim in v3 api (#12949)
This commit is contained in:
Родитель
e578e9c6df
Коммит
70a2c8e98a
|
@ -10,26 +10,6 @@ Add-ons
|
|||
The only authentication method available at
|
||||
the moment is :ref:`the internal one<api-auth-internal>`.
|
||||
|
||||
--------
|
||||
Featured
|
||||
--------
|
||||
|
||||
.. _addon-featured:
|
||||
|
||||
This endpoint allows you to list featured add-ons matching some parameters.
|
||||
Results are sorted randomly and therefore, the standard pagination parameters
|
||||
are not accepted. The query parameter ``page_size`` is allowed but only serves
|
||||
to customize the number of results returned, clients can not request a specific
|
||||
page.
|
||||
|
||||
.. http:get:: /api/v4/addons/featured/
|
||||
|
||||
:query string app: **Required**. Filter by :ref:`add-on application <addon-detail-application>` availability.
|
||||
:query string category: Filter by :ref:`category slug <category-list>`. ``app`` and ``type`` parameters need to be set, otherwise this parameter is ignored.
|
||||
:query string lang: Request add-ons featured for this specific language to be returned alongside add-ons featured globally. Also activate translations for that query. (See :ref:`translated fields <api-overview-translations>`)
|
||||
:query string type: Filter by :ref:`add-on type <addon-detail-type>`.
|
||||
:query int page_size: Maximum number of results to return. Defaults to 25.
|
||||
:>json array results: An array of :ref:`add-ons <addon-detail-object>`.
|
||||
|
||||
------
|
||||
Search
|
||||
|
@ -48,9 +28,6 @@ This endpoint allows you to search through public add-ons.
|
|||
:query string category: Filter by :ref:`category slug <category-list>`. ``app`` and ``type`` parameters need to be set, otherwise this parameter is ignored.
|
||||
:query string color: (Experimental) Filter by color in RGB hex format, trying to find themes that approximately match the specified color. Only works for static themes.
|
||||
:query string exclude_addons: Exclude add-ons by ``slug`` or ``id``. Multiple add-ons can be specified, separated by comma(s).
|
||||
:query boolean featured: Filter to only featured add-ons. Only ``featured=true`` is supported.
|
||||
If ``app`` is provided as a parameter then only featured collections targeted to that application are taken into account.
|
||||
If ``lang`` is provided then only featured collections targeted to that language, (and collections for all languages), are taken into account. Both ``app`` and ``lang`` can be provided to filter to addons that are featured in collections that application and for that language, (and for all languages).
|
||||
:query string guid: Filter by exact add-on guid. Multiple guids can be specified, separated by comma(s), in which case any add-ons matching any of the guids will be returned. As guids are unique there should be at most one add-on result per guid specified. For usage with Firefox, instead of separating multiple guids by comma(s), a single guid can be passed in base64url format, prefixed by the ``rta:`` string.
|
||||
:query string lang: Activate translations in the specific language for that query. (See :ref:`translated fields <api-overview-translations>`)
|
||||
:query int page: 1-based page number. Defaults to 1.
|
||||
|
@ -76,8 +53,7 @@ This endpoint allows you to search through public add-ons.
|
|||
downloads Number of weekly downloads, descending.
|
||||
hotness Hotness (average number of users progression), descending.
|
||||
random Random ordering. Only available when no search query is
|
||||
passed and when filtering to only return featured or
|
||||
recommended add-ons.
|
||||
passed and when filtering to only return recommended add-ons.
|
||||
rating Bayesian rating, descending.
|
||||
recommended Recommended add-ons above non-recommend add-ons. Only
|
||||
available combined with another sort - ignored on its own.
|
||||
|
@ -188,7 +164,6 @@ This endpoint allows you to fetch a specific add-on by id, slug or guid.
|
|||
:>json object icons: An object holding the URLs to an add-ons icon including a cachebusting query string as values and their size as properties. Currently exposes 32, 64, 128 pixels wide icons.
|
||||
:>json boolean is_disabled: Whether the add-on is disabled or not.
|
||||
:>json boolean is_experimental: Whether the add-on has been marked by the developer as experimental or not.
|
||||
:>json boolean is_featured: The add-on appears in a featured collection.
|
||||
:>json boolean is_recommended: The add-on is recommended by Mozilla.
|
||||
:>json string|object|null name: The add-on name (See :ref:`translated fields <api-overview-translations>`).
|
||||
:>json string last_updated: The date of the last time the add-on was updated by its developer(s).
|
||||
|
|
|
@ -365,6 +365,7 @@ v4 API changelog
|
|||
* 2019-09-19: added /site/ endpoint to expose read-only mode and any site notice. Also added the same response to the /accounts/account/ non-public response as a convenience for logged in users. https://github.com/mozilla/addons-server/issues/11493)
|
||||
* 2019-10-17: moved /authenticate endpoint from api/v4/accounts/authenticate to version-less api/auth/authenticate-callback https://github.com/mozilla/addons-server/issues/10487
|
||||
* 2019-11-14: removed ``is_source_public`` property from addons API https://github.com/mozilla/addons-server/issues/12514
|
||||
* 2019-12-05: removed /addons/featured endpoint from v4+ and featured support from other addon api endpoints. https://github.com/mozilla/addons-server/issues/12937
|
||||
|
||||
.. _`#11380`: https://github.com/mozilla/addons-server/issues/11380/
|
||||
.. _`#11379`: https://github.com/mozilla/addons-server/issues/11379/
|
||||
|
|
|
@ -28,7 +28,6 @@ urls = [
|
|||
url(r'^autocomplete/$', AddonAutoCompleteSearchView.as_view(),
|
||||
name='addon-autocomplete'),
|
||||
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'),
|
||||
|
@ -41,6 +40,8 @@ urls = [
|
|||
|
||||
addons_v3 = urls + [
|
||||
url(r'^compat-override/$', CompatOverrideView.as_view(),
|
||||
name='addon-compat-override')]
|
||||
name='addon-compat-override'),
|
||||
url(r'^featured/$', AddonFeaturedView.as_view(), name='addon-featured'),
|
||||
]
|
||||
|
||||
addons_v4 = urls
|
||||
|
|
|
@ -123,18 +123,6 @@ class AddonIndexer(BaseSearchIndexer):
|
|||
'current_version': version_mapping,
|
||||
'default_locale': {'type': 'keyword', 'index': False},
|
||||
'description': {'type': 'text', 'analyzer': 'snowball'},
|
||||
'featured_for': {
|
||||
'type': 'nested',
|
||||
'properties': {
|
||||
'application': {'type': 'byte'},
|
||||
'locales': {
|
||||
'type': 'keyword',
|
||||
# A null locale means not targeted to a locale,
|
||||
# so shown to all locales.
|
||||
'null_value': 'ALL',
|
||||
},
|
||||
},
|
||||
},
|
||||
'guid': {'type': 'keyword'},
|
||||
'has_eula': {'type': 'boolean', 'index': False},
|
||||
'has_privacy_policy': {'type': 'boolean', 'index': False},
|
||||
|
@ -143,7 +131,6 @@ class AddonIndexer(BaseSearchIndexer):
|
|||
'icon_type': {'type': 'keyword', 'index': False},
|
||||
'is_disabled': {'type': 'boolean'},
|
||||
'is_experimental': {'type': 'boolean'},
|
||||
'is_featured': {'type': 'boolean'},
|
||||
'is_recommended': {'type': 'boolean'},
|
||||
'last_updated': {'type': 'date'},
|
||||
'listed_authors': {
|
||||
|
@ -340,12 +327,6 @@ class AddonIndexer(BaseSearchIndexer):
|
|||
for a in obj.listed_authors
|
||||
]
|
||||
|
||||
data['is_featured'] = obj.is_featured(None, None)
|
||||
data['featured_for'] = [
|
||||
{'application': [app], 'locales': list(sorted(
|
||||
locales, key=lambda x: x or ''))}
|
||||
for app, locales in obj.get_featured_by_app().items()]
|
||||
|
||||
data['has_eula'] = bool(obj.eula)
|
||||
data['has_privacy_policy'] = bool(obj.privacy_policy)
|
||||
|
||||
|
|
|
@ -371,6 +371,8 @@ class AddonSerializer(serializers.ModelSerializer):
|
|||
data.pop('created', None)
|
||||
if request and not is_gate_active(request, 'is-source-public-shim'):
|
||||
data.pop('is_source_public', None)
|
||||
if request and not is_gate_active(request, 'is-featured-addon-shim'):
|
||||
data.pop('is_featured', None)
|
||||
return data
|
||||
|
||||
def outgoingify(self, data):
|
||||
|
@ -393,12 +395,9 @@ class AddonSerializer(serializers.ModelSerializer):
|
|||
return bool(getattr(obj, 'has_eula', obj.eula))
|
||||
|
||||
def get_is_featured(self, obj):
|
||||
# obj._is_featured is set from ES, so will only be present for list
|
||||
# requests.
|
||||
if not hasattr(obj, '_is_featured'):
|
||||
# Any featuring will do.
|
||||
obj._is_featured = obj.is_featured(app=None, lang=None)
|
||||
return obj._is_featured
|
||||
# featured is gone, but we need to keep the API backwards compatible so
|
||||
# fake it with recommended status instead.
|
||||
return obj.is_recommended
|
||||
|
||||
def get_has_privacy_policy(self, obj):
|
||||
return bool(getattr(obj, 'has_privacy_policy', obj.privacy_policy))
|
||||
|
@ -622,8 +621,6 @@ class ESAddonSerializer(BaseESSerializer, AddonSerializer):
|
|||
obj.total_ratings = ratings.get('count')
|
||||
obj.text_ratings_count = ratings.get('text_count')
|
||||
|
||||
obj._is_featured = data.get('is_featured', False)
|
||||
|
||||
return obj
|
||||
|
||||
def get__score(self, obj):
|
||||
|
|
|
@ -6,9 +6,7 @@ from olympia.addons.indexers import AddonIndexer
|
|||
from olympia.addons.models import (
|
||||
Addon, Preview, attach_tags, attach_translations)
|
||||
from olympia.amo.models import SearchMixin
|
||||
from olympia.amo.tests import (
|
||||
ESTestCase, TestCase, collection_factory, file_factory)
|
||||
from olympia.bandwagon.models import FeaturedCollection
|
||||
from olympia.amo.tests import ESTestCase, TestCase, file_factory
|
||||
from olympia.constants.applications import FIREFOX
|
||||
from olympia.constants.platforms import PLATFORM_ALL, PLATFORM_MAC
|
||||
from olympia.constants.search import SEARCH_ANALYZER_MAP
|
||||
|
@ -51,10 +49,9 @@ class TestAddonIndexer(TestCase):
|
|||
# to store in ES differs from the one in the db.
|
||||
complex_fields = [
|
||||
'app', 'boost', 'category', 'colors', 'current_version',
|
||||
'description', 'featured_for', 'has_eula', 'has_privacy_policy',
|
||||
'is_featured', 'listed_authors', 'name',
|
||||
'platforms', 'previews', 'public_stats', 'ratings', 'summary',
|
||||
'tags',
|
||||
'description', 'has_eula', 'has_privacy_policy', 'listed_authors',
|
||||
'name', 'platforms', 'previews', 'public_stats', 'ratings',
|
||||
'summary', 'tags',
|
||||
]
|
||||
|
||||
# Fields that need to be present in the mapping, but might be skipped
|
||||
|
@ -185,54 +182,8 @@ class TestAddonIndexer(TestCase):
|
|||
assert extracted['tags'] == []
|
||||
assert extracted['has_eula'] is True
|
||||
assert extracted['has_privacy_policy'] is True
|
||||
assert extracted['is_featured'] is False
|
||||
assert extracted['colors'] is None
|
||||
|
||||
def test_extract_is_featured(self):
|
||||
collection = collection_factory()
|
||||
FeaturedCollection.objects.create(collection=collection,
|
||||
application=collection.application)
|
||||
collection.add_addon(self.addon)
|
||||
assert self.addon.is_featured()
|
||||
extracted = self._extract()
|
||||
assert extracted['is_featured'] is True
|
||||
|
||||
def test_extract_featured_for(self):
|
||||
collection = collection_factory()
|
||||
featured_collection = FeaturedCollection.objects.create(
|
||||
collection=collection, application=amo.FIREFOX.id)
|
||||
collection.add_addon(self.addon)
|
||||
extracted = self._extract()
|
||||
assert extracted['featured_for'] == [
|
||||
{'application': [amo.FIREFOX.id], 'locales': [None]}]
|
||||
|
||||
# Even if the locale for the FeaturedCollection is an empty string
|
||||
# instead of None, we extract it as None so that it keeps its special
|
||||
# meaning.
|
||||
featured_collection.update(locale='')
|
||||
extracted = self._extract()
|
||||
assert extracted['featured_for'] == [
|
||||
{'application': [amo.FIREFOX.id], 'locales': [None]}]
|
||||
|
||||
collection = collection_factory()
|
||||
FeaturedCollection.objects.create(collection=collection,
|
||||
application=amo.FIREFOX.id,
|
||||
locale='fr')
|
||||
collection.add_addon(self.addon)
|
||||
extracted = self._extract()
|
||||
assert extracted['featured_for'] == [
|
||||
{'application': [amo.FIREFOX.id], 'locales': [None, 'fr']}]
|
||||
|
||||
collection = collection_factory()
|
||||
FeaturedCollection.objects.create(collection=collection,
|
||||
application=amo.ANDROID.id,
|
||||
locale='de-DE')
|
||||
collection.add_addon(self.addon)
|
||||
extracted = self._extract()
|
||||
assert extracted['featured_for'] == [
|
||||
{'application': [amo.FIREFOX.id], 'locales': [None, 'fr']},
|
||||
{'application': [amo.ANDROID.id], 'locales': ['de-DE']}]
|
||||
|
||||
def test_extract_eula_privacy_policy(self):
|
||||
# Remove eula.
|
||||
self.addon.eula_id = None
|
||||
|
|
|
@ -21,7 +21,6 @@ from olympia.amo.tests import (
|
|||
ESTestCase, TestCase, addon_factory, collection_factory, file_factory,
|
||||
user_factory, version_factory)
|
||||
from olympia.amo.urlresolvers import get_outgoing_url, reverse
|
||||
from olympia.bandwagon.models import FeaturedCollection
|
||||
from olympia.constants.categories import CATEGORIES
|
||||
from olympia.constants.licenses import LICENSES_BY_BUILTIN
|
||||
from olympia.discovery.models import DiscoveryItem
|
||||
|
@ -227,7 +226,6 @@ class AddonSerializerOutputTestMixin(object):
|
|||
}
|
||||
assert result['is_disabled'] == self.addon.is_disabled
|
||||
assert result['is_experimental'] == self.addon.is_experimental is False
|
||||
assert result['is_featured'] == self.addon.is_featured() is False
|
||||
assert result['is_recommended'] == self.addon.is_recommended is False
|
||||
assert result['last_updated'] == (
|
||||
self.addon.last_updated.replace(microsecond=0).isoformat() + 'Z')
|
||||
|
@ -438,14 +436,20 @@ class AddonSerializerOutputTestMixin(object):
|
|||
|
||||
def test_is_featured(self):
|
||||
self.addon = addon_factory()
|
||||
collection = collection_factory()
|
||||
FeaturedCollection.objects.create(collection=collection,
|
||||
application=collection.application)
|
||||
collection.add_addon(self.addon)
|
||||
assert self.addon.is_featured()
|
||||
|
||||
# As we've dropped featuring, we're faking it with recommended status
|
||||
DiscoveryItem.objects.create(addon=self.addon, recommendable=True)
|
||||
result = self.serialize()
|
||||
assert result['is_featured'] is True
|
||||
|
||||
assert 'is_featured' not in result
|
||||
|
||||
# It's only present in v3
|
||||
gates = {None: ('is-featured-addon-shim',)}
|
||||
with override_settings(DRF_API_GATES=gates):
|
||||
result = self.serialize()
|
||||
assert result['is_featured'] is False
|
||||
self.addon.current_version.update(recommendation_approved=True)
|
||||
result = self.serialize()
|
||||
assert result['is_featured'] is True
|
||||
|
||||
def test_is_recommended(self):
|
||||
self.addon = addon_factory()
|
||||
|
|
|
@ -25,7 +25,7 @@ from olympia.amo.tests import (
|
|||
APITestClient, ESTestCase, TestCase, addon_factory, collection_factory,
|
||||
reverse_ns, user_factory, version_factory)
|
||||
from olympia.amo.urlresolvers import get_outgoing_url, reverse
|
||||
from olympia.bandwagon.models import FeaturedCollection, CollectionAddon
|
||||
from olympia.bandwagon.models import CollectionAddon
|
||||
from olympia.constants.categories import CATEGORIES, CATEGORIES_BY_ID
|
||||
from olympia.discovery.models import DiscoveryItem
|
||||
from olympia.users.models import UserProfile
|
||||
|
@ -378,8 +378,8 @@ class TestAddonViewSetDetail(AddonAndVersionViewSetDetailMixin, TestCase):
|
|||
'addon-detail', api_version='v5', kwargs={'pk': param})
|
||||
|
||||
def test_queries(self):
|
||||
with self.assertNumQueries(16):
|
||||
# 16 queries
|
||||
with self.assertNumQueries(15):
|
||||
# 15 queries
|
||||
# - 2 savepoints because of tests
|
||||
# - 1 for the add-on
|
||||
# - 1 for its translations
|
||||
|
@ -393,7 +393,6 @@ class TestAddonViewSetDetail(AddonAndVersionViewSetDetailMixin, TestCase):
|
|||
# - 1 for license
|
||||
# - 1 for translations of the license
|
||||
# - 1 for discovery item (is_recommended)
|
||||
# - 1 for featured collection presence (is_featured)
|
||||
# - 1 for tags
|
||||
self._test_url(lang='en-US')
|
||||
|
||||
|
@ -1216,12 +1215,12 @@ class TestAddonSearchView(ESTestCase):
|
|||
result_ids = (data['results'][0]['id'], data['results'][1]['id'])
|
||||
assert sorted(result_ids) == [addon.pk, theme.pk]
|
||||
|
||||
@patch('olympia.addons.models.get_featured_ids')
|
||||
def test_filter_by_featured_no_app_no_lang(self, get_featured_ids_mock):
|
||||
def test_filter_by_featured_no_app_no_lang(self):
|
||||
addon = addon_factory(slug='my-addon', name=u'Featured Addôn')
|
||||
addon_factory(slug='other-addon', name=u'Other Addôn')
|
||||
get_featured_ids_mock.return_value = [addon.pk]
|
||||
assert addon.is_featured()
|
||||
DiscoveryItem.objects.create(addon=addon, recommendable=True)
|
||||
addon.current_version.update(recommendation_approved=True)
|
||||
assert addon.is_recommended
|
||||
self.reindex(Addon)
|
||||
|
||||
data = self.perform_search(self.url, {'featured': 'true'})
|
||||
|
@ -1229,74 +1228,6 @@ class TestAddonSearchView(ESTestCase):
|
|||
assert len(data['results']) == 1
|
||||
assert data['results'][0]['id'] == addon.pk
|
||||
|
||||
def test_filter_by_featured_app_and_langs(self):
|
||||
fx_addon = addon_factory(slug='my-addon', name=u'Featured Addôn')
|
||||
collection = collection_factory()
|
||||
FeaturedCollection.objects.create(
|
||||
collection=collection, application=amo.FIREFOX.id)
|
||||
collection.add_addon(fx_addon)
|
||||
|
||||
fx_fr_addon = addon_factory(slug='my-addon', name=u'Lé Featured Addôn')
|
||||
collection = collection_factory()
|
||||
FeaturedCollection.objects.create(
|
||||
collection=collection, application=amo.FIREFOX.id, locale='fr')
|
||||
collection.add_addon(fx_fr_addon)
|
||||
|
||||
fn_addon = addon_factory(slug='my-addon', name=u'Featured Addôn 2 go')
|
||||
collection = collection_factory()
|
||||
FeaturedCollection.objects.create(
|
||||
collection=collection, application=amo.ANDROID.id)
|
||||
collection.add_addon(fn_addon)
|
||||
|
||||
fn_fr_addon = addon_factory(slug='my-addon', name=u'Lé Featured Mobil')
|
||||
collection = collection_factory()
|
||||
FeaturedCollection.objects.create(
|
||||
collection=collection, application=amo.ANDROID.id, locale='fr')
|
||||
collection.add_addon(fn_fr_addon)
|
||||
|
||||
addon_factory(slug='other-addon', name=u'Other Addôn')
|
||||
self.reindex(Addon)
|
||||
|
||||
# Searching for just Firefox should return the two Firefox collections.
|
||||
# The filter should be `Q('term', **{'featured_for.application': app})`
|
||||
data = self.perform_search(self.url, {'featured': 'true',
|
||||
'app': 'firefox'})
|
||||
assert data['count'] == 2 == len(data['results'])
|
||||
ids = {data['results'][0]['id'], data['results'][1]['id']}
|
||||
self.assertSetEqual(ids, {fx_addon.pk, fx_fr_addon.pk})
|
||||
|
||||
# If we specify lang 'fr' too it should be the same collections.
|
||||
# In addition to the app query above, this will be executed too:
|
||||
# `Q('terms', **{'featured_for.locales': [locale, 'ALL']}))`
|
||||
data = self.perform_search(
|
||||
self.url, {'featured': 'true', 'app': 'firefox', 'lang': 'fr'})
|
||||
assert data['count'] == 2 == len(data['results'])
|
||||
ids = {data['results'][0]['id'], data['results'][1]['id']}
|
||||
self.assertSetEqual(ids, {fx_addon.pk, fx_fr_addon.pk})
|
||||
|
||||
# But 'en-US' will exclude the 'fr' collection.
|
||||
data = self.perform_search(
|
||||
self.url, {'featured': 'true', 'app': 'firefox',
|
||||
'lang': 'en-US'})
|
||||
assert data['count'] == 1 == len(data['results'])
|
||||
assert data['results'][0]['id'] == fx_addon.pk
|
||||
|
||||
# If we only search for lang, application is ignored.
|
||||
# Just `Q('terms', **{'featured_for.locales': [locale, 'ALL']}))` now.
|
||||
data = self.perform_search(
|
||||
self.url, {'featured': 'true', 'lang': 'en-US'})
|
||||
assert data['count'] == 2 == len(data['results'])
|
||||
ids = {data['results'][0]['id'], data['results'][1]['id']}
|
||||
self.assertSetEqual(ids, {fx_addon.pk, fn_addon.pk})
|
||||
|
||||
data = self.perform_search(
|
||||
self.url, {'featured': 'true', 'lang': 'fr'})
|
||||
assert data['count'] == 4 == len(data['results'])
|
||||
ids = {data['results'][0]['id'], data['results'][1]['id'],
|
||||
data['results'][2]['id'], data['results'][3]['id']}
|
||||
self.assertSetEqual(
|
||||
ids, {fx_addon.pk, fx_fr_addon.pk, fn_addon.pk, fn_fr_addon.pk})
|
||||
|
||||
def test_filter_by_recommended(self):
|
||||
addon = addon_factory(slug='my-addon', name=u'Recomménded Addôn')
|
||||
addon_factory(slug='other-addon', name=u'Other Addôn')
|
||||
|
@ -2043,166 +1974,62 @@ class TestAddonAutoCompleteSearchView(ESTestCase):
|
|||
addon.pk, addon2.pk}
|
||||
|
||||
|
||||
class TestAddonFeaturedView(TestCase):
|
||||
class TestAddonFeaturedView(ESTestCase):
|
||||
client_class = APITestClient
|
||||
|
||||
fixtures = ['base/users']
|
||||
|
||||
def setUp(self):
|
||||
self.url = reverse_ns('addon-featured')
|
||||
super().setUp()
|
||||
# This api endpoint only still exists in v3.
|
||||
self.url = reverse_ns('addon-featured', api_version='v3')
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
self.empty_index('default')
|
||||
self.refresh()
|
||||
|
||||
def test_basic(self):
|
||||
addon1 = addon_factory()
|
||||
DiscoveryItem.objects.create(addon=addon1, recommendable=True)
|
||||
addon1.current_version.update(recommendation_approved=True)
|
||||
addon2 = addon_factory()
|
||||
DiscoveryItem.objects.create(addon=addon2, recommendable=True)
|
||||
addon2.current_version.update(recommendation_approved=True)
|
||||
assert addon1.is_recommended
|
||||
assert addon2.is_recommended
|
||||
addon_factory() # not recommended so shouldn't show up
|
||||
self.refresh()
|
||||
|
||||
def test_no_parameters(self):
|
||||
response = self.client.get(self.url)
|
||||
assert response.status_code == 400
|
||||
assert json.loads(force_text(response.content)) == {
|
||||
'detail': 'Invalid app, category and/or type parameter(s).'}
|
||||
|
||||
@patch('olympia.addons.views.get_featured_ids')
|
||||
def test_app_only(self, get_featured_ids_mock):
|
||||
addon1 = addon_factory()
|
||||
addon2 = addon_factory()
|
||||
get_featured_ids_mock.return_value = [addon1.pk, addon2.pk]
|
||||
|
||||
response = self.client.get(self.url, {'app': 'firefox'})
|
||||
assert get_featured_ids_mock.call_count == 1
|
||||
assert (get_featured_ids_mock.call_args_list[0][0][0] ==
|
||||
amo.FIREFOX) # app
|
||||
assert (get_featured_ids_mock.call_args_list[0][1] ==
|
||||
{'types': None, 'lang': None})
|
||||
assert response.status_code == 200
|
||||
data = json.loads(force_text(response.content))
|
||||
assert data['results']
|
||||
assert len(data['results']) == 2
|
||||
assert data['results'][0]['id'] == addon1.pk
|
||||
assert data['results'][1]['id'] == addon2.pk
|
||||
|
||||
@patch('olympia.addons.views.get_featured_ids')
|
||||
def test_app_and_type(self, get_featured_ids_mock):
|
||||
addon1 = addon_factory()
|
||||
addon2 = addon_factory()
|
||||
get_featured_ids_mock.return_value = [addon1.pk, addon2.pk]
|
||||
|
||||
response = self.client.get(self.url, {
|
||||
'app': 'firefox', 'type': 'extension'
|
||||
})
|
||||
assert get_featured_ids_mock.call_count == 1
|
||||
assert (get_featured_ids_mock.call_args_list[0][0][0] ==
|
||||
amo.FIREFOX) # app
|
||||
assert (get_featured_ids_mock.call_args_list[0][1] ==
|
||||
{'types': [amo.ADDON_EXTENSION], 'lang': None})
|
||||
assert response.status_code == 200
|
||||
data = json.loads(force_text(response.content))
|
||||
assert data['results']
|
||||
assert len(data['results']) == 2
|
||||
assert data['results'][0]['id'] == addon1.pk
|
||||
assert data['results'][1]['id'] == addon2.pk
|
||||
|
||||
@patch('olympia.addons.views.get_featured_ids')
|
||||
def test_app_and_types(self, get_featured_ids_mock):
|
||||
addon1 = addon_factory()
|
||||
addon2 = addon_factory()
|
||||
get_featured_ids_mock.return_value = [addon1.pk, addon2.pk]
|
||||
|
||||
response = self.client.get(self.url, {
|
||||
'app': 'firefox', 'type': 'extension,statictheme'
|
||||
})
|
||||
assert get_featured_ids_mock.call_count == 1
|
||||
assert (get_featured_ids_mock.call_args_list[0][0][0] ==
|
||||
amo.FIREFOX) # app
|
||||
assert (get_featured_ids_mock.call_args_list[0][1] ==
|
||||
{'types': [amo.ADDON_EXTENSION, amo.ADDON_STATICTHEME],
|
||||
'lang': None})
|
||||
assert response.status_code == 200
|
||||
data = json.loads(force_text(response.content))
|
||||
assert data['results']
|
||||
assert len(data['results']) == 2
|
||||
assert data['results'][0]['id'] == addon1.pk
|
||||
assert data['results'][1]['id'] == addon2.pk
|
||||
|
||||
@patch('olympia.addons.views.get_featured_ids')
|
||||
def test_app_and_type_and_lang(self, get_featured_ids_mock):
|
||||
addon1 = addon_factory()
|
||||
addon2 = addon_factory()
|
||||
get_featured_ids_mock.return_value = [addon1.pk, addon2.pk]
|
||||
|
||||
response = self.client.get(self.url, {
|
||||
'app': 'firefox', 'type': 'extension', 'lang': 'es'
|
||||
})
|
||||
assert get_featured_ids_mock.call_count == 1
|
||||
assert (get_featured_ids_mock.call_args_list[0][0][0] ==
|
||||
amo.FIREFOX) # app
|
||||
assert (get_featured_ids_mock.call_args_list[0][1] ==
|
||||
{'types': [amo.ADDON_EXTENSION], 'lang': 'es'})
|
||||
assert response.status_code == 200
|
||||
data = json.loads(force_text(response.content))
|
||||
assert data['results']
|
||||
assert len(data['results']) == 2
|
||||
assert data['results'][0]['id'] == addon1.pk
|
||||
assert data['results'][1]['id'] == addon2.pk
|
||||
# order is random
|
||||
ids = {result['id'] for result in data['results']}
|
||||
assert ids == {addon1.id, addon2.id}
|
||||
|
||||
def test_invalid_app(self):
|
||||
response = self.client.get(
|
||||
self.url, {'app': 'foxeh', 'type': 'extension'})
|
||||
assert response.status_code == 400
|
||||
assert json.loads(force_text(response.content)) == {
|
||||
'detail': 'Invalid app, category and/or type parameter(s).'}
|
||||
assert json.loads(force_text(response.content)) == [
|
||||
'Invalid "app" parameter.']
|
||||
|
||||
def test_invalid_type(self):
|
||||
response = self.client.get(self.url, {'app': 'firefox', 'type': 'lol'})
|
||||
assert response.status_code == 400
|
||||
assert json.loads(force_text(response.content)) == {
|
||||
'detail': 'Invalid app, category and/or type parameter(s).'}
|
||||
|
||||
def test_category_no_app_or_type(self):
|
||||
response = self.client.get(self.url, {'category': 'lol'})
|
||||
assert response.status_code == 400
|
||||
assert json.loads(force_text(response.content)) == {
|
||||
'detail': 'Invalid app, category and/or type parameter(s).'}
|
||||
assert json.loads(force_text(response.content)) == [
|
||||
'Invalid "type" parameter.']
|
||||
|
||||
def test_invalid_category(self):
|
||||
response = self.client.get(self.url, {
|
||||
'category': 'lol', 'app': 'firefox', 'type': 'extension'
|
||||
})
|
||||
assert response.status_code == 400
|
||||
assert json.loads(force_text(response.content)) == {
|
||||
'detail': 'Invalid app, category and/or type parameter(s).'}
|
||||
|
||||
@patch('olympia.addons.views.get_creatured_ids')
|
||||
def test_category(self, get_creatured_ids_mock):
|
||||
addon1 = addon_factory()
|
||||
addon2 = addon_factory()
|
||||
get_creatured_ids_mock.return_value = [addon1.pk, addon2.pk]
|
||||
|
||||
response = self.client.get(self.url, {
|
||||
'category': 'alerts-updates', 'app': 'firefox', 'type': 'extension'
|
||||
})
|
||||
assert get_creatured_ids_mock.call_count == 1
|
||||
assert get_creatured_ids_mock.call_args_list[0][0][0] == 72 # category
|
||||
assert get_creatured_ids_mock.call_args_list[0][0][1] is None # lang
|
||||
assert response.status_code == 200
|
||||
data = json.loads(force_text(response.content))
|
||||
assert data['results']
|
||||
assert len(data['results']) == 2
|
||||
assert data['results'][0]['id'] == addon1.pk
|
||||
assert data['results'][1]['id'] == addon2.pk
|
||||
|
||||
@patch('olympia.addons.views.get_creatured_ids')
|
||||
def test_category_with_lang(self, get_creatured_ids_mock):
|
||||
addon1 = addon_factory()
|
||||
addon2 = addon_factory()
|
||||
get_creatured_ids_mock.return_value = [addon1.pk, addon2.pk]
|
||||
|
||||
response = self.client.get(self.url, {
|
||||
'category': 'alerts-updates', 'app': 'firefox',
|
||||
'type': 'extension', 'lang': 'fr',
|
||||
})
|
||||
assert get_creatured_ids_mock.call_count == 1
|
||||
assert get_creatured_ids_mock.call_args_list[0][0][0] == 72 # cat id.
|
||||
assert get_creatured_ids_mock.call_args_list[0][0][1] == 'fr' # lang
|
||||
assert response.status_code == 200
|
||||
data = json.loads(force_text(response.content))
|
||||
assert data['results']
|
||||
assert len(data['results']) == 2
|
||||
assert data['results'][0]['id'] == addon1.pk
|
||||
assert data['results'][1]['id'] == addon2.pk
|
||||
assert json.loads(force_text(response.content)) == [
|
||||
'Invalid "category" parameter.']
|
||||
|
||||
|
||||
class TestStaticCategoryView(TestCase):
|
||||
|
|
|
@ -11,7 +11,7 @@ from django.views.decorators.cache import cache_page
|
|||
from elasticsearch_dsl import Q, query, Search
|
||||
from rest_framework import exceptions, serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.generics import GenericAPIView, ListAPIView
|
||||
from rest_framework.generics import ListAPIView
|
||||
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
|
@ -22,7 +22,6 @@ import olympia.core.logger
|
|||
|
||||
from olympia import amo
|
||||
from olympia.access import acl
|
||||
from olympia.amo.models import manual_order
|
||||
from olympia.amo.urlresolvers import get_outgoing_url
|
||||
from olympia.api.pagination import ESPageNumberPagination
|
||||
from olympia.api.permissions import (
|
||||
|
@ -31,7 +30,7 @@ from olympia.api.permissions import (
|
|||
from olympia.constants.categories import CATEGORIES_BY_ID
|
||||
from olympia.search.filters import (
|
||||
AddonAppQueryParam, AddonAppVersionQueryParam, AddonAuthorQueryParam,
|
||||
AddonCategoryQueryParam, AddonTypeQueryParam, AutoCompleteSortFilter,
|
||||
AddonTypeQueryParam, AutoCompleteSortFilter,
|
||||
ReviewedContentFilter, SearchParameterFilter, SearchQueryFilter,
|
||||
SortingFilter)
|
||||
from olympia.translations.query import order_by_translation
|
||||
|
@ -47,7 +46,7 @@ from .serializers import (
|
|||
ReplacementAddonSerializer, StaticCategorySerializer, VersionSerializer)
|
||||
from .utils import (
|
||||
get_addon_recommendations, get_addon_recommendations_invalid,
|
||||
get_creatured_ids, get_featured_ids, is_outcome_recommended)
|
||||
is_outcome_recommended)
|
||||
|
||||
|
||||
log = olympia.core.logger.getLogger('z.addons')
|
||||
|
@ -98,22 +97,6 @@ class BaseFilter(object):
|
|||
"""Get the queryset for the given field."""
|
||||
return getattr(self, 'filter_{0}'.format(field))()
|
||||
|
||||
def filter_featured(self):
|
||||
ids = self.model.featured_random(self.request.APP, self.request.LANG)
|
||||
return manual_order(self.base_queryset, ids, 'addons.id')
|
||||
|
||||
def filter_free(self):
|
||||
if self.model == Addon:
|
||||
return self.base_queryset.top_free(self.request.APP, listed=False)
|
||||
else:
|
||||
return self.base_queryset.top_free(listed=False)
|
||||
|
||||
def filter_paid(self):
|
||||
if self.model == Addon:
|
||||
return self.base_queryset.top_paid(self.request.APP, listed=False)
|
||||
else:
|
||||
return self.base_queryset.top_paid(listed=False)
|
||||
|
||||
def filter_popular(self):
|
||||
return self.base_queryset.order_by('-weekly_downloads')
|
||||
|
||||
|
@ -433,10 +416,7 @@ class AddonAutoCompleteSearchView(AddonSearchView):
|
|||
return Response({'results': serializer.data})
|
||||
|
||||
|
||||
class AddonFeaturedView(GenericAPIView):
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
serializer_class = AddonSerializer
|
||||
class AddonFeaturedView(AddonSearchView):
|
||||
# We accept the 'page_size' parameter but we do not allow pagination for
|
||||
# this endpoint since the order is random.
|
||||
pagination_class = None
|
||||
|
@ -445,61 +425,18 @@ class AddonFeaturedView(GenericAPIView):
|
|||
queryset = self.filter_queryset(self.get_queryset())
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
|
||||
# Simulate pagination-like results, without actual pagination.
|
||||
return Response({'results': serializer.data})
|
||||
|
||||
@classmethod
|
||||
def as_view(cls, **kwargs):
|
||||
view = super(AddonFeaturedView, cls).as_view(**kwargs)
|
||||
return non_atomic_requests(view)
|
||||
|
||||
def get_queryset(self):
|
||||
return Addon.objects.valid()
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
# We can pass the optional lang parameter to either get_creatured_ids()
|
||||
# or get_featured_ids() below to get locale-specific results in
|
||||
# addition to the generic ones.
|
||||
lang = self.request.GET.get('lang')
|
||||
if 'category' in self.request.GET:
|
||||
# If a category is passed then the app and type parameters are
|
||||
# mandatory because we need to find a category in the constants to
|
||||
# pass to get_creatured_ids(), and category slugs are not unique.
|
||||
# AddonCategoryQueryParam parses the request parameters for us to
|
||||
# determine the category.
|
||||
try:
|
||||
categories = AddonCategoryQueryParam(self.request).get_value()
|
||||
except ValueError:
|
||||
raise exceptions.ParseError(
|
||||
'Invalid app, category and/or type parameter(s).')
|
||||
ids = []
|
||||
for category in categories:
|
||||
ids.extend(get_creatured_ids(category, lang))
|
||||
else:
|
||||
# If no category is passed, only the app parameter is mandatory,
|
||||
# because get_featured_ids() needs it to find the right collection
|
||||
# to pick addons from. It can optionally filter by type, so we
|
||||
# parse request for that as well.
|
||||
try:
|
||||
app = AddonAppQueryParam(
|
||||
self.request).get_object_from_reverse_dict()
|
||||
types = None
|
||||
if 'type' in self.request.GET:
|
||||
types = AddonTypeQueryParam(self.request).get_value()
|
||||
except ValueError:
|
||||
raise exceptions.ParseError(
|
||||
'Invalid app, category and/or type parameter(s).')
|
||||
ids = get_featured_ids(app, lang=lang, types=types)
|
||||
# ids is going to be a random list of ids, we just slice it to get
|
||||
# the number of add-ons that was requested. We do it before calling
|
||||
# manual_order(), since it'll use the ids as part of a id__in filter.
|
||||
try:
|
||||
page_size = int(
|
||||
self.request.GET.get('page_size', api_settings.PAGE_SIZE))
|
||||
except ValueError:
|
||||
raise exceptions.ParseError('Invalid page_size parameter')
|
||||
ids = ids[:page_size]
|
||||
return manual_order(queryset, ids, 'addons.id')
|
||||
# Simulate pagination-like results, without actual pagination.
|
||||
return Response({'results': serializer.data[:page_size]})
|
||||
|
||||
def filter_queryset(self, qs):
|
||||
qs = super().filter_queryset(qs)
|
||||
qs = qs.query(query.Bool(must=[Q('term', is_recommended=True)]))
|
||||
return qs.query('function_score', functions=[query.SF('random_score')])
|
||||
|
||||
|
||||
class StaticCategoryView(ListAPIView):
|
||||
|
|
|
@ -1684,6 +1684,7 @@ DRF_API_GATES = {
|
|||
'activity-user-shim',
|
||||
'autocomplete-sort-param',
|
||||
'is-source-public-shim',
|
||||
'is-featured-addon-shim',
|
||||
),
|
||||
'v4': (
|
||||
'l10n_flat_input_output',
|
||||
|
|
|
@ -329,31 +329,7 @@ class AddonFeaturedQueryParam(AddonQueryParam):
|
|||
query_param = 'featured'
|
||||
reverse_dict = {'true': True}
|
||||
valid_values = [True]
|
||||
|
||||
def get_es_query(self):
|
||||
self.get_value() # Call to validate the value - we only want True.
|
||||
app_filter = AddonAppQueryParam(self.request)
|
||||
app = (app_filter.get_value()
|
||||
if self.request.GET.get(app_filter.query_param) else None)
|
||||
locale = self.request.GET.get('lang')
|
||||
if not app and not locale:
|
||||
# If neither app nor locale is specified fall back on is_featured.
|
||||
# It's a simple term clause.
|
||||
return [Q('term', is_featured=True)]
|
||||
# If either app or locale are specified then we need to do a nested
|
||||
# query to look for the right featured_for fields.
|
||||
clauses = []
|
||||
if app:
|
||||
# Search for featured collections targeting `app`.
|
||||
clauses.append(
|
||||
Q('term', **{'featured_for.application': app}))
|
||||
if locale:
|
||||
# Search for featured collections targeting `locale` or all locales
|
||||
# ('ALL' is the null_value for featured_for.locales).
|
||||
clauses.append(
|
||||
Q('terms', **{'featured_for.locales': [locale, 'ALL']}))
|
||||
return [Q('nested', path='featured_for', query=query.Bool(
|
||||
filter=clauses))]
|
||||
es_field = 'is_recommended'
|
||||
|
||||
|
||||
class AddonRecommendedQueryParam(AddonQueryParam):
|
||||
|
|
|
@ -849,52 +849,12 @@ class TestSearchParameterFilter(FilterTestsBase):
|
|||
qs = self._filter(data={'featured': 'true'})
|
||||
assert 'must' not in qs['query']['bool']
|
||||
filter_ = qs['query']['bool']['filter']
|
||||
assert {'term': {'is_featured': True}} in filter_
|
||||
assert {'term': {'is_recommended': True}} in filter_
|
||||
|
||||
with self.assertRaises(serializers.ValidationError) as context:
|
||||
self._filter(data={'featured': 'false'})
|
||||
assert context.exception.detail == ['Invalid "featured" parameter.']
|
||||
|
||||
def test_search_by_featured_yes_app_no_locale(self):
|
||||
qs = self._filter(data={'featured': 'true', 'app': 'firefox'})
|
||||
assert 'must' not in qs['query']['bool']
|
||||
filter_ = qs['query']['bool']['filter']
|
||||
assert len(filter_) == 2
|
||||
assert filter_[0] == {'term': {'app': amo.FIREFOX.id}}
|
||||
inner = filter_[1]['nested']['query']['bool']['filter']
|
||||
assert len(inner) == 1
|
||||
assert {'term': {'featured_for.application': amo.FIREFOX.id}} in inner
|
||||
|
||||
with self.assertRaises(serializers.ValidationError) as context:
|
||||
self._filter(data={'featured': 'true', 'app': 'foobaa'})
|
||||
assert context.exception.detail == ['Invalid "app" parameter.']
|
||||
|
||||
def test_search_by_featured_yes_app_yes_locale(self):
|
||||
qs = self._filter(data={'featured': 'true', 'app': 'firefox',
|
||||
'lang': 'fr'})
|
||||
assert 'must' not in qs['query']['bool']
|
||||
filter_ = qs['query']['bool']['filter']
|
||||
assert len(filter_) == 2
|
||||
assert filter_[0] == {'term': {'app': amo.FIREFOX.id}}
|
||||
inner = filter_[1]['nested']['query']['bool']['filter']
|
||||
assert len(inner) == 2
|
||||
assert {'term': {'featured_for.application': amo.FIREFOX.id}} in inner
|
||||
assert {'terms': {'featured_for.locales': ['fr', 'ALL']}} in inner
|
||||
|
||||
with self.assertRaises(serializers.ValidationError) as context:
|
||||
self._filter(data={'featured': 'true', 'app': 'foobaa'})
|
||||
assert context.exception.detail == ['Invalid "app" parameter.']
|
||||
|
||||
def test_search_by_featured_no_app_yes_locale(self):
|
||||
qs = self._filter(data={'featured': 'true', 'lang': 'fr'})
|
||||
assert 'must' not in qs['query']['bool']
|
||||
assert 'must_not' not in qs['query']['bool']
|
||||
filter_ = qs['query']['bool']['filter']
|
||||
assert len(filter_) == 1
|
||||
inner = filter_[0]['nested']['query']['bool']['filter']
|
||||
assert len(inner) == 1
|
||||
assert {'terms': {'featured_for.locales': ['fr', 'ALL']}} in inner
|
||||
|
||||
def test_search_by_recommended(self):
|
||||
qs = self._filter(data={'recommended': 'true'})
|
||||
assert 'must' not in qs['query']['bool']
|
||||
|
|
Загрузка…
Ссылка в новой задаче