Fixes #9274

* Part 1: Remove legacy api
* Smaller cleanups
* Restore opensearch
* Remove CompatOverride.is_hosted
* Remove one more test
* Fix AuthenticationMiddlewareWithoutAPI middleware
This commit is contained in:
Christopher Grebs 2019-02-19 08:01:14 +01:00 коммит произвёл GitHub
Родитель 6287c53fbc
Коммит d0c435bca0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
30 изменённых файлов: 126 добавлений и 2355 удалений

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

@ -2054,10 +2054,6 @@ class CompatOverride(ModelBase):
else:
return self.guid
def is_hosted(self):
"""Am I talking about an add-on on AMO?"""
return bool(self.addon_id)
@staticmethod
def transformer(overrides):
if not overrides:

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

@ -2765,14 +2765,6 @@ class TestCompatOverride(TestCase):
actual = getattr(obj, key)
assert actual == expected
def test_is_hosted(self):
c = CompatOverride.objects.create(guid='a')
assert not c.is_hosted()
Addon.objects.create(type=1, guid='b')
c = CompatOverride.objects.create(guid='b')
assert c.is_hosted()
def test_override_type(self):
one = CompatOverride.objects.get(guid='one')

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

@ -97,11 +97,6 @@ class LocaleAndAppURLMiddleware(MiddlewareMixin):
activate(request.LANG)
request.APP = amo.APPS.get(prefixer.app, amo.FIREFOX)
# Match legacy api requests too - IdentifyAPIRequestMiddleware is v3+
# TODO - remove this when legacy_api goes away
# https://github.com/mozilla/addons-server/issues/9274
request.is_legacy_api = request.path_info.startswith('/api/')
class AuthenticationMiddlewareWithoutAPI(AuthenticationMiddleware):
"""
@ -109,8 +104,7 @@ class AuthenticationMiddlewareWithoutAPI(AuthenticationMiddleware):
own authentication mechanism.
"""
def process_request(self, request):
legacy_or_drf_api = request.is_api or request.is_legacy_api
if legacy_or_drf_api and not auth_path.match(request.path):
if request.is_api and not auth_path.match(request.path):
request.user = AnonymousUser()
else:
return super(

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

@ -37,7 +37,6 @@ class TestMiddleware(TestCase):
def test_authentication_used_outside_the_api(self, process_request):
req = RequestFactory().get('/')
req.is_api = False
req.is_legacy_api = False
AuthenticationMiddlewareWithoutAPI().process_request(req)
assert process_request.called
@ -46,13 +45,6 @@ class TestMiddleware(TestCase):
def test_authentication_not_used_with_the_api(self, process_request):
req = RequestFactory().get('/')
req.is_api = True
req.is_legacy_api = False
AuthenticationMiddlewareWithoutAPI().process_request(req)
assert not process_request.called
req = RequestFactory().get('/')
req.is_api = False
req.is_legacy_api = True
AuthenticationMiddlewareWithoutAPI().process_request(req)
assert not process_request.called

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

@ -30,13 +30,6 @@ class TestRedirects(TestCase):
redirect = response.redirect_chain[-1][0]
assert redirect.endswith('/en-US/firefox/addon/5326/')
def test_utf8(self):
"""Without proper unicode handling this will fail."""
response = self.client.get(u'/api/1.5/search/ツールバー',
follow=True)
# Sphinx will be off so let's just test that it redirects.
assert response.redirect_chain[0][1] == 302
def test_reviews(self):
response = self.client.get('/reviews/display/4', follow=True)
self.assert3xx(response, '/en-US/firefox/addon/a4/reviews/',

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

@ -81,18 +81,6 @@ class MiddlewareTest(TestCase):
response = self.process('/api/v4/some/endpoint/')
assert response is None
def test_v1_api_is_identified_as_api_request(self):
response = self.process('/en-US/firefox/api/')
assert response is None
assert self.request.LANG == 'en-US'
assert self.request.is_legacy_api
# double-check _only_ /api/ is marked as .is_api
response = self.process('/en-US/firefox/apii/')
assert response is None
assert self.request.LANG == 'en-US'
assert not self.request.is_legacy_api
def test_vary(self):
response = self.process('/')
assert response['Vary'] == 'Accept-Language, User-Agent'

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

@ -384,12 +384,6 @@ class TestCORS(TestCase):
assert not response.has_header('Access-Control-Allow-Origin')
assert not response.has_header('Access-Control-Allow-Credentials')
def test_no_cors_legacy_api(self):
response = self.get('/en-US/firefox/api/1.5/search/test')
assert response.status_code == 200
assert not response.has_header('Access-Control-Allow-Origin')
assert not response.has_header('Access-Control-Allow-Credentials')
def test_cors_api_v3(self):
url = reverse_ns('addon-detail', api_version='v3', args=(3615,))
assert '/api/v3/' in url

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

@ -1,9 +1,8 @@
from django.conf.urls import include, url
from django.views.decorators.cache import never_cache
from olympia.legacy_api import views as legacy_views
from . import views
from .utils import render_xml
services_patterns = [
@ -20,8 +19,7 @@ urlpatterns = [
url(r'^contribute\.json$', views.contribute, name='contribute.json'),
url(r'^services/', include(services_patterns)),
url(r'^__version__$', views.version, name='version.json'),
url(r'^opensearch\.xml$', legacy_views.render_xml,
{'template': 'amo/opensearch.xml'},
url(r'^opensearch\.xml$', render_xml, {'template': 'amo/opensearch.xml'},
name='amo.opensearch'),
]

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

@ -49,6 +49,8 @@ from html5lib.serializer import HTMLSerializer
from PIL import Image
from rest_framework.utils.encoders import JSONEncoder
from django.db.transaction import non_atomic_requests
from olympia.core.logger import getLogger
from olympia.amo import ADDON_ICON_SIZES, search
from olympia.amo.pagination import ESPaginator
@ -71,6 +73,36 @@ def from_string(string):
return engines['jinja2'].from_string(string)
def render_xml_to_string(request, template, context=None):
from olympia.amo.templatetags.jinja_helpers import strip_controls
if context is None:
context = {}
xml_env = engines['jinja2'].env.overlay()
old_finalize = xml_env.finalize
xml_env.finalize = lambda x: strip_controls(old_finalize(x))
for processor in engines['jinja2'].context_processors:
context.update(processor(request))
template = xml_env.get_template(template)
return template.render(context)
@non_atomic_requests
def render_xml(request, template, context=None, **kwargs):
"""Safely renders xml, stripping out nasty control characters."""
if context is None:
context = {}
rendered = render_xml_to_string(request, template, context)
if 'content_type' not in kwargs:
kwargs['content_type'] = 'text/xml'
return HttpResponse(rendered, **kwargs)
def days_ago(n):
return datetime.datetime.now() - datetime.timedelta(days=n)

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

@ -13,7 +13,7 @@ import six
from django_statsd.clients import statsd
from rest_framework.exceptions import NotFound
from olympia import amo, legacy_api
from olympia import amo
from olympia.amo.utils import render
from . import monitors
@ -68,11 +68,7 @@ def contribute(request):
@non_atomic_requests
def handler403(request, **kwargs):
if request.is_legacy_api:
# Pass over to handler403 view in api if api was targeted.
return legacy_api.views.handler403(request)
else:
return render(request, 'amo/403.html', status=403)
return render(request, 'amo/403.html', status=403)
@non_atomic_requests
@ -81,9 +77,6 @@ def handler404(request, **kwargs):
# It's a v3+ api request
return JsonResponse(
{'detail': six.text_type(NotFound.default_detail)}, status=404)
elif request.is_legacy_api:
# It's a legacy api request - pass over to legacy api handler404.
return legacy_api.views.handler404(request)
# X_IS_MOBILE_AGENTS is set by nginx as an env variable when it detects
# a mobile User Agent or when the mamo cookie is present.
if request.META.get('X_IS_MOBILE_AGENTS') == '1':
@ -94,11 +87,7 @@ def handler404(request, **kwargs):
@non_atomic_requests
def handler500(request, **kwargs):
if getattr(request, 'is_legacy_api', False):
# Pass over to handler500 view in api if api was targeted.
return legacy_api.views.handler500(request)
else:
return render(request, 'amo/500.html', status=500)
return render(request, 'amo/500.html', status=500)
@non_atomic_requests

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

@ -11,8 +11,6 @@ edit_urls = [
detail_urls = [
url(r'^$', views.collection_detail, name='collections.detail'),
url(r'^format:json$', views.collection_detail_json,
name='collections.detail.json'),
url(r'^edit/', include(edit_urls)),
url(r'^delete$', views.delete, name='collections.delete'),
url(r'^(?P<action>add|remove)$', views.collection_alter,

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

@ -24,13 +24,12 @@ from olympia.addons.models import Addon
from olympia.addons.views import BaseFilter
from olympia.amo import messages
from olympia.amo.decorators import (
allow_mine, json_view, login_required, post_required, use_primary_db)
allow_mine, login_required, post_required, use_primary_db)
from olympia.amo.urlresolvers import reverse
from olympia.amo.utils import paginate, render, urlparams
from olympia.api.filters import OrderingAliasFilter
from olympia.api.permissions import (
AllOf, AllowReadOnlyIfPublic, AnyOf, PreventActionPermission)
from olympia.legacy_api.utils import addon_to_dict
from olympia.translations.query import order_by_translation
from olympia.users.decorators import process_user_id
from olympia.users.models import UserProfile
@ -194,24 +193,6 @@ def collection_detail(request, user_id, slug):
'user_perms': user_perms})
@json_view(has_trans=True)
@allow_mine
@process_user_id
@non_atomic_requests
def collection_detail_json(request, user_id, slug):
collection = get_collection(request, user_id, slug)
if not (collection.listed or acl.check_collection_ownership(
request, collection)):
raise PermissionDenied
# We evaluate the QuerySet with `list` to work around bug 866454.
addons_dict = [addon_to_dict(a) for a in list(collection.addons.valid())]
return {
'name': collection.name,
'url': collection.get_abs_url(),
'addons': addons_dict
}
def get_notes(collection, raw=False):
# This might hurt in a big collection with lots of notes.
# It's a generator so we don't evaluate anything by default.

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

@ -1,3 +0,0 @@
MIN_VERSION = 1.0
CURRENT_VERSION = 1.5
MAX_VERSION = CURRENT_VERSION

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

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

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
{% include 'legacy_api/includes/addon.xml' %}

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

@ -1,27 +0,0 @@
{% for c in compat %}
<addon_compatibility
hosted="{{ c.is_hosted()|json }}"
{% if c.addon_id %}id="{{ c.addon_id }}"{% endif %}>
<guid>{{ c.guid }}</guid>
<name>{{ c.name }}</name>
<version_ranges>
{% for vr in c.collapsed_ranges() %}
<version_range type="{{ vr.type }}">
<min_version>{{ vr.min }}</min_version>
<max_version>{{ vr.max }}</max_version>
<compatible_applications>
{% for app in vr.apps %}
<application>
<name>{{ app.app.pretty }}</name>
<application_id>{{ app.app.id }}</application_id>
<min_version>{{ app.min }}</min_version>
<max_version>{{ app.max }}</max_version>
<appID>{{ app.app.guid }}</appID>
</application>
{% endfor %}
</compatible_applications>
</version_range>
{% endfor %}
</version_ranges>
</addon_compatibility>
{% endfor %}

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

@ -1,130 +0,0 @@
{% set new_api = (api_version >= 1.5) %}
{% if addon.compat_version %}
{% set version = addon.compat_version %}
{% else %}
{% set version = addon.current_version %}
{% endif %}
<addon id="{{ addon.id }}">
<name>{{ addon.name }}</name>
<type id="{{ addon.type }}">{{ amo.ADDON_TYPE[addon.type] }}</type>
<guid>{{ addon.guid }}</guid>
<slug>{{ addon.slug }}</slug>
<version>{{ version.version }}</version>
<status id="{{ addon.status }}">{{ addon.STATUS_CHOICES[addon.status] }}</status>
<authors>
{% if not addon.is_persona() %}
{% for author in addon.listed_authors -%}
{%- if new_api -%}
<author id="{{ author.id }}">
<name>{{ author.name }}</name>
<link>{{ author.get_url_path()|absolutify|urlparams(src='api') }}</link>
</author>
{%- else -%}
<author>{{ author.name }}</author>
{%- endif -%}
{%- endfor %}
{% endif %}
</authors>
<summary>{%- if new_api -%}{{ addon.summary }}{% else %}{{ addon.summary|clean(true) }}{% endif -%}</summary>
<description>{%- if new_api %}{{ addon.description }}{% else -%}{{ addon.description|clean(true) }}{% endif %}</description>
{%- if version and version.license -%}
<license>
<name>{{ version.license.name }}</name>
<url>
{%- if not version.license.url -%}
{{ version.license_url()|absolutify }}
{%- else -%}
{{ version.license.url|absolutify }}
{%- endif -%}
</url>
</license>
{%- endif -%}
{# The 32px icon must be the last icon specified. #}
<icon size="64">{{ addon.get_icon_url(64, use_default=False) }}</icon>
<icon size="32">{{ addon.get_icon_url(32, use_default=False) }}</icon>
<compatible_applications>
{%- if version -%}
{%- for app in version.compatible_apps.values() %}
{%- if amo.APP_IDS.get(app.application) -%}
<application>
<name>{{ amo.APP_IDS[app.application].pretty }}</name>
<application_id>{{ app.application }}</application_id>
<min_version>{{ app.min }}</min_version>
<max_version>{{ app.max }}</max_version>
<appID>{{ amo.APP_IDS[app.application].guid }}</appID>
</application>
{%- endif -%}
{%- endfor -%}
{%- endif -%}
</compatible_applications>
<all_compatible_os>
{%- if version -%}
{% for os in version.supported_platforms -%}
<os>{{ os.api_name }}</os>
{%- endfor -%}
{%- endif -%}
</all_compatible_os>
<eula>{%- if new_api %}{{ addon.eula }}{% else %}{{ addon.eula|clean(true) }}{% endif -%}</eula>
{%- if new_api -%}
<previews>
{%- for preview in addon.current_previews -%}
<preview position="{{ preview.position|int }}">
<full type="image/png"{% with sizes=preview.image_size -%}{% if sizes %} width="{{ sizes[0] }}" height="{{ sizes[1] }}"{% endif %}{% endwith -%}>
{{ preview.image_url|urlparams(src='api') }}
</full>
<thumbnail type="image/png"{% with sizes=preview.thumbnail_size -%}{% if sizes %} width="{{ sizes[0] }}" height="{{ sizes[1] }}"{% endif %}{% endwith -%}>
{{ preview.thumbnail_url|urlparams(src='api') }}
</thumbnail>
{%- if preview.caption -%}
<caption>{{ preview.caption }}</caption>
{%- endif -%}
</preview>
{%- endfor -%}
</previews>
{%- else -%}
<thumbnail>{{ addon.thumbnail_url }}</thumbnail>
{%- endif -%}
<rating>{{ addon.average_rating|round|int }}</rating>
<learnmore>{{ addon.get_url_path()|absolutify|urlparams(src='api') }}</learnmore>
{%- if version -%}
{%- for file in version.all_files -%}
<install hash="{{ file.hash }}"
os="{{ amo.PLATFORMS[file.platform].api_name }}"
size="{{ file.size }}">{{ file.get_url_path('api') }}</install>
{% endfor -%}
{%- endif -%}
{%- if new_api %}
{%- if addon.contributions -%}
<contribution_data>
<meet_developers>
{{ addon.contributions }}
</meet_developers>
</contribution_data>
{%- endif -%}
<developer_comments>{{ addon.developer_comments }}</developer_comments>
<reviews num="{{ addon.total_ratings }}">
{{ addon.ratings_url|absolutify|urlparams(src='api') }}
</reviews>
<total_downloads>{{ addon.total_downloads }}</total_downloads>
<weekly_downloads>{{ addon.weekly_downloads }}</weekly_downloads>
<daily_users>{{ addon.average_daily_users }}</daily_users>
<created epoch="{{ addon.created|epoch }}">
{{ addon.created|isotime }}
</created>
<last_updated epoch="{{ addon.last_updated|epoch }}">
{{ addon.last_updated|isotime }}
</last_updated>
<homepage>{{ addon.homepage }}</homepage>
<support>{{ addon.support_url }}</support>
<featured>{{ addon.is_featured(request.APP, request.LANG)|int }}</featured>
{# Only kept for backwards compatibility of the API #}
<performance>
</performance>
{% if show_localepicker -%}
<strings><![CDATA[
{{ addon.get_localepicker() }}
]]></strings>
{% endif %}
{%- endif -%}
</addon>

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

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<addons>
{% for addon in addons %}
{% include 'legacy_api/includes/addon.xml' %}
{% endfor %}
</addons>

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

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
{# Compatibility across both Piston and DRF on error messages/vars. #}
{% if detail %}
<error>{{_(detail)}}</error>
{% else %}
{% if error_level %}
<error>{{_(msg)}}</error>
{% else %}
<msg>{{_(msg)}}</msg>
{% endif %}
{% endif %}

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

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<searchresults total_results="{{ total }}">
{% if addons_xml %}
{% for xml in addons_xml %}
{{ xml|xssafe }}
{% endfor %}
{% else %}
{% for addon in results -%}
{% include 'legacy_api/includes/addon.xml' -%}
{% endfor %}
{% endif %}
{% if compat %}{% include 'legacy_api/compat.xml' %}{% endif %}
</searchresults>

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

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

@ -1,13 +0,0 @@
from olympia import amo
from olympia.amo.tests import TestCase, addon_factory, version_factory
from olympia.legacy_api.utils import find_compatible_version
class TestCompatibleVersion(TestCase):
def test_compatible_version(self):
addon = addon_factory()
version = version_factory(addon=addon, version='99')
version_factory(
addon=addon, version='100',
channel=amo.RELEASE_CHANNEL_UNLISTED)
assert find_compatible_version(addon, amo.FIREFOX.id) == version

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -1,74 +0,0 @@
from django.conf.urls import include, url
from django.db.transaction import non_atomic_requests
from olympia.addons.urls import ADDON_ID
from olympia.legacy_api import views
# Wrap class views in a lambda call so we get an fresh instance of the class
# for thread-safety.
@non_atomic_requests
def api_view(cls):
return lambda *args, **kw: cls()(*args, **kw)
def build_urls(base, appendages):
"""
Many of our urls build off each other:
e.g.
/search/:query
/search/:query/:type
.
.
/search/:query/:type/:limit/:platform/:version
/search/:query/:type/:limit/:platform/:version/:compatMode
"""
urls = [base]
for i in range(len(appendages)):
urls.append(base + ''.join(appendages[:i + 1]))
return urls
base_search_regexp = r'search/(?P<query>[^/]+)'
appendages = [
# Regular expressions that we use in our urls.
r'/(?P<addon_type>[^/]*)',
r'/(?P<limit>\d*)',
r'/(?P<platform>\w*)',
r'/(?P<version>[^/]*)',
r'(?:/(?P<compat_mode>(?:strict|normal|ignore)))?',
]
search_regexps = build_urls(base_search_regexp, appendages)
appendages.insert(0, r'/(?P<list_type>[^/]+)')
list_regexps = build_urls(r'list', appendages)
legacy_api_patterns = [
# Addon_details
url(r'addon/%s$' % ADDON_ID, api_view(views.AddonDetailView),
name='legacy_api.addon_detail'),
url(r'^get_language_packs$', api_view(views.LanguageView),
name='legacy_api.language'),
]
for regexp in search_regexps:
legacy_api_patterns.append(
url(regexp + r'/?$', api_view(views.SearchView),
name='legacy_api.search'))
for regexp in list_regexps:
legacy_api_patterns.append(
url(regexp + r'/?$', api_view(views.ListView),
name='legacy_api.list'))
urlpatterns = [
# Redirect api requests without versions
url(r'^((?:addon|search|list)/.*)$', views.redirect_view),
# Endpoints.
url(r'^1.5/search_suggestions/', views.search_suggestions),
url(r'^(?P<api_version>\d+|\d+.\d+)/search/guid:(?P<guids>.*)',
views.guid_search),
url(r'^(?P<api_version>\d+|\d+.\d+)/', include(legacy_api_patterns)),
]

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

@ -1,221 +0,0 @@
from django.conf import settings
from django.core.cache import cache
from django.utils.html import strip_tags
import six
import olympia.core.logger
from olympia import amo
from olympia.amo.templatetags.jinja_helpers import absolutify
from olympia.amo.urlresolvers import reverse
from olympia.amo.utils import cache_ns_key, epoch, urlparams
from olympia.versions.compare import version_int
from olympia.versions.models import Version
log = olympia.core.logger.getLogger('z.api')
def addon_to_dict(addon, disco=False, src='api'):
"""
Renders an addon into a dict for the legacy API.
"""
def url(u, **kwargs):
return settings.SITE_URL + urlparams(u, **kwargs)
v = addon.current_version
if disco:
learnmore = settings.SERVICES_URL + reverse('discovery.addons.detail',
args=[addon.slug])
learnmore = urlparams(learnmore, src='discovery-personalrec')
else:
learnmore = url(addon.get_url_path(), src=src)
d = {
'id': addon.id,
'name': six.text_type(addon.name) if addon.name else None,
'guid': addon.guid,
'status': amo.STATUS_CHOICES_API[addon.status],
'type': amo.ADDON_SLUGS_UPDATE[addon.type],
'authors': [{'id': a.id, 'name': six.text_type(a.name),
'link': absolutify(a.get_url_path(src=src))}
for a in addon.listed_authors],
'summary': (
strip_tags(six.text_type(addon.summary)) if addon.summary
else None),
'description': strip_tags(six.text_type(addon.description)),
'icon': addon.get_icon_url(32),
'learnmore': learnmore,
'reviews': url(addon.ratings_url),
'total_dls': addon.total_downloads,
'weekly_dls': addon.weekly_downloads,
'adu': addon.average_daily_users,
'created': epoch(addon.created),
'last_updated': epoch(addon.last_updated),
'homepage': six.text_type(addon.homepage) if addon.homepage else None,
'support': (
six.text_type(addon.support_url) if addon.support_url else None),
}
if addon.is_persona():
d['theme'] = addon.persona.theme_data
if v:
d['version'] = v.version
d['platforms'] = [six.text_type(a.name) for a in v.supported_platforms]
d['compatible_apps'] = [{
six.text_type(amo.APP_IDS[appver.application].pretty): {
'min': six.text_type(appver.min) if appver else (
amo.D2C_MIN_VERSIONS.get(app.id, '1.0')),
'max': (
six.text_type(appver.max) if appver
else amo.FAKE_MAX_VERSION),
}} for app, appver in v.compatible_apps.items() if appver]
if addon.eula:
d['eula'] = six.text_type(addon.eula)
if addon.developer_comments:
d['dev_comments'] = six.text_type(addon.developer_comments)
if addon.contributions:
d['contribution'] = {
'meet_developers': addon.contributions,
}
if addon.type == amo.ADDON_PERSONA:
d['previews'] = [addon.persona.preview_url]
else:
def preview_as_dict(preview, src):
d = {'full': urlparams(preview.image_url, src=src),
'thumbnail': urlparams(preview.thumbnail_url, src=src),
'caption': six.text_type(preview.caption)}
return d
d['previews'] = [
preview_as_dict(p, src) for p in addon.current_previews]
return d
def find_compatible_version(addon, app_id, app_version=None, platform=None,
compat_mode='strict'):
"""Returns the newest compatible version (ordered by version id desc)
for the given addon."""
if not app_id:
return None
if platform:
# We include platform_id=1 always in the SQL so we skip it here.
platform = platform.lower()
if platform != 'all' and platform in amo.PLATFORM_DICT:
platform = amo.PLATFORM_DICT[platform].id
else:
platform = None
log.debug(u'Checking compatibility for add-on ID:%s, APP:%s, V:%s, '
u'OS:%s, Mode:%s' % (addon.id, app_id, app_version, platform,
compat_mode))
valid_file_statuses = ','.join(map(str, addon.valid_file_statuses))
data = {
'id': addon.id,
'app_id': app_id,
'platform': platform,
'valid_file_statuses': valid_file_statuses,
'channel': amo.RELEASE_CHANNEL_LISTED,
}
if app_version:
data.update(version_int=version_int(app_version))
else:
# We can't perform the search queries for strict or normal without
# an app version.
compat_mode = 'ignore'
ns_key = cache_ns_key('d2c-versions:%s' % addon.id)
cache_key = '%s:%s:%s:%s:%s' % (ns_key, app_id, app_version, platform,
compat_mode)
version_id = cache.get(cache_key)
if version_id is not None:
log.debug(u'Found compatible version in cache: %s => %s' % (
cache_key, version_id))
if version_id == 0:
return None
else:
try:
return Version.objects.get(pk=version_id)
except Version.DoesNotExist:
pass
raw_sql = ["""
SELECT versions.*
FROM versions
INNER JOIN addons
ON addons.id = versions.addon_id AND addons.id = %(id)s
INNER JOIN applications_versions
ON applications_versions.version_id = versions.id
INNER JOIN appversions appmin
ON appmin.id = applications_versions.min
AND appmin.application_id = %(app_id)s
INNER JOIN appversions appmax
ON appmax.id = applications_versions.max
AND appmax.application_id = %(app_id)s
INNER JOIN files
ON files.version_id = versions.id AND
(files.platform_id = 1"""]
if platform:
raw_sql.append(' OR files.platform_id = %(platform)s')
raw_sql.append(') WHERE files.status IN (%(valid_file_statuses)s) ')
raw_sql.append(' AND versions.channel = %(channel)s ')
if app_version:
raw_sql.append('AND appmin.version_int <= %(version_int)s ')
if compat_mode == 'ignore':
pass # No further SQL modification required.
elif compat_mode == 'normal':
raw_sql.append("""AND
CASE WHEN files.strict_compatibility = 1 OR
files.binary_components = 1
THEN appmax.version_int >= %(version_int)s ELSE 1 END
""")
# Filter out versions that don't have the minimum maxVersion
# requirement to qualify for default-to-compatible.
d2c_max = amo.D2C_MIN_VERSIONS.get(app_id)
if d2c_max:
data['d2c_max_version'] = version_int(d2c_max)
raw_sql.append(
"AND appmax.version_int >= %(d2c_max_version)s ")
# Filter out versions found in compat overrides
raw_sql.append("""AND
NOT versions.id IN (
SELECT version_id FROM incompatible_versions
WHERE app_id=%(app_id)s AND
(min_app_version='0' AND
max_app_version_int >= %(version_int)s) OR
(min_app_version_int <= %(version_int)s AND
max_app_version='*') OR
(min_app_version_int <= %(version_int)s AND
max_app_version_int >= %(version_int)s)) """)
else: # Not defined or 'strict'.
raw_sql.append('AND appmax.version_int >= %(version_int)s ')
raw_sql.append('ORDER BY versions.id DESC LIMIT 1;')
version = list(Version.objects.raw(''.join(raw_sql) % data))
if version:
version = version[0]
version_id = version.id
else:
version = None
version_id = 0
log.debug(u'Caching compat version %s => %s' % (cache_key, version_id))
cache.set(cache_key, version_id, None)
return version

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

@ -1,493 +0,0 @@
import hashlib
import itertools
import json
import random
from datetime import date, timedelta
from django.core.cache import cache
from django.db.transaction import non_atomic_requests
from django.http import HttpResponse, HttpResponsePermanentRedirect
from django.template import engines
from django.utils.decorators import method_decorator
from django.utils.encoding import force_bytes
from django.utils.translation import get_language, ugettext, ugettext_lazy as _
import six
import waffle
from six.moves.urllib_parse import quote
import olympia.core.logger
from olympia import amo, legacy_api
from olympia.addons.models import Addon, CompatOverride
from olympia.amo.decorators import allow_cross_site_request, json_view
from olympia.amo.models import manual_order
from olympia.amo.urlresolvers import get_url_prefix
from olympia.amo.utils import AMOJSONEncoder
from olympia.legacy_api.utils import addon_to_dict, find_compatible_version
from olympia.search.views import AddonSuggestionsAjax, PersonaSuggestionsAjax
from olympia.versions.compare import version_int
ERROR = 'error'
OUT_OF_DATE = _(
u'The API version, {0:.1f}, you are using is not valid. '
u'Please upgrade to the current version {1:.1f} API.')
xml_env = engines['jinja2'].env.overlay()
old_finalize = xml_env.finalize
xml_env.finalize = lambda x: amo.templatetags.jinja_helpers.strip_controls(
old_finalize(x))
# Hard limit of 30. The buffer is to try for locale-specific add-ons.
MAX_LIMIT, BUFFER = 30, 10
# "New" is arbitrarily defined as 10 days old.
NEW_DAYS = 10
log = olympia.core.logger.getLogger('z.api')
def partition(seq, key):
"""Group a sequence based into buckets by key(x)."""
groups = itertools.groupby(sorted(seq, key=key), key=key)
return ((k, list(v)) for k, v in groups)
def render_xml_to_string(request, template, context=None):
if context is None:
context = {}
for processor in engines['jinja2'].context_processors:
context.update(processor(request))
template = xml_env.get_template(template)
return template.render(context)
@non_atomic_requests
def render_xml(request, template, context=None, **kwargs):
"""Safely renders xml, stripping out nasty control characters."""
if context is None:
context = {}
rendered = render_xml_to_string(request, template, context)
if 'content_type' not in kwargs:
kwargs['content_type'] = 'text/xml'
return HttpResponse(rendered, **kwargs)
@non_atomic_requests
def handler403(request):
context = {'error_level': ERROR, 'msg': 'Not allowed'}
return render_xml(request, 'legacy_api/message.xml', context, status=403)
@non_atomic_requests
def handler404(request):
context = {'error_level': ERROR, 'msg': 'Not Found'}
return render_xml(request, 'legacy_api/message.xml', context, status=404)
@non_atomic_requests
def handler500(request):
context = {'error_level': ERROR, 'msg': 'Server Error'}
return render_xml(request, 'legacy_api/message.xml', context, status=500)
def validate_api_version(version):
"""
We want to be able to deprecate old versions of the API, therefore we check
for a minimum API version before continuing.
"""
if float(version) < legacy_api.MIN_VERSION:
return False
if float(version) > legacy_api.MAX_VERSION:
return False
return True
def addon_filter(addons, addon_type, limit, app, platform, version,
compat_mode='strict', shuffle=True):
"""
Filter addons by type, application, app version, and platform.
Add-ons that support the current locale will be sorted to front of list.
Shuffling will be applied to the add-ons supporting the locale and the
others separately.
Doing this in the database takes too long, so we do it in code and wrap
it in generous caching.
"""
APP = app
if addon_type.upper() != 'ALL':
try:
addon_type = int(addon_type)
if addon_type:
addons = [a for a in addons if a.type == addon_type]
except ValueError:
# `addon_type` is ALL or a type id. Otherwise we ignore it.
pass
# Take out personas since they don't have versions.
groups = dict(partition(addons,
lambda x: x.type == amo.ADDON_PERSONA))
personas, addons = groups.get(True, []), groups.get(False, [])
platform = platform.lower()
if platform != 'all' and platform in amo.PLATFORM_DICT:
def f(ps):
return pid in ps or amo.PLATFORM_ALL in ps
pid = amo.PLATFORM_DICT[platform]
addons = [a for a in addons
if f(a.current_version.supported_platforms)]
if version is not None:
vint = version_int(version)
def f_strict(app):
return app.min.version_int <= vint <= app.max.version_int
def f_ignore(app):
return app.min.version_int <= vint
xs = [(a, a.compatible_apps) for a in addons]
# Iterate over addons, checking compatibility depending on compat_mode.
addons = []
for addon, apps in xs:
app = apps.get(APP)
if compat_mode == 'strict':
if app and f_strict(app):
addons.append(addon)
elif compat_mode == 'ignore':
if app and f_ignore(app):
addons.append(addon)
elif compat_mode == 'normal':
# This does a db hit but it's cached. This handles the cases
# for strict opt-in, binary components, and compat overrides.
v = find_compatible_version(addon, APP.id, version, platform,
compat_mode)
if v: # There's a compatible version.
addons.append(addon)
# Put personas back in.
addons.extend(personas)
# We prefer add-ons that support the current locale.
lang = get_language()
def partitioner(x):
return x.description is not None and (x.description.locale == lang)
groups = dict(partition(addons, partitioner))
good, others = groups.get(True, []), groups.get(False, [])
if shuffle:
random.shuffle(good)
random.shuffle(others)
# If limit=0, we return all addons with `good` coming before `others`.
# Otherwise pad `good` if less than the limit and return the limit.
if limit > 0:
if len(good) < limit:
good.extend(others[:limit - len(good)])
return good[:limit]
else:
good.extend(others)
return good
class APIView(object):
"""
Base view class for all API views.
"""
@method_decorator(non_atomic_requests)
def __call__(self, request, api_version, *args, **kwargs):
self.version = float(api_version)
self.format = request.GET.get('format', 'xml')
self.content_type = ('text/xml' if self.format == 'xml'
else 'application/json')
self.request = request
if not validate_api_version(api_version):
msg = OUT_OF_DATE.format(self.version, legacy_api.CURRENT_VERSION)
return self.render_msg(msg, ERROR, status=403,
content_type=self.content_type)
return self.process_request(*args, **kwargs)
def render_msg(self, msg, error_level=None, *args, **kwargs):
"""
Renders a simple message.
"""
if self.format == 'xml':
return render_xml(
self.request, 'legacy_api/message.xml',
{'error_level': error_level, 'msg': msg}, *args, **kwargs)
else:
return HttpResponse(json.dumps({'msg': _(msg)}), *args, **kwargs)
def render(self, template, context):
context['api_version'] = self.version
context['api'] = legacy_api
if self.format == 'xml':
return render_xml(self.request, template, context,
content_type=self.content_type)
else:
return HttpResponse(self.render_json(context),
content_type=self.content_type)
def render_json(self, context):
return json.dumps({'msg': ugettext('Not implemented yet.')})
class AddonDetailView(APIView):
@allow_cross_site_request
def process_request(self, addon_id):
try:
# Nominated or public add-ons should be viewable using the legacy
# API detail endpoint.
addon = Addon.objects.valid().id_or_slug(addon_id).get()
except Addon.DoesNotExist:
# Add-on is either inexistent or not public/nominated.
return self.render_msg(
'Add-on not found!', ERROR, status=404,
content_type=self.content_type
)
return self.render_addon(addon)
def render_addon(self, addon):
return self.render('legacy_api/addon_detail.xml', {'addon': addon})
def render_json(self, context):
return json.dumps(addon_to_dict(context['addon']), cls=AMOJSONEncoder)
@non_atomic_requests
def guid_search(request, api_version, guids):
lang = request.LANG
app_id = request.APP.id
api_version = float(api_version)
def guid_search_cache_key(guid):
key = 'guid_search:%s:%s:%s:%s' % (api_version, lang, app_id, guid)
return hashlib.sha256(force_bytes(key)).hexdigest()
guids = [guid.strip() for guid in guids.split(',')] if guids else []
addons_xml = cache.get_many(
[guid_search_cache_key(guid) for guid in guids])
dirty_keys = set()
for guid in guids:
key = guid_search_cache_key(guid)
if key not in addons_xml:
dirty_keys.add(key)
try:
# Only search through public (and not disabled) add-ons.
addon = Addon.objects.public().get(guid=guid)
except Addon.DoesNotExist:
addons_xml[key] = ''
else:
addon_xml = render_xml_to_string(
request, 'legacy_api/includes/addon.xml', {
'addon': addon,
'api_version': api_version,
'api': legacy_api
})
addons_xml[key] = addon_xml
if dirty_keys:
cache.set_many({k: v for k, v in six.iteritems(addons_xml)
if k in dirty_keys})
compat = (CompatOverride.objects.filter(guid__in=guids)
.transform(CompatOverride.transformer))
addons_xml = [v for v in addons_xml.values() if v]
return render_xml(request, 'legacy_api/search.xml', {
'addons_xml': addons_xml,
'total': len(addons_xml),
'compat': compat,
'api_version': api_version, 'api': legacy_api
})
class SearchView(APIView):
def process_request(self, query, addon_type='ALL', limit=10,
platform='ALL', version=None, compat_mode='strict'):
"""
Query the search backend and serve up the XML.
"""
limit = min(MAX_LIMIT, int(limit))
app_id = self.request.APP.id
# We currently filter for status=PUBLIC for all versions. If
# that changes, the contract for API version 1.5 requires
# that we continue filtering for it there.
filters = {
'app': app_id,
'status': amo.STATUS_PUBLIC,
'is_experimental': False,
'is_disabled': False,
'current_version__exists': True,
}
params = {'version': version, 'platform': None}
# Specific case for Personas (bug 990768): if we search providing the
# Persona addon type (9), don't filter on the platform as Personas
# don't have compatible platforms to filter on.
if addon_type != '9':
params['platform'] = platform
# Type filters.
if addon_type:
try:
atype = int(addon_type)
if atype in amo.ADDON_SEARCH_TYPES:
filters['type'] = atype
except ValueError:
atype = amo.ADDON_SEARCH_SLUGS.get(addon_type.lower())
if atype:
filters['type'] = atype
if 'type' not in filters:
# Filter by ALL types, which is really all types except for apps.
filters['type__in'] = list(amo.ADDON_SEARCH_TYPES)
qs = (
Addon.search()
.filter(**filters)
.filter_query_string(query)
[:limit])
results = []
total = qs.count()
for addon in qs:
compat_version = find_compatible_version(
addon, app_id, params['version'], params['platform'],
compat_mode)
# Specific case for Personas (bug 990768): if we search
# providing the Persona addon type (9), then don't look for a
# compatible version.
if compat_version or addon_type == '9':
addon.compat_version = compat_version
results.append(addon)
if len(results) == limit:
break
else:
# We're excluding this addon because there are no
# compatible versions. Decrement the total.
total -= 1
return self.render('legacy_api/search.xml', {
'results': results,
'total': total,
# For caching
'version': version,
'compat_mode': compat_mode,
})
@json_view
@non_atomic_requests
def search_suggestions(request):
if waffle.sample_is_active('autosuggest-throttle'):
return HttpResponse(status=503)
cat = request.GET.get('cat', 'all')
suggesterClass = {
'all': AddonSuggestionsAjax,
'themes': PersonaSuggestionsAjax,
}.get(cat, AddonSuggestionsAjax)
items = suggesterClass(request, ratings=True).items
for s in items:
s['rating'] = float(s['rating'])
return {'suggestions': items}
class ListView(APIView):
def process_request(self, list_type='recommended', addon_type='ALL',
limit=10, platform='ALL', version=None,
compat_mode='strict'):
"""
Find a list of new or featured add-ons. Filtering is done in Python
to avoid heavy queries.
"""
limit = min(MAX_LIMIT, int(limit))
APP, platform = self.request.APP, platform.lower()
qs = Addon.objects.listed(APP)
shuffle = True
if list_type in ('by_adu', 'featured'):
qs = qs.exclude(type=amo.ADDON_PERSONA)
if list_type == 'newest':
new = date.today() - timedelta(days=NEW_DAYS)
addons = (qs.filter(created__gte=new)
.order_by('-created'))[:limit + BUFFER]
elif list_type == 'by_adu':
addons = qs.order_by('-average_daily_users')[:limit + BUFFER]
shuffle = False # By_adu is an ordered list.
elif list_type == 'hotness':
# Filter to type=1 so we hit visible_idx. Only extensions have a
# hotness index right now so this is not incorrect.
addons = (qs.filter(type=amo.ADDON_EXTENSION)
.order_by('-hotness'))[:limit + BUFFER]
shuffle = False
else:
ids = Addon.featured_random(APP, self.request.LANG)
addons = manual_order(qs, ids[:limit + BUFFER], 'addons.id')
shuffle = False
args = (addon_type, limit, APP, platform, version, compat_mode,
shuffle)
return self.render('legacy_api/list.xml',
{'addons': addon_filter(addons.all(), *args)})
def render_json(self, context):
return json.dumps([addon_to_dict(a) for a in context['addons']],
cls=AMOJSONEncoder)
class LanguageView(APIView):
def process_request(self):
addons = (Addon.objects.public()
.filter(type=amo.ADDON_LPAPP,
appsupport__app=self.request.APP.id)
.order_by('pk'))
return self.render('legacy_api/list.xml', {'addons': addons,
'show_localepicker': True})
# pylint: disable-msg=W0613
@non_atomic_requests
def redirect_view(request, url):
"""
Redirect all requests that come here to an API call with a view parameter.
"""
dest = '/api/%.1f/%s' % (legacy_api.CURRENT_VERSION,
quote(url.encode('utf-8')))
dest = get_url_prefix().fix(dest)
return HttpResponsePermanentRedirect(dest)

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

@ -1,14 +1,18 @@
# -*- coding: utf-8 -*-
import itertools
import random
from django.template.loader import render_to_string
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import get_language
import jinja2
import six
from olympia import amo
from olympia.addons.models import Addon
from olympia.bandwagon.models import (
Collection, MonthlyPick as MonthlyPickModel)
from olympia.legacy_api.views import addon_filter
from olympia.versions.compare import version_int
from olympia.lib.cache import cache_get_or_set, make_key
@ -17,6 +21,84 @@ from olympia.lib.cache import cache_get_or_set, make_key
registry = {}
# Temporarily here as part of the legacy-api removal
# Simplified a bit by stuff that isn't used
def addon_filter(addons, addon_type, limit, app, platform, version,
compat_mode='strict'):
"""
Filter addons by type, application, app version, and platform.
Add-ons that support the current locale will be sorted to front of list.
Shuffling will be applied to the add-ons supporting the locale and the
others separately.
Doing this in the database takes too long, so we do it in code and wrap
it in generous caching.
"""
APP = app
def partition(seq, key):
"""Group a sequence based into buckets by key(x)."""
groups = itertools.groupby(sorted(seq, key=key), key=key)
return ((k, list(v)) for k, v in groups)
# Take out personas since they don't have versions.
groups = dict(partition(addons,
lambda x: x.type == amo.ADDON_PERSONA))
personas, addons = groups.get(True, []), groups.get(False, [])
platform = platform.lower()
if platform != 'all' and platform in amo.PLATFORM_DICT:
def f(ps):
return pid in ps or amo.PLATFORM_ALL in ps
pid = amo.PLATFORM_DICT[platform]
addons = [a for a in addons
if f(a.current_version.supported_platforms)]
if version is not None:
vint = version_int(version)
def f_strict(app):
return app.min.version_int <= vint <= app.max.version_int
def f_ignore(app):
return app.min.version_int <= vint
xs = [(a, a.compatible_apps) for a in addons]
# Iterate over addons, checking compatibility depending on compat_mode.
addons = []
for addon, apps in xs:
app = apps.get(APP)
if compat_mode == 'ignore':
if app and f_ignore(app):
addons.append(addon)
# Put personas back in.
addons.extend(personas)
# We prefer add-ons that support the current locale.
lang = get_language()
def partitioner(x):
return x.description is not None and (x.description.locale == lang)
groups = dict(partition(addons, partitioner))
good, others = groups.get(True, []), groups.get(False, [])
random.shuffle(good)
random.shuffle(others)
# If limit=0, we return all addons with `good` coming before `others`.
# Otherwise pad `good` if less than the limit and return the limit.
if limit > 0:
if len(good) < limit:
good.extend(others[:limit - len(good)])
return good[:limit]
else:
good.extend(others)
return good
class PromoModuleMeta(type):
"""Adds new PromoModules to the module registry."""
@ -41,9 +123,7 @@ class PromoModule(six.with_metaclass(PromoModuleMeta, object)):
self.request = request
self.platform = platform
self.version = version
self.compat_mode = 'strict'
if version_int(self.version) >= version_int('10.0'):
self.compat_mode = 'ignore'
self.compat_mode = 'ignore'
def render(self):
raise NotImplementedError

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

@ -457,7 +457,6 @@ INSTALLED_APPS = (
'olympia.devhub',
'olympia.discovery',
'olympia.files',
'olympia.legacy_api',
'olympia.legacy_discovery',
'olympia.lib.es',
'olympia.lib.akismet',

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

@ -701,19 +701,13 @@ class TestRankingScenarios(ESTestCase):
))
def test_scenario_grapple(self):
"""Making sure this scenario works via the API,
see `legacy_api.SearchTest` for various examples.
"""
"""Making sure this scenario works via the API"""
self._check_scenario('grapple', (
['GrApple Yummy', 0.69091946],
))
def test_scenario_delicious(self):
"""Making sure this scenario works via the API,
see `legacy_api.SearchTest` for various examples.
"""
"""Making sure this scenario works via the API"""
self._check_scenario('delicious', (
['Delicious Bookmarks', 0.8113203],
))

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

@ -81,9 +81,6 @@ urlpatterns = [
cache_page(60 * 60 * 24 * 365)(JavaScriptCatalog.as_view()),
{'domain': 'djangojs', 'packages': []}, name='jsi18n'),
# SAMO (Legacy API)
url(r'^api/', include('olympia.legacy_api.urls')),
# API v3+.
url(r'^api/', include('olympia.api.urls')),