Implement appversion compatibility filtering in language tools API
- Allow filtering by type to only show language packs - Expose a new current_compatible_version if appversion is passed. - This property will fetch the latest publicly available version compatible with the appversion passed. - Replace caching with a filesystem-based implementation to work around cache eviction issues.
This commit is contained in:
Родитель
bf98023a8a
Коммит
c125bf9354
|
@ -1,5 +1,5 @@
|
|||
from django import http, test
|
||||
from django.core.cache import cache
|
||||
from django.core.cache import caches
|
||||
from django.utils import translation
|
||||
|
||||
import caching
|
||||
|
@ -86,7 +86,8 @@ def default_prefixer(settings):
|
|||
|
||||
@pytest.yield_fixture(autouse=True)
|
||||
def test_pre_setup(request, tmpdir, settings):
|
||||
cache.clear()
|
||||
caches['default'].clear()
|
||||
caches['filesystem'].clear()
|
||||
# Override django-cache-machine caching.base.TIMEOUT because it's
|
||||
# computed too early, before settings_test.py is imported.
|
||||
caching.base.TIMEOUT = settings.CACHE_COUNT_TIMEOUT
|
||||
|
|
|
@ -457,10 +457,16 @@ on AMO.
|
|||
it's up to the clients to re-order results as needed before displaying
|
||||
the add-ons to the end-users.
|
||||
|
||||
In addition, the results can be cached for up to 24 hours, based on the
|
||||
full URL used in the request.
|
||||
|
||||
:query string app: Mandatory. Filter by :ref:`add-on application <addon-detail-application>` availability.
|
||||
:query string appversion: Filter by application version compatibility. Pass the full version as a string, e.g. ``46.0``. Only valid when both the ``app`` and ``type`` parameters are also present, and only makes sense for Language Packs, since Dictionaries are always compatible with every application version.
|
||||
:query string lang: Activate translations in the specific language for that query. (See :ref:`translated fields <api-overview-translations>`)
|
||||
:query string type: Filter by :ref:`add-on type <addon-detail-type>`. The default is to return both Language Packs or Dictionaries.
|
||||
:>json array results: An array of language tools.
|
||||
:>json int results[].id: The add-on id on AMO.
|
||||
:>json object results[].current_compatible_version: Object holding the latest publicly available :ref:`version <version-detail-object>` of the add-on compatible with the ``appversion`` parameter used. Only present when ``appversion`` is passed and valid. For performance reasons, only the following version properties are returned on the object: ``id``, ``files``, ``reviewed``, and ``version``.
|
||||
:>json string results[].default_locale: The add-on default locale for translations.
|
||||
:>json string|object|null results[].name: The add-on name (See :ref:`translated fields <api-overview-translations>`).
|
||||
:>json string results[].guid: The add-on `extension identifier <https://developer.mozilla.org/en-US/Add-ons/Install_Manifests#id>`_.
|
||||
|
|
|
@ -19,6 +19,8 @@ INSTALLED_APPS += (
|
|||
'olympia.landfill',
|
||||
)
|
||||
|
||||
FILESYSTEM_CACHE_ROOT = os.path.join(TMP_PATH, 'cache')
|
||||
|
||||
# Using locmem deadlocks in certain scenarios. This should all be fixed,
|
||||
# hopefully, in Django1.7. At that point, we may try again, and remove this to
|
||||
# not require memcache installation for newcomers.
|
||||
|
@ -35,6 +37,10 @@ CACHES = {
|
|||
'default': {
|
||||
'BACKEND': 'caching.backends.memcached.MemcachedCache',
|
||||
'LOCATION': os.environ.get('MEMCACHE_LOCATION', 'localhost:11211'),
|
||||
},
|
||||
'filesystem': {
|
||||
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
|
||||
'LOCATION': FILESYSTEM_CACHE_ROOT,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from settings import * # noqa
|
||||
|
||||
from django.utils.functional import lazy
|
||||
|
||||
# Make sure the app needed to test translations is present.
|
||||
INSTALLED_APPS += TEST_INSTALLED_APPS
|
||||
|
||||
|
@ -39,6 +37,10 @@ CACHES = {
|
|||
'default': {
|
||||
'BACKEND': 'caching.backends.locmem.LocMemCache',
|
||||
'LOCATION': 'olympia',
|
||||
},
|
||||
'filesystem': { # In real settings it's a filesystem cache, not here.
|
||||
'BACKEND': 'caching.backends.locmem.LocMemCache',
|
||||
'LOCATION': 'olympia-filesystem',
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ from olympia.constants.applications import APPS_ALL
|
|||
from olympia.constants.base import ADDON_TYPE_CHOICES_API
|
||||
from olympia.constants.categories import CATEGORIES_BY_ID
|
||||
from olympia.files.models import File
|
||||
from olympia.search.filters import AddonAppVersionQueryParam
|
||||
from olympia.users.models import UserProfile
|
||||
from olympia.versions.models import ApplicationsVersions, License, Version
|
||||
|
||||
|
@ -144,11 +145,18 @@ class CompactLicenseSerializer(LicenseSerializer):
|
|||
fields = ('id', 'name', 'url')
|
||||
|
||||
|
||||
class SimpleVersionSerializer(serializers.ModelSerializer):
|
||||
compatibility = serializers.SerializerMethodField()
|
||||
is_strict_compatibility_enabled = serializers.SerializerMethodField()
|
||||
edit_url = serializers.SerializerMethodField()
|
||||
class MinimalVersionSerializer(serializers.ModelSerializer):
|
||||
files = FileSerializer(source='all_files', many=True)
|
||||
|
||||
class Meta:
|
||||
model = Version
|
||||
fields = ('id', 'files', 'reviewed', 'version')
|
||||
|
||||
|
||||
class SimpleVersionSerializer(MinimalVersionSerializer):
|
||||
compatibility = serializers.SerializerMethodField()
|
||||
edit_url = serializers.SerializerMethodField()
|
||||
is_strict_compatibility_enabled = serializers.SerializerMethodField()
|
||||
license = CompactLicenseSerializer()
|
||||
release_notes = TranslationSerializerField(source='releasenotes')
|
||||
url = serializers.SerializerMethodField()
|
||||
|
@ -166,13 +174,6 @@ class SimpleVersionSerializer(serializers.ModelSerializer):
|
|||
instance.license.version_instance = instance
|
||||
return super(SimpleVersionSerializer, self).to_representation(instance)
|
||||
|
||||
def get_url(self, obj):
|
||||
return absolutify(obj.get_url_path())
|
||||
|
||||
def get_edit_url(self, obj):
|
||||
return absolutify(obj.addon.get_dev_url(
|
||||
'versions.edit', args=[obj.pk], prefix_only=True))
|
||||
|
||||
def get_compatibility(self, obj):
|
||||
return {
|
||||
app.short: {
|
||||
|
@ -182,9 +183,16 @@ class SimpleVersionSerializer(serializers.ModelSerializer):
|
|||
} for app, compat in obj.compatible_apps.items()
|
||||
}
|
||||
|
||||
def get_edit_url(self, obj):
|
||||
return absolutify(obj.addon.get_dev_url(
|
||||
'versions.edit', args=[obj.pk], prefix_only=True))
|
||||
|
||||
def get_is_strict_compatibility_enabled(self, obj):
|
||||
return any(file_.strict_compatibility for file_ in obj.all_files)
|
||||
|
||||
def get_url(self, obj):
|
||||
return absolutify(obj.get_url_path())
|
||||
|
||||
|
||||
class SimpleESVersionSerializer(SimpleVersionSerializer):
|
||||
class Meta:
|
||||
|
@ -362,7 +370,10 @@ class AddonSerializer(serializers.ModelSerializer):
|
|||
return getattr(obj, 'tag_list', [])
|
||||
|
||||
def get_url(self, obj):
|
||||
return absolutify(obj.get_url_path())
|
||||
# Use get_detail_url(), get_url_path() does an extra check on
|
||||
# current_version that is annoying in subclasses which don't want to
|
||||
# load that version.
|
||||
return absolutify(obj.get_detail_url())
|
||||
|
||||
def get_edit_url(self, obj):
|
||||
return absolutify(obj.get_dev_url())
|
||||
|
@ -632,13 +643,40 @@ class StaticCategorySerializer(serializers.Serializer):
|
|||
class LanguageToolsSerializer(AddonSerializer):
|
||||
target_locale = serializers.CharField()
|
||||
locale_disambiguation = serializers.CharField()
|
||||
current_compatible_version = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Addon
|
||||
fields = ('id', 'default_locale', 'guid',
|
||||
fields = ('id', 'current_compatible_version', 'default_locale', 'guid',
|
||||
'locale_disambiguation', 'name', 'slug', 'target_locale',
|
||||
'type', 'url', )
|
||||
|
||||
def get_current_compatible_version(self, obj):
|
||||
compatible_versions = getattr(obj, 'compatible_versions', None)
|
||||
if compatible_versions is not None:
|
||||
data = MinimalVersionSerializer(
|
||||
compatible_versions, many=True).data
|
||||
try:
|
||||
# 99% of the cases there will only be one result, since most
|
||||
# language packs are automatically uploaded for a given app
|
||||
# version. If there are more, pick the most recent one.
|
||||
return data[0]
|
||||
except IndexError:
|
||||
# This should not happen, because the queryset in the view is
|
||||
# supposed to filter results to only return add-ons that do
|
||||
# have at least one compatible version, but let's not fail
|
||||
# too loudly if the unthinkable happens...
|
||||
pass
|
||||
return None
|
||||
|
||||
def to_representation(self, obj):
|
||||
data = super(LanguageToolsSerializer, self).to_representation(obj)
|
||||
request = self.context['request']
|
||||
if (AddonAppVersionQueryParam.query_param not in request.GET and
|
||||
'current_compatible_version' in data):
|
||||
data.pop('current_compatible_version')
|
||||
return data
|
||||
|
||||
|
||||
class ReplacementAddonSerializer(serializers.ModelSerializer):
|
||||
replacement = serializers.SerializerMethodField()
|
||||
|
|
|
@ -933,11 +933,43 @@ class TestLanguageToolsSerializerOutput(TestCase):
|
|||
assert result['target_locale'] == self.addon.target_locale
|
||||
assert result['type'] == 'language'
|
||||
assert result['url'] == absolutify(self.addon.get_url_path())
|
||||
assert 'current_compatible_version' not in result
|
||||
|
||||
def test_basic_dict(self):
|
||||
self.addon = addon_factory(type=amo.ADDON_DICT)
|
||||
result = self.serialize()
|
||||
assert result['type'] == 'dictionary'
|
||||
assert 'current_compatible_version' not in result
|
||||
|
||||
def test_current_compatible_version(self):
|
||||
self.addon = addon_factory(type=amo.ADDON_LPAPP)
|
||||
# compatible_versions is set by the view through prefetch, it
|
||||
# looks like a list.
|
||||
self.addon.compatible_versions = [self.addon.current_version]
|
||||
self.addon.compatible_versions[0].update(created=self.days_ago(1))
|
||||
# Create a new current version, just to prove that
|
||||
# current_compatible_version does not use that.
|
||||
version_factory(addon=self.addon)
|
||||
self.addon.reload
|
||||
assert (
|
||||
self.addon.compatible_versions[0] !=
|
||||
self.addon.current_version)
|
||||
self.request = APIRequestFactory().get('/?app=firefox&appversion=57.0')
|
||||
result = self.serialize()
|
||||
assert 'current_compatible_version' in result
|
||||
assert result['current_compatible_version'] is not None
|
||||
assert set(result['current_compatible_version'].keys()) == set(
|
||||
['id', 'files', 'reviewed', 'version'])
|
||||
|
||||
self.addon.compatible_versions = None
|
||||
result = self.serialize()
|
||||
assert 'current_compatible_version' in result
|
||||
assert result['current_compatible_version'] is None
|
||||
|
||||
self.addon.compatible_versions = []
|
||||
result = self.serialize()
|
||||
assert 'current_compatible_version' in result
|
||||
assert result['current_compatible_version'] is None
|
||||
|
||||
|
||||
class TestESAddonAutoCompleteSerializer(ESTestCase):
|
||||
|
|
|
@ -3254,19 +3254,26 @@ class TestLanguageToolsView(TestCase):
|
|||
super(TestLanguageToolsView, self).setUp()
|
||||
self.url = reverse('addon-language-tools')
|
||||
|
||||
def test_wrong_app(self):
|
||||
def test_wrong_app_or_no_app(self):
|
||||
response = self.client.get(self.url)
|
||||
assert response.status_code == 400
|
||||
assert response.data == {
|
||||
'detail': u'Invalid or missing app parameter.'}
|
||||
|
||||
response = self.client.get(self.url, {'app': 'foo'})
|
||||
assert response.status_code == 400
|
||||
assert response.data == {
|
||||
'detail': u'Invalid or missing app parameter.'}
|
||||
|
||||
def test_basic(self):
|
||||
dictionary = addon_factory(type=amo.ADDON_DICT, target_locale='fr')
|
||||
dictionary_spelling_variant = addon_factory(
|
||||
type=amo.ADDON_DICT, target_locale='fr',
|
||||
locale_disambiguation='For spelling reform')
|
||||
language_pack = addon_factory(type=amo.ADDON_LPAPP, target_locale='es')
|
||||
language_pack = addon_factory(
|
||||
type=amo.ADDON_LPAPP, target_locale='es',
|
||||
file_kw={'strict_compatibility': True},
|
||||
version_kw={'min_app_version': '57.0', 'max_app_version': '57.*'})
|
||||
|
||||
# These add-ons below should be ignored: they are either not public or
|
||||
# of the wrong type, not supporting the app we care about, or their
|
||||
|
@ -3297,6 +3304,136 @@ class TestLanguageToolsView(TestCase):
|
|||
|
||||
assert 'locale_disambiguation' in data['results'][0]
|
||||
assert 'target_locale' in data['results'][0]
|
||||
# We were not filtering by appversion, so we do not get the
|
||||
# current_compatible_version property.
|
||||
assert 'current_compatible_version' not in data['results'][0]
|
||||
|
||||
def test_with_appversion_but_no_type(self):
|
||||
response = self.client.get(
|
||||
self.url, {'app': 'firefox', 'appversion': '57.0'})
|
||||
assert response.status_code == 400
|
||||
assert response.data == {
|
||||
'detail': 'Invalid or missing type parameter while appversion '
|
||||
'parameter is set.'}
|
||||
|
||||
def test_with_invalid_appversion(self):
|
||||
response = self.client.get(
|
||||
self.url,
|
||||
{'app': 'firefox', 'type': 'language', 'appversion': u'foôbar'})
|
||||
assert response.status_code == 400
|
||||
assert response.data == {'detail': 'Invalid appversion parameter.'}
|
||||
|
||||
def test_with_appversion_filtering(self):
|
||||
# Add compatible add-ons. We're going to request language packs
|
||||
# compatible with 58.0.
|
||||
compatible_pack1 = addon_factory(
|
||||
name='Spanish Language Pack',
|
||||
type=amo.ADDON_LPAPP, target_locale='es',
|
||||
file_kw={'strict_compatibility': True},
|
||||
version_kw={'min_app_version': '57.0', 'max_app_version': '57.*'})
|
||||
compatible_pack1.current_version.update(created=self.days_ago(2))
|
||||
compatible_version1 = version_factory(
|
||||
addon=compatible_pack1, file_kw={'strict_compatibility': True},
|
||||
min_app_version='58.0', max_app_version='58.*')
|
||||
compatible_version1.update(created=self.days_ago(1))
|
||||
compatible_pack2 = addon_factory(
|
||||
name='French Language Pack',
|
||||
type=amo.ADDON_LPAPP, target_locale='fr',
|
||||
file_kw={'strict_compatibility': True},
|
||||
version_kw={'min_app_version': '58.0', 'max_app_version': '58.*'})
|
||||
compatible_version2 = compatible_pack2.current_version
|
||||
compatible_version2.update(created=self.days_ago(1))
|
||||
version_factory(
|
||||
addon=compatible_pack2, file_kw={'strict_compatibility': True},
|
||||
min_app_version='59.0', max_app_version='59.*')
|
||||
# Add a more recent version for both add-ons, that would be compatible
|
||||
# with 58.0, but is not public/listed so should not be returned.
|
||||
version_factory(
|
||||
addon=compatible_pack1, file_kw={'strict_compatibility': True},
|
||||
min_app_version='58.0', max_app_version='58.*',
|
||||
channel=amo.RELEASE_CHANNEL_UNLISTED)
|
||||
version_factory(
|
||||
addon=compatible_pack2,
|
||||
file_kw={'strict_compatibility': True,
|
||||
'status': amo.STATUS_DISABLED},
|
||||
min_app_version='58.0', max_app_version='58.*')
|
||||
|
||||
# Add a few of incompatible add-ons.
|
||||
incompatible_pack1 = addon_factory(
|
||||
name='German Language Pack (incompatible with 58.0)',
|
||||
type=amo.ADDON_LPAPP, target_locale='fr',
|
||||
file_kw={'strict_compatibility': True},
|
||||
version_kw={'min_app_version': '56.0', 'max_app_version': '56.*'})
|
||||
version_factory(
|
||||
addon=incompatible_pack1, file_kw={'strict_compatibility': True},
|
||||
min_app_version='59.0', max_app_version='59.*')
|
||||
addon_factory(
|
||||
name='Italian Language Pack (incompatible with 58.0)',
|
||||
type=amo.ADDON_LPAPP, target_locale='it',
|
||||
file_kw={'strict_compatibility': True},
|
||||
version_kw={'min_app_version': '59.0', 'max_app_version': '59.*'})
|
||||
addon_factory(
|
||||
name='Thunderbird Polish Language Pack',
|
||||
type=amo.ADDON_LPAPP, target_locale='pl',
|
||||
file_kw={'strict_compatibility': True},
|
||||
version_kw={
|
||||
'application': amo.THUNDERBIRD.id,
|
||||
'min_app_version': '58.0', 'max_app_version': '58.*'})
|
||||
# Even add a pack with a compatible version... not public. And another
|
||||
# one with a compatible version... not listed.
|
||||
incompatible_pack2 = addon_factory(
|
||||
name='Japanese Language Pack (public, but 58.0 version is not)',
|
||||
type=amo.ADDON_LPAPP, target_locale='ja',
|
||||
file_kw={'strict_compatibility': True},
|
||||
version_kw={'min_app_version': '57.0', 'max_app_version': '57.*'})
|
||||
version_factory(
|
||||
addon=incompatible_pack2,
|
||||
min_app_version='58.0', max_app_version='58.*',
|
||||
file_kw={'status': amo.STATUS_AWAITING_REVIEW,
|
||||
'strict_compatibility': True})
|
||||
incompatible_pack3 = addon_factory(
|
||||
name='Nederlands Language Pack (58.0 version is unlisted)',
|
||||
type=amo.ADDON_LPAPP, target_locale='ja',
|
||||
file_kw={'strict_compatibility': True},
|
||||
version_kw={'min_app_version': '57.0', 'max_app_version': '57.*'})
|
||||
version_factory(
|
||||
addon=incompatible_pack3,
|
||||
min_app_version='58.0', max_app_version='58.*',
|
||||
channel=amo.RELEASE_CHANNEL_UNLISTED,
|
||||
file_kw={'strict_compatibility': True})
|
||||
|
||||
# Test it.
|
||||
with self.assertNumQueries(5):
|
||||
# 5 queries, regardless of how many add-ons are returned:
|
||||
# - 1 for the add-ons
|
||||
# - 1 for the add-ons translations (name)
|
||||
# - 1 for the compatible versions (through prefetch_related)
|
||||
# - 1 for the applications versions for those versions
|
||||
# (we don't need it, but we're using the default Version
|
||||
# transformer to get the files... this could be improved.)
|
||||
# - 1 for the files for those versions
|
||||
response = self.client.get(
|
||||
self.url,
|
||||
{'app': 'firefox', 'appversion': '58.0', 'type': 'language',
|
||||
'lang': 'en-US'})
|
||||
assert response.status_code == 200, response.content
|
||||
results = response.data['results']
|
||||
assert len(results) == 2
|
||||
|
||||
# Ordering is not guaranteed by this API, but do check that the
|
||||
# current_compatible_version returned makes sense.
|
||||
assert results[0]['current_compatible_version']
|
||||
assert results[1]['current_compatible_version']
|
||||
|
||||
expected_versions = set((
|
||||
(compatible_pack1.pk, compatible_version1.pk),
|
||||
(compatible_pack2.pk, compatible_version2.pk),
|
||||
))
|
||||
returned_versions = set((
|
||||
(results[0]['id'], results[0]['current_compatible_version']['id']),
|
||||
(results[1]['id'], results[1]['current_compatible_version']['id']),
|
||||
))
|
||||
assert expected_versions == returned_versions
|
||||
|
||||
def test_memoize(self):
|
||||
addon_factory(type=amo.ADDON_DICT, target_locale='fr')
|
||||
|
@ -3308,26 +3445,31 @@ class TestLanguageToolsView(TestCase):
|
|||
type=amo.ADDON_LPAPP, target_locale='de',
|
||||
version_kw={'application': amo.THUNDERBIRD.id})
|
||||
|
||||
response = self.client.get(self.url, {'app': 'firefox'})
|
||||
with self.assertNumQueries(2):
|
||||
response = self.client.get(
|
||||
self.url, {'app': 'firefox', 'lang': 'fr'})
|
||||
assert response.status_code == 200
|
||||
assert len(json.loads(response.content)['results']) == 3
|
||||
|
||||
# Same again, should be cached; no queries.
|
||||
with self.assertNumQueries(0):
|
||||
assert self.client.get(self.url, {'app': 'firefox'}).content == (
|
||||
assert self.client.get(
|
||||
self.url, {'app': 'firefox', 'lang': 'fr'}).content == (
|
||||
response.content
|
||||
)
|
||||
# But different app is different
|
||||
with self.assertNumQueries(12):
|
||||
|
||||
with self.assertNumQueries(2):
|
||||
assert (
|
||||
self.client.get(self.url, {'app': 'thunderbird'}).content !=
|
||||
self.client.get(
|
||||
self.url, {'app': 'thunderbird', 'lang': 'fr'}).content !=
|
||||
response.content
|
||||
)
|
||||
# Same again, should be cached; no queries.
|
||||
with self.assertNumQueries(0):
|
||||
self.client.get(self.url, {'app': 'thunderbird'})
|
||||
# But throw in a lang request and not cached:
|
||||
with self.assertNumQueries(10):
|
||||
self.client.get(self.url, {'app': 'firefox', 'lang': 'fr'})
|
||||
self.client.get(self.url, {'app': 'thunderbird', 'lang': 'fr'})
|
||||
# Change the lang, we should get queries again.
|
||||
with self.assertNumQueries(2):
|
||||
self.client.get(self.url, {'app': 'firefox', 'lang': 'de'})
|
||||
|
||||
|
||||
class TestReplacementAddonView(TestCase):
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
from django import http
|
||||
from django.db.models import Prefetch
|
||||
from django.db.transaction import non_atomic_requests
|
||||
from django.shortcuts import get_list_or_404, get_object_or_404, redirect
|
||||
from django.utils.cache import patch_cache_control
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import ugettext
|
||||
from django.views.decorators.cache import cache_control
|
||||
from django.views.decorators.cache import cache_control, cache_page
|
||||
from django.views.decorators.vary import vary_on_headers
|
||||
|
||||
import caching.base as caching
|
||||
|
@ -25,7 +27,6 @@ from olympia import amo
|
|||
from olympia.abuse.models import send_abuse_report
|
||||
from olympia.access import acl
|
||||
from olympia.amo import messages
|
||||
from olympia.amo.cache_nuggets import memoize
|
||||
from olympia.amo.forms import AbuseForm
|
||||
from olympia.amo.models import manual_order
|
||||
from olympia.amo.urlresolvers import get_outgoing_url, get_url_prefix, reverse
|
||||
|
@ -39,9 +40,9 @@ from olympia.constants.categories import CATEGORIES_BY_ID
|
|||
from olympia.ratings.forms import RatingForm
|
||||
from olympia.ratings.models import GroupedRating, Rating
|
||||
from olympia.search.filters import (
|
||||
AddonAppQueryParam, AddonCategoryQueryParam, AddonGuidQueryParam,
|
||||
AddonTypeQueryParam, ReviewedContentFilter, SearchParameterFilter,
|
||||
SearchQueryFilter, SortingFilter)
|
||||
AddonAppQueryParam, AddonAppVersionQueryParam, AddonCategoryQueryParam,
|
||||
AddonGuidQueryParam, AddonTypeQueryParam, ReviewedContentFilter,
|
||||
SearchParameterFilter, SearchQueryFilter, SortingFilter)
|
||||
from olympia.translations.query import order_by_translation
|
||||
from olympia.versions.models import Version
|
||||
|
||||
|
@ -812,33 +813,127 @@ class LanguageToolsView(ListAPIView):
|
|||
return non_atomic_requests(
|
||||
super(LanguageToolsView, cls).as_view(**initkwargs))
|
||||
|
||||
def get_application_id(self):
|
||||
if not hasattr(self, 'application_id'):
|
||||
def get_query_params(self):
|
||||
"""
|
||||
Parse query parameters that this API supports:
|
||||
- app (mandatory)
|
||||
- type (optional)
|
||||
- appversion (optional, makes type mandatory)
|
||||
|
||||
Can raise ParseError() in case a mandatory parameter is missing or a
|
||||
parameter is invalid.
|
||||
|
||||
Returns a tuple with application, addon_types tuple (or None), and
|
||||
appversions dict (or None) ready to be consumed by the get_queryset_*()
|
||||
methods.
|
||||
"""
|
||||
# app parameter is mandatory when calling this API.
|
||||
try:
|
||||
self.application_id = AddonAppQueryParam(
|
||||
self.request).get_value()
|
||||
application = AddonAppQueryParam(self.request).get_value()
|
||||
except ValueError:
|
||||
raise exceptions.ParseError('Invalid app parameter.')
|
||||
return self.application_id
|
||||
raise exceptions.ParseError('Invalid or missing app parameter.')
|
||||
|
||||
# appversion parameter is optional.
|
||||
if AddonAppVersionQueryParam.query_param in self.request.GET:
|
||||
try:
|
||||
value = AddonAppVersionQueryParam(self.request).get_values()
|
||||
appversions = {
|
||||
'min': value[1],
|
||||
'max': value[2]
|
||||
}
|
||||
except ValueError:
|
||||
raise exceptions.ParseError('Invalid appversion parameter.')
|
||||
else:
|
||||
appversions = None
|
||||
|
||||
# type is optional, unless appversion is set. That's because the way
|
||||
# dicts and language packs have their compatibility info set in the
|
||||
# database differs, so to make things simpler for us we force clients
|
||||
# to filter by type if they want appversion filtering.
|
||||
if AddonTypeQueryParam.query_param in self.request.GET or appversions:
|
||||
try:
|
||||
addon_types = (AddonTypeQueryParam(self.request).get_value(),)
|
||||
except ValueError:
|
||||
raise exceptions.ParseError(
|
||||
'Invalid or missing type parameter while appversion '
|
||||
'parameter is set.')
|
||||
else:
|
||||
addon_types = (amo.ADDON_LPAPP, amo.ADDON_DICT)
|
||||
return application, addon_types, appversions
|
||||
|
||||
def get_queryset(self):
|
||||
types = (amo.ADDON_DICT, amo.ADDON_LPAPP)
|
||||
return Addon.objects.public().filter(
|
||||
appsupport__app=self.get_application_id(), type__in=types,
|
||||
target_locale__isnull=False).exclude(target_locale='')
|
||||
application, addon_types, appversions = self.get_query_params()
|
||||
if addon_types == (amo.ADDON_LPAPP,) and appversions:
|
||||
return self.get_language_packs_queryset_with_appversions(
|
||||
application, appversions)
|
||||
else:
|
||||
# appversions filtering only makes sense for language packs only,
|
||||
# so it's ignored here.
|
||||
return self.get_queryset_base(application, addon_types)
|
||||
|
||||
@memoize('API:language-tools', time=(60 * 60 * 24))
|
||||
def get_data(self, app_id, lang):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return serializer.data
|
||||
def get_queryset_base(self, application, addon_types):
|
||||
return (
|
||||
Addon.objects.public()
|
||||
.no_cache()
|
||||
.filter(appsupport__app=application, type__in=addon_types,
|
||||
target_locale__isnull=False)
|
||||
.exclude(target_locale='')
|
||||
# Deactivate default transforms which fetch a ton of stuff we
|
||||
# don't need here like authors, previews or current version.
|
||||
# It would be nice to avoid translations entirely, because the
|
||||
# translations transformer is going to fetch a lot of translations
|
||||
# we don't need, but some language packs or dictionaries have
|
||||
# custom names, so we can't use a generic one for them...
|
||||
.only_translations()
|
||||
# Since we're fetching everything with no pagination, might as well
|
||||
# not order it.
|
||||
.order_by()
|
||||
)
|
||||
|
||||
def get_language_packs_queryset_with_appversions(
|
||||
self, application, appversions):
|
||||
"""
|
||||
Return queryset to use specifically when requesting language packs
|
||||
compatible with a given app + versions.
|
||||
|
||||
application is an application id, and appversions is a dict with min
|
||||
and max keys pointing to application versions expressed as ints.
|
||||
"""
|
||||
# Version queryset we'll prefetch once for all results. We need to
|
||||
# find the ones compatible with the app+appversion requested, and we
|
||||
# can avoid loading translations by removing transforms and then
|
||||
# re-applying the default one that takes care of the files and compat
|
||||
# info.
|
||||
versions_qs = Version.objects.no_cache().filter(
|
||||
apps__application=application,
|
||||
apps__min__version_int__lte=appversions['min'],
|
||||
apps__max__version_int__gte=appversions['max'],
|
||||
channel=amo.RELEASE_CHANNEL_LISTED,
|
||||
files__status=amo.STATUS_PUBLIC,
|
||||
).no_transforms().transform(Version.transformer)
|
||||
qs = self.get_queryset_base(application, (amo.ADDON_LPAPP,))
|
||||
return (
|
||||
qs.prefetch_related(Prefetch('versions',
|
||||
to_attr='compatible_versions',
|
||||
queryset=versions_qs))
|
||||
.filter(versions__apps__application=application,
|
||||
versions__apps__min__version_int__lte=appversions['min'],
|
||||
versions__apps__max__version_int__gte=appversions['max'],
|
||||
versions__channel=amo.RELEASE_CHANNEL_LISTED,
|
||||
versions__files__status=amo.STATUS_PUBLIC)
|
||||
)
|
||||
|
||||
@method_decorator(cache_page(60 * 60 * 24, cache='filesystem'))
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super(LanguageToolsView, self).dispatch(*args, **kwargs)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
# Ignore pagination (return everything) but do wrap the data in a
|
||||
# 'results' property to mimic what the default implementation of list()
|
||||
# does in DRF.
|
||||
return Response({'results': self.get_data(
|
||||
self.get_application_id(), self.request.GET.get('lang'))})
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response({'results': serializer.data})
|
||||
|
||||
|
||||
class ReplacementAddonView(ListAPIView):
|
||||
|
|
|
@ -64,6 +64,24 @@ MOZLOG_NAME = SYSLOG_TAG
|
|||
SYSLOG_TAG2 = "http_app_addons_dev_timer"
|
||||
SYSLOG_CSP = "http_app_addons_dev_csp"
|
||||
|
||||
NETAPP_STORAGE_ROOT = env('NETAPP_STORAGE_ROOT')
|
||||
NETAPP_STORAGE = NETAPP_STORAGE_ROOT + '/shared_storage'
|
||||
GUARDED_ADDONS_PATH = NETAPP_STORAGE_ROOT + '/guarded-addons'
|
||||
MEDIA_ROOT = NETAPP_STORAGE + '/uploads'
|
||||
TMP_PATH = os.path.join(NETAPP_STORAGE, 'tmp')
|
||||
PACKAGER_PATH = os.path.join(TMP_PATH, 'packager')
|
||||
|
||||
ADDONS_PATH = NETAPP_STORAGE_ROOT + '/files'
|
||||
|
||||
# Must be forced in settings because name => path can't be dyncamically
|
||||
# computed: reviewer_attachmentS VS reviewer_attachment.
|
||||
# TODO: rename folder on file system.
|
||||
# (One can also just rename the setting, but this will not be consistent
|
||||
# with the naming scheme.)
|
||||
REVIEWER_ATTACHMENTS_PATH = MEDIA_ROOT + '/reviewer_attachment'
|
||||
|
||||
FILESYSTEM_CACHE_ROOT = NETAPP_STORAGE_ROOT + '/cache'
|
||||
|
||||
DATABASES = {}
|
||||
DATABASES['default'] = env.db('DATABASES_DEFAULT_URL')
|
||||
DATABASES['default']['ENGINE'] = 'django.db.backends.mysql'
|
||||
|
@ -85,7 +103,12 @@ SLAVE_DATABASES = ['slave']
|
|||
|
||||
CACHE_MIDDLEWARE_KEY_PREFIX = CACHE_PREFIX
|
||||
|
||||
CACHES = {}
|
||||
CACHES = {
|
||||
'filesystem': {
|
||||
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
|
||||
'LOCATION': FILESYSTEM_CACHE_ROOT,
|
||||
}
|
||||
}
|
||||
CACHES['default'] = env.cache('CACHES_DEFAULT')
|
||||
CACHES['default']['TIMEOUT'] = 500
|
||||
CACHES['default']['BACKEND'] = 'caching.backends.memcached.MemcachedCache'
|
||||
|
@ -103,22 +126,6 @@ CELERY_WORKER_DISABLE_RATE_LIMITS = True
|
|||
CELERY_WORKER_PREFETCH_MULTIPLIER = 1
|
||||
CELERY_RESULT_BACKEND = env('CELERY_RESULT_BACKEND')
|
||||
|
||||
NETAPP_STORAGE_ROOT = env('NETAPP_STORAGE_ROOT')
|
||||
NETAPP_STORAGE = NETAPP_STORAGE_ROOT + '/shared_storage'
|
||||
GUARDED_ADDONS_PATH = NETAPP_STORAGE_ROOT + '/guarded-addons'
|
||||
MEDIA_ROOT = NETAPP_STORAGE + '/uploads'
|
||||
TMP_PATH = os.path.join(NETAPP_STORAGE, 'tmp')
|
||||
PACKAGER_PATH = os.path.join(TMP_PATH, 'packager')
|
||||
|
||||
ADDONS_PATH = NETAPP_STORAGE_ROOT + '/files'
|
||||
|
||||
# Must be forced in settings because name => path can't be dyncamically
|
||||
# computed: reviewer_attachmentS VS reviewer_attachment.
|
||||
# TODO: rename folder on file system.
|
||||
# (One can also just rename the setting, but this will not be consistent
|
||||
# with the naming scheme.)
|
||||
REVIEWER_ATTACHMENTS_PATH = MEDIA_ROOT + '/reviewer_attachment'
|
||||
|
||||
LOGGING['loggers'].update({
|
||||
'amqp': {'level': logging.WARNING},
|
||||
'raven': {'level': logging.WARNING},
|
||||
|
|
|
@ -52,6 +52,25 @@ SYSLOG_TAG = "http_app_addons"
|
|||
SYSLOG_TAG2 = "http_app_addons_timer"
|
||||
SYSLOG_CSP = "http_app_addons_csp"
|
||||
|
||||
NETAPP_STORAGE_ROOT = env('NETAPP_STORAGE_ROOT')
|
||||
NETAPP_STORAGE = NETAPP_STORAGE_ROOT + '/shared_storage'
|
||||
GUARDED_ADDONS_PATH = NETAPP_STORAGE_ROOT + '/guarded-addons'
|
||||
MEDIA_ROOT = NETAPP_STORAGE + '/uploads'
|
||||
|
||||
TMP_PATH = os.path.join(NETAPP_STORAGE, 'tmp')
|
||||
PACKAGER_PATH = os.path.join(TMP_PATH, 'packager')
|
||||
|
||||
ADDONS_PATH = NETAPP_STORAGE_ROOT + '/files'
|
||||
|
||||
# Must be forced in settings because name => path can't be dyncamically
|
||||
# computed: reviewer_attachmentS VS reviewer_attachment.
|
||||
# TODO: rename folder on file system.
|
||||
# (One can also just rename the setting, but this will not be consistent
|
||||
# with the naming scheme.)
|
||||
REVIEWER_ATTACHMENTS_PATH = MEDIA_ROOT + '/reviewer_attachment'
|
||||
|
||||
FILESYSTEM_CACHE_ROOT = NETAPP_STORAGE_ROOT + '/cache'
|
||||
|
||||
DATABASES = {}
|
||||
DATABASES['default'] = env.db('DATABASES_DEFAULT_URL')
|
||||
DATABASES['default']['ENGINE'] = 'django.db.backends.mysql'
|
||||
|
@ -73,7 +92,12 @@ SLAVE_DATABASES = ['slave']
|
|||
|
||||
CACHE_MIDDLEWARE_KEY_PREFIX = CACHE_PREFIX
|
||||
|
||||
CACHES = {}
|
||||
CACHES = {
|
||||
'filesystem': {
|
||||
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
|
||||
'LOCATION': FILESYSTEM_CACHE_ROOT,
|
||||
}
|
||||
}
|
||||
CACHES['default'] = env.cache('CACHES_DEFAULT')
|
||||
CACHES['default']['TIMEOUT'] = 500
|
||||
CACHES['default']['BACKEND'] = 'caching.backends.memcached.MemcachedCache'
|
||||
|
@ -90,23 +114,6 @@ CELERY_WORKER_DISABLE_RATE_LIMITS = True
|
|||
CELERY_BROKER_CONNECTION_TIMEOUT = 0.5
|
||||
CELERY_RESULT_BACKEND = env('CELERY_RESULT_BACKEND')
|
||||
|
||||
NETAPP_STORAGE_ROOT = env('NETAPP_STORAGE_ROOT')
|
||||
NETAPP_STORAGE = NETAPP_STORAGE_ROOT + '/shared_storage'
|
||||
GUARDED_ADDONS_PATH = NETAPP_STORAGE_ROOT + '/guarded-addons'
|
||||
MEDIA_ROOT = NETAPP_STORAGE + '/uploads'
|
||||
|
||||
TMP_PATH = os.path.join(NETAPP_STORAGE, 'tmp')
|
||||
PACKAGER_PATH = os.path.join(TMP_PATH, 'packager')
|
||||
|
||||
ADDONS_PATH = NETAPP_STORAGE_ROOT + '/files'
|
||||
|
||||
# Must be forced in settings because name => path can't be dyncamically
|
||||
# computed: reviewer_attachmentS VS reviewer_attachment.
|
||||
# TODO: rename folder on file system.
|
||||
# (One can also just rename the setting, but this will not be consistent
|
||||
# with the naming scheme.)
|
||||
REVIEWER_ATTACHMENTS_PATH = MEDIA_ROOT + '/reviewer_attachment'
|
||||
|
||||
LOG_LEVEL = logging.DEBUG
|
||||
|
||||
LOGGING['loggers'].update({
|
||||
|
|
|
@ -61,6 +61,25 @@ SYSLOG_TAG = "http_app_addons_stage"
|
|||
SYSLOG_TAG2 = "http_app_addons_stage_timer"
|
||||
SYSLOG_CSP = "http_app_addons_stage_csp"
|
||||
|
||||
NETAPP_STORAGE_ROOT = env('NETAPP_STORAGE_ROOT')
|
||||
NETAPP_STORAGE = NETAPP_STORAGE_ROOT + '/shared_storage'
|
||||
GUARDED_ADDONS_PATH = NETAPP_STORAGE_ROOT + '/guarded-addons'
|
||||
MEDIA_ROOT = NETAPP_STORAGE + '/uploads'
|
||||
|
||||
TMP_PATH = os.path.join(NETAPP_STORAGE, 'tmp')
|
||||
PACKAGER_PATH = os.path.join(TMP_PATH, 'packager')
|
||||
|
||||
ADDONS_PATH = NETAPP_STORAGE_ROOT + '/files'
|
||||
|
||||
# Must be forced in settings because name => path can't be dyncamically
|
||||
# computed: reviewer_attachmentS VS reviewer_attachment.
|
||||
# TODO: rename folder on file system.
|
||||
# (One can also just rename the setting, but this will not be consistent
|
||||
# with the naming scheme.)
|
||||
REVIEWER_ATTACHMENTS_PATH = MEDIA_ROOT + '/reviewer_attachment'
|
||||
|
||||
FILESYSTEM_CACHE_ROOT = NETAPP_STORAGE_ROOT + '/cache'
|
||||
|
||||
DATABASES = {}
|
||||
DATABASES['default'] = env.db('DATABASES_DEFAULT_URL')
|
||||
DATABASES['default']['ENGINE'] = 'django.db.backends.mysql'
|
||||
|
@ -82,7 +101,12 @@ SLAVE_DATABASES = ['slave']
|
|||
|
||||
CACHE_MIDDLEWARE_KEY_PREFIX = CACHE_PREFIX
|
||||
|
||||
CACHES = {}
|
||||
CACHES = {
|
||||
'filesystem': {
|
||||
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
|
||||
'LOCATION': FILESYSTEM_CACHE_ROOT,
|
||||
}
|
||||
}
|
||||
CACHES['default'] = env.cache('CACHES_DEFAULT')
|
||||
CACHES['default']['TIMEOUT'] = 500
|
||||
CACHES['default']['BACKEND'] = 'caching.backends.memcached.MemcachedCache'
|
||||
|
@ -100,23 +124,6 @@ CELERY_WORKER_DISABLE_RATE_LIMITS = True
|
|||
CELERY_WORKER_PREFETCH_MULTIPLIER = 1
|
||||
CELERY_RESULT_BACKEND = env('CELERY_RESULT_BACKEND')
|
||||
|
||||
NETAPP_STORAGE_ROOT = env('NETAPP_STORAGE_ROOT')
|
||||
NETAPP_STORAGE = NETAPP_STORAGE_ROOT + '/shared_storage'
|
||||
GUARDED_ADDONS_PATH = NETAPP_STORAGE_ROOT + '/guarded-addons'
|
||||
MEDIA_ROOT = NETAPP_STORAGE + '/uploads'
|
||||
|
||||
TMP_PATH = os.path.join(NETAPP_STORAGE, 'tmp')
|
||||
PACKAGER_PATH = os.path.join(TMP_PATH, 'packager')
|
||||
|
||||
ADDONS_PATH = NETAPP_STORAGE_ROOT + '/files'
|
||||
|
||||
# Must be forced in settings because name => path can't be dyncamically
|
||||
# computed: reviewer_attachmentS VS reviewer_attachment.
|
||||
# TODO: rename folder on file system.
|
||||
# (One can also just rename the setting, but this will not be consistent
|
||||
# with the naming scheme.)
|
||||
REVIEWER_ATTACHMENTS_PATH = MEDIA_ROOT + '/reviewer_attachment'
|
||||
|
||||
LOGGING['loggers'].update({
|
||||
'z.task': {'level': logging.DEBUG},
|
||||
'z.redis': {'level': logging.DEBUG},
|
||||
|
|
Загрузка…
Ссылка в новой задаче