Remove Legacy API (#10705)
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:
Родитель
6287c53fbc
Коммит
d0c435bca0
|
@ -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')),
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче