Merge branch 'master' of github.com:jbalogh/zamboni

This commit is contained in:
Jeff Balogh 2010-02-08 16:28:48 -08:00
Родитель 14e457346e 6b6c720a6c
Коммит b9e21846d3
17 изменённых файлов: 369 добавлений и 11 удалений

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

@ -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))

3
apps/api/__init__.py Normal file
Просмотреть файл

@ -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 %}

72
apps/api/tests.py Normal file
Просмотреть файл

@ -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")

13
apps/api/urls.py Normal file
Просмотреть файл

@ -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'),
)

91
apps/api/views.py Normal file
Просмотреть файл

@ -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

@ -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>

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

@ -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: