Merge branch 'master' of github.com:jbalogh/zamboni
This commit is contained in:
Коммит
b9e21846d3
|
@ -101,6 +101,17 @@ ADDON_PLUGIN = 7
|
|||
ADDON_API = 8 # not actually a type but used to identify extensions + themes
|
||||
ADDON_PERSONA = 9
|
||||
|
||||
# Singular
|
||||
ADDON_TYPE = {
|
||||
ADDON_EXTENSION: _('Extension'),
|
||||
ADDON_THEME: _('Theme'),
|
||||
ADDON_DICT: _('Dictionary'),
|
||||
ADDON_SEARCH: _('Search Engine'),
|
||||
ADDON_PLUGIN: _('Plugin'),
|
||||
ADDON_PERSONA: _('Persona'),
|
||||
}
|
||||
|
||||
# Plural
|
||||
ADDON_TYPES = {
|
||||
ADDON_EXTENSION: _('Extensions'),
|
||||
ADDON_THEME: _('Themes'),
|
||||
|
|
|
@ -40,4 +40,4 @@ def global_settings(request):
|
|||
link['href'] = '/users/logout?to=' + urlquote(request.path)
|
||||
account_links.append(link)
|
||||
|
||||
return {'account_links': account_links}
|
||||
return {'account_links': account_links, 'settings': settings}
|
||||
|
|
|
@ -1,4 +1,15 @@
|
|||
[
|
||||
{
|
||||
"pk": 36151,
|
||||
"model": "translations.translation",
|
||||
"fields": {
|
||||
"id": 36151,
|
||||
"locale": "en-US",
|
||||
"localized_string": "Best Addon Evar",
|
||||
"created": "2001-01-01 13:14:15"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"pk": 1,
|
||||
"model": "translations.translation",
|
||||
|
@ -57,6 +68,33 @@
|
|||
"created": "2006-08-21 23:53:24"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 55021,
|
||||
"model": "users.userprofile",
|
||||
"fields": {
|
||||
"nickname": "carlsjr",
|
||||
"sandboxshown": 1,
|
||||
"display_collections_fav": 0,
|
||||
"display_collections": 0,
|
||||
"occupation": "",
|
||||
"confirmationcode": "",
|
||||
"location": "",
|
||||
"picture_type": "",
|
||||
"averagerating": "3.11",
|
||||
"homepage": "http://www.yahoo.com/",
|
||||
"email": "dd+0cb167b90cc5ed1b6d0e4566d2c2c3a6@davedash.com",
|
||||
"notifycompat": 1,
|
||||
"firstname": "Carl",
|
||||
"deleted": 0,
|
||||
"lastname": "Yahoo!, Jr.",
|
||||
"emailhidden": 1,
|
||||
"password": "sha512$61ba34cb04c185f51ca6b5c3132bfbf5b4d6b16c329ff076039c597c0d14912f$9b461aca85bcbc326c0555c3165ff526eabdce82a49c7408bbb71121d3d9d48499c952b96dc520cb4accbca0df0cd1d17756873d08e488a9a144aaf34cb78bf1",
|
||||
"resetcode": "",
|
||||
"created": "2007-03-05 13:09:56",
|
||||
"modified": "2007-07-21 11:45:47",
|
||||
"notifyevents": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 3615,
|
||||
"model": "addons.addon",
|
||||
|
@ -80,6 +118,7 @@
|
|||
"icon_type": "image/png",
|
||||
"status": 4,
|
||||
"description": 15002,
|
||||
"summary": 36151,
|
||||
"site_specific": 1,
|
||||
"nomination_date": "2009-03-26 07:41:12",
|
||||
"wants_contributions": 0,
|
||||
|
@ -183,6 +222,16 @@
|
|||
"version_int": 2,
|
||||
"created": "2006-08-21 23:53:19"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 240071,
|
||||
"model": "versions.applicationsversions",
|
||||
"fields": {
|
||||
"application": 1,
|
||||
"version": 24007,
|
||||
"min": 200,
|
||||
"max": 201
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 20,
|
||||
|
@ -353,5 +402,16 @@
|
|||
"modified": "2010-01-12 17:01:41",
|
||||
"notifyevents": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 2808,
|
||||
"model": "addons.addonuser",
|
||||
"fields": {
|
||||
"position": 0,
|
||||
"listed": 1,
|
||||
"role": 5,
|
||||
"user": 55021,
|
||||
"addon": 3615
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import collections
|
||||
import math
|
||||
|
||||
from django.utils import translation
|
||||
from django.utils.translation import ugettext as _
|
||||
|
@ -105,3 +106,39 @@ def numberfmt(num, format=None):
|
|||
@jinja2.contextfunction
|
||||
def page_title(context, title):
|
||||
return "%s :: %s" % (title, _("Add-ons for {0}").format(context['APP'].pretty))
|
||||
|
||||
|
||||
# XXX: Jinja2's round is broken:
|
||||
# http://dev.pocoo.org/projects/jinja/ticket/367
|
||||
@register.filter
|
||||
def wround(value, precision=0, method='common'):
|
||||
"""Round the number to a given precision. The first
|
||||
parameter specifies the precision (default is ``0``), the
|
||||
second the rounding method:
|
||||
|
||||
- ``'common'`` rounds either up or down
|
||||
- ``'ceil'`` always rounds up
|
||||
- ``'floor'`` always rounds down
|
||||
|
||||
If you don't specify a method ``'common'`` is used.
|
||||
|
||||
.. sourcecode:: jinja
|
||||
|
||||
{{ 42.55|round }}
|
||||
-> 43
|
||||
{{ 42.55|round(1, 'floor') }}
|
||||
-> 42.5
|
||||
"""
|
||||
if not method in ('common', 'ceil', 'floor'):
|
||||
raise FilterArgumentError('method must be common, ceil or floor')
|
||||
if precision < 0:
|
||||
raise FilterArgumentError('precision must be a postive integer '
|
||||
'or zero.')
|
||||
if method == 'common':
|
||||
val = round(value, precision)
|
||||
return val if precision else int(val)
|
||||
func = getattr(math, method)
|
||||
if precision:
|
||||
return func(value * 10 * precision) / (10 * precision)
|
||||
else:
|
||||
return int(func(value))
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
MIN_VERSION = 1.0
|
||||
CURRENT_VERSION = 3.0
|
||||
MAX_VERSION = CURRENT_VERSION
|
|
@ -0,0 +1,48 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
|
||||
<addon>
|
||||
<name>{{addon.name}}</name>
|
||||
<type id="{{addon.type.id}}">{{amo.ADDON_TYPE[addon.type.id]}}</type>
|
||||
<guid>{{addon.guid}}</guid>
|
||||
<version>{{addon.current_version.version}}</version>
|
||||
<status id="{{addon.status}}">{{amo.STATUS_CHOICES[addon.status]}}</status>
|
||||
<authors>
|
||||
{% for author in addon.authors.filter(addonuser__listed=True) %}
|
||||
<author>{{author.display_name}}</author>
|
||||
{% endfor %}
|
||||
</authors>
|
||||
<summary>{{addon.summary}}</summary>
|
||||
<description>{{addon.description}}</description>
|
||||
<icon>{{addon.icon_url}}</icon>
|
||||
<compatible_applications>
|
||||
{% if addon.current_version %}
|
||||
{% for app in addon.current_version.applicationsversions_set.all() %}
|
||||
<application>
|
||||
<name>{{amo.APP_IDS[app.application_id].pretty}}</name>
|
||||
<application_id>{{app.application.id}}</application_id>
|
||||
<min_version>{{app.min}}</min_version>
|
||||
<max_version>{{app.max}}</max_version>
|
||||
</application>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</compatible_applications>
|
||||
<all_compatible_os>
|
||||
{% if addon.current_version %}
|
||||
{% for os in addon.current_version.supported_platforms %}
|
||||
<os>{{os}}</os>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</all_compatible_os>
|
||||
<eula>{{addon.eula}}</eula>
|
||||
<thumbnail>{{addon.thumbnail_url}}</thumbnail>
|
||||
<rating>{{addon.bayesian_rating|wround}}</rating>
|
||||
<learnmore>{{settings.SITE_URL+addon.get_absolute_url()+'?src=api'}}</learnmore>
|
||||
{% if addon.current_version %}
|
||||
{% for file in addon.current_version.files.all() %}
|
||||
<install hash="{{file.hash}}" os="{{file.platform.name}}">
|
||||
{{file.get_absolute_url('api')}}
|
||||
</install>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</addon>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
{% if error_level %}
|
||||
<error>{{_(msg)}}</error>
|
||||
{% else %}
|
||||
<msg>{{_(msg)}} </msg>
|
||||
{% endif %}
|
|
@ -0,0 +1,72 @@
|
|||
from test_utils import TestCase
|
||||
import api
|
||||
|
||||
|
||||
class APITest(TestCase):
|
||||
|
||||
fixtures = ['base/addons']
|
||||
|
||||
def test_redirection(self):
|
||||
"""
|
||||
Test that /api/addon is redirected to /api/LATEST_API_VERSION/addon
|
||||
"""
|
||||
response = self.client.get('/en-US/firefox/api/addon/12', follow=True)
|
||||
last_link = response.redirect_chain[-1]
|
||||
assert last_link[0].endswith('en-US/firefox/api/%.1f/addon/12' %
|
||||
api.CURRENT_VERSION)
|
||||
|
||||
def test_forbidden_api(self):
|
||||
"""
|
||||
APIs older than api.MIN_VERSION are deprecated, and we send a 403.
|
||||
We suggest people to use api.CURRENT_VERSION.
|
||||
"""
|
||||
|
||||
response = self.client.get('/en-US/firefox/api/0.9/addon/12')
|
||||
self.assertContains(response, 'The API version, %.1f, you are using '
|
||||
'is not valid. Please upgrade to the current version %.1f '
|
||||
'API.' % (0.9, api.CURRENT_VERSION), status_code=403)
|
||||
|
||||
def test_addon_detail_missing(self):
|
||||
"""
|
||||
Check missing addons.
|
||||
"""
|
||||
response = self.client.get('/en-US/firefox/api/%.1f/addon/999' %
|
||||
api.CURRENT_VERSION)
|
||||
|
||||
self.assertContains(response, 'Add-on not found!', status_code=404)
|
||||
|
||||
def test_addon_detail(self):
|
||||
"""
|
||||
Test for expected strings in the XML.
|
||||
"""
|
||||
response = self.client.get('/en-US/firefox/api/%.1f/addon/3615' %
|
||||
api.CURRENT_VERSION)
|
||||
|
||||
self.assertContains(response, "<name>Delicious Bookmarks</name>")
|
||||
self.assertContains(response, """id="1">Extension</type>""")
|
||||
self.assertContains(response,
|
||||
"""<guid>{2fa4ed95-0317-4c6a-a74c-5f3e3912c1f9}</guid>""")
|
||||
self.assertContains(response, "<version>1.0.43</version>")
|
||||
self.assertContains(response, """<status id="4">Public</status>""")
|
||||
self.assertContains(response, "<author>carlsjr</author>")
|
||||
self.assertContains(response, "<summary>Best Addon Evar</summary>")
|
||||
self.assertContains(response,
|
||||
"<description>Delicious blah blah blah</description>")
|
||||
|
||||
self.assertContains(response,
|
||||
"/en-US/firefox/images/addon_icon/3615/1256144332</icon>")
|
||||
self.assertContains(response, "<application>")
|
||||
self.assertContains(response, "<name>Firefox</name>")
|
||||
self.assertContains(response, "<application_id>1</application_id>")
|
||||
self.assertContains(response, "<min_version>1</min_version>")
|
||||
self.assertContains(response, "<max_version>2</max_version>")
|
||||
self.assertContains(response, "<os>ALL</os>")
|
||||
self.assertContains(response, "<eula>None</eula>")
|
||||
self.assertContains(response, "/img/no-preview.png</thumbnail>")
|
||||
self.assertContains(response, "<rating>3</rating>")
|
||||
self.assertContains(response, "/en-US/firefox/addon/3615/?src=api</learnmore>")
|
||||
self.assertContains(response,
|
||||
"""hash="sha256:5b5aaf7b38e332cc95d92ba759c01"""
|
||||
"c3076b53a840f6c16e01dc272eefcb29566")
|
||||
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
from django.conf.urls.defaults import patterns, url
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
# Redirect api requests without versions
|
||||
url('^((?:addon)/.*)$', views.redirect_view),
|
||||
url('^(\d+|\d+\.\d+)/addon/(\d+)$',
|
||||
lambda *args, **kwargs: views.AddonDetailView()(*args, **kwargs),
|
||||
name='api.addon_detail'),
|
||||
|
||||
)
|
|
@ -0,0 +1,91 @@
|
|||
"""
|
||||
API views
|
||||
"""
|
||||
import json
|
||||
|
||||
from django.http import HttpResponse, HttpResponsePermanentRedirect
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
import jingo
|
||||
|
||||
import amo
|
||||
import api
|
||||
from addons.models import Addon
|
||||
|
||||
ERROR = 'error'
|
||||
OUT_OF_DATE = _("The API version, {0:.1f}, you are using is not valid. "
|
||||
"Please upgrade to the current version {1:.1f} API.")
|
||||
|
||||
|
||||
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) < api.MIN_VERSION:
|
||||
return False
|
||||
|
||||
if float(version) > api.MAX_VERSION:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class APIView(object):
|
||||
"""
|
||||
Base view class for all API views.
|
||||
"""
|
||||
|
||||
def __call__(self, request, api_version, *args, **kwargs):
|
||||
|
||||
self.format = request.REQUEST.get('format', 'xml')
|
||||
self.mimetype = ('text/xml' if self.format == 'xml'
|
||||
else 'application/json')
|
||||
self.request = request
|
||||
self.version = float(api_version)
|
||||
if not validate_api_version(api_version):
|
||||
msg = OUT_OF_DATE.format(self.version, api.CURRENT_VERSION)
|
||||
return self.render_msg(msg, ERROR, status=403,
|
||||
mimetype=self.mimetype)
|
||||
|
||||
return self.process_request(request, *args, **kwargs)
|
||||
|
||||
def render_msg(self, msg, error_level=None, *args, **kwargs):
|
||||
"""
|
||||
Renders a simple message.
|
||||
"""
|
||||
|
||||
if self.format == 'xml':
|
||||
return jingo.render(self.request, 'api/message.xml',
|
||||
{'error_level': error_level, 'msg': msg}, *args, **kwargs)
|
||||
else:
|
||||
return HttpResponse(json.dumps({'msg': _(msg)}), *args, **kwargs)
|
||||
|
||||
|
||||
class AddonDetailView(APIView):
|
||||
|
||||
def process_request(self, request, addon_id):
|
||||
try:
|
||||
addon = Addon.objects.get(id=addon_id)
|
||||
except Addon.DoesNotExist:
|
||||
return self.render_msg('Add-on not found!', ERROR, status=404,
|
||||
mimetype=self.mimetype)
|
||||
|
||||
return self.render_addon(addon)
|
||||
|
||||
def render_addon(self, addon):
|
||||
if self.format == 'xml':
|
||||
return jingo.render(
|
||||
self.request, 'api/addon.xml',
|
||||
{'addon': addon, 'amo': amo}, mimetype=self.mimetype)
|
||||
else:
|
||||
pass
|
||||
# serialize me?
|
||||
|
||||
|
||||
def redirect_view(request, url):
|
||||
"""
|
||||
Redirect all requests that come here to an API call with a view parameter.
|
||||
"""
|
||||
return HttpResponsePermanentRedirect('/api/%.1f/%s' %
|
||||
(api.CURRENT_VERSION, url))
|
|
@ -11,6 +11,9 @@ from .utils import convert_version, crc32
|
|||
m_dot_n_re = re.compile(r'^\d+\.\d+$')
|
||||
SEARCH_ENGINE_APP = 99
|
||||
|
||||
class SearchError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""
|
||||
|
@ -84,7 +87,7 @@ class Client(object):
|
|||
# STATUS_DISABLED and 0 (which likely means null) are filtered from
|
||||
# search
|
||||
|
||||
sc.SetFilter('status', (0, amo.STATUS_DISABLED), True)
|
||||
sc.SetFilter('addon_status', (0, amo.STATUS_DISABLED), True)
|
||||
|
||||
# Unless we're in admin mode, or we're looking at stub entries,
|
||||
# everything must have a file.
|
||||
|
@ -109,7 +112,7 @@ class Client(object):
|
|||
else:
|
||||
# We want to boost public addons, and addons in your native
|
||||
# language.
|
||||
expr = ("@weight + IF(status=%d, 30, 0) + "
|
||||
expr = ("@weight + IF(addon_status=%d, 30, 0) + "
|
||||
"IF(locale_ord=%d, 29, 0)") % (amo.STATUS_PUBLIC,
|
||||
crc32(translation.get_language()))
|
||||
sc.SetSortMode(sphinx.SPH_SORT_EXPR, expr)
|
||||
|
@ -150,5 +153,8 @@ class Client(object):
|
|||
|
||||
result = sc.Query(term)
|
||||
|
||||
if sc.GetLastError():
|
||||
raise SearchError(sc.GetLastError())
|
||||
|
||||
if result:
|
||||
return result['matches']
|
||||
|
|
|
@ -31,14 +31,14 @@ class TestUserProfile(test.TestCase):
|
|||
|
||||
def test_resetcode_expires(self):
|
||||
"""
|
||||
For some reasone resetcode is required, and we default it to
|
||||
For some reason resetcode is required, and we default it to
|
||||
'0000-00-00 00:00' in mysql, but that doesn't fly in Django since it's
|
||||
an invalid date. If Django reads this from the db, it interprets this
|
||||
as resetcode_expires as None
|
||||
"""
|
||||
|
||||
u = UserProfile(lastname='Connor', pk=2, resetcode_expires=None,
|
||||
email='j.connor@sky.net')
|
||||
nickname='jconnor', email='j.connor@sky.net')
|
||||
u.save()
|
||||
assert u.resetcode_expires
|
||||
|
||||
|
|
2
configs
2
configs
|
@ -1 +1 @@
|
|||
Subproject commit d9ba389ed4011f745d878967c5a39cd1fa236106
|
||||
Subproject commit d743bd05d6afcff10626036e9bd0d8bd40a66f26
|
|
@ -0,0 +1,4 @@
|
|||
ALTER TABLE addons_users
|
||||
ADD UNIQUE (addon_id, user_id),
|
||||
DROP PRIMARY KEY,
|
||||
ADD COLUMN id INTEGER UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY FIRST;
|
|
@ -134,6 +134,7 @@ INSTALLED_APPS = (
|
|||
'access',
|
||||
'addons',
|
||||
'admin',
|
||||
'api',
|
||||
'applications',
|
||||
'bandwagon',
|
||||
'blocklist',
|
||||
|
@ -187,10 +188,13 @@ def JINJA_CONFIG():
|
|||
config['bytecode_cache'] = bc
|
||||
return config
|
||||
|
||||
# The host currently running the site. Only use this in code for good reason;
|
||||
# the site is designed to run on a cluster and should continue to support that
|
||||
HOSTNAME = socket.gethostname()
|
||||
|
||||
# Full base URL for your main site including protocol. No trailing slash.
|
||||
# Example: https://addons.mozilla.org
|
||||
SITE_URL = 'http://%s' % socket.gethostname()
|
||||
SITE_URL = 'http://%s' % HOSTNAME
|
||||
|
||||
# If you want to run Selenium tests, you'll need to have a server running.
|
||||
# Then give this a dictionary of settings. Something like:
|
||||
|
|
|
@ -2,10 +2,9 @@
|
|||
<div class="section">
|
||||
<div class="primary">
|
||||
<p>{% trans legalurl="http://www.mozilla.com/en-US/about/legal.html#site", ccurl="http://creativecommons.org/licenses/by-sa/3.0/" -%}
|
||||
Except where otherwise <a href="{{ legalurl }}">noted</a>, content on this site is licensed under the<br /><strong>
|
||||
<a href="{{ ccurl }}">Creative Commons Attribution Share-Alike License v3.0</a></strong> or any later version
|
||||
{%- endtrans %}
|
||||
.{# Period is outside of the translation for future use. #}
|
||||
Except where otherwise <a href="{{ legalurl }}">noted</a>, content on this site is licensed under the
|
||||
<strong><a href="{{ ccurl }}">Creative Commons Attribution Share-Alike License v3.0</a></strong> or any later version
|
||||
{%- endtrans %}<span title="{{ settings.HOSTNAME }}">.</span>
|
||||
</p>
|
||||
<form class="languages go" method="get" action="">
|
||||
<label for="language">{{ _('Other languages:') }}</label>
|
||||
|
|
4
urls.py
4
urls.py
|
@ -33,8 +33,12 @@ urlpatterns = patterns('',
|
|||
# Redirect patterns.
|
||||
('^reviews/display/(\d+)',
|
||||
lambda r, id: redirect('reviews.list', id, permanent=True)),
|
||||
|
||||
('^browse/type:3$',
|
||||
lambda r: redirect('browse.language_tools', permanent=True)),
|
||||
|
||||
# SAMO/API
|
||||
('^api/', include('api.urls')),
|
||||
)
|
||||
|
||||
if settings.DEBUG:
|
||||
|
|
Загрузка…
Ссылка в новой задаче