rm featured in the v4 api/shim in v3 api (#12949)

This commit is contained in:
Andrew Williamson 2019-11-25 16:06:36 +00:00 коммит произвёл GitHub
Родитель e578e9c6df
Коммит 70a2c8e98a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
12 изменённых файлов: 80 добавлений и 469 удалений

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

@ -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']