diff --git a/conftest.py b/conftest.py index f39e8c56f7..1da2dd21d2 100644 --- a/conftest.py +++ b/conftest.py @@ -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 diff --git a/docs/topics/api/addons.rst b/docs/topics/api/addons.rst index c3f2c638ed..9f0b5cb212 100644 --- a/docs/topics/api/addons.rst +++ b/docs/topics/api/addons.rst @@ -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 ` 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 `) + :query string type: Filter by :ref:`add-on 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 ` 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 `). :>json string results[].guid: The add-on `extension identifier `_. diff --git a/settings.py b/settings.py index cf5b3c91c3..eec1b7355b 100644 --- a/settings.py +++ b/settings.py @@ -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, } } diff --git a/settings_test.py b/settings_test.py index 11cbfe238c..97a14ae86c 100644 --- a/settings_test.py +++ b/settings_test.py @@ -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', } } diff --git a/src/olympia/addons/serializers.py b/src/olympia/addons/serializers.py index 91441a4f82..331e5027f6 100644 --- a/src/olympia/addons/serializers.py +++ b/src/olympia/addons/serializers.py @@ -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() diff --git a/src/olympia/addons/tests/test_serializers.py b/src/olympia/addons/tests/test_serializers.py index 6627fece93..37ebefe541 100644 --- a/src/olympia/addons/tests/test_serializers.py +++ b/src/olympia/addons/tests/test_serializers.py @@ -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): diff --git a/src/olympia/addons/tests/test_views.py b/src/olympia/addons/tests/test_views.py index 4ed3716147..08686e9ec5 100644 --- a/src/olympia/addons/tests/test_views.py +++ b/src/olympia/addons/tests/test_views.py @@ -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 == ( - response.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): diff --git a/src/olympia/addons/views.py b/src/olympia/addons/views.py index c215ab6853..90319c69eb 100644 --- a/src/olympia/addons/views.py +++ b/src/olympia/addons/views.py @@ -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: + application = AddonAppQueryParam(self.request).get_value() + except ValueError: + raise exceptions.ParseError('Invalid or missing app parameter.') + + # appversion parameter is optional. + if AddonAppVersionQueryParam.query_param in self.request.GET: try: - self.application_id = AddonAppQueryParam( - self.request).get_value() + value = AddonAppVersionQueryParam(self.request).get_values() + appversions = { + 'min': value[1], + 'max': value[2] + } except ValueError: - raise exceptions.ParseError('Invalid app parameter.') - return self.application_id + 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): diff --git a/src/olympia/conf/dev/settings.py b/src/olympia/conf/dev/settings.py index 0d7b9f9255..e695361d50 100644 --- a/src/olympia/conf/dev/settings.py +++ b/src/olympia/conf/dev/settings.py @@ -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}, diff --git a/src/olympia/conf/prod/settings.py b/src/olympia/conf/prod/settings.py index 6beef97e2f..5d7d47d4b5 100644 --- a/src/olympia/conf/prod/settings.py +++ b/src/olympia/conf/prod/settings.py @@ -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({ diff --git a/src/olympia/conf/stage/settings.py b/src/olympia/conf/stage/settings.py index 93042b8c34..0a7a93c1e7 100644 --- a/src/olympia/conf/stage/settings.py +++ b/src/olympia/conf/stage/settings.py @@ -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},