Skip to content

Commit

Permalink
Create addon API
Browse files Browse the repository at this point in the history
  • Loading branch information
davedash committed Sep 20, 2010
1 parent 59ce2db commit d27ccef
Show file tree
Hide file tree
Showing 11 changed files with 417 additions and 11 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -13,3 +13,4 @@ build.py
*-all.js
vendor
.noseids
tmp/*
8 changes: 7 additions & 1 deletion apps/amo/__init__.py
Expand Up @@ -150,6 +150,7 @@ def __set__(self, obj, value):
# Applications
class FIREFOX:
id = 1
shortername = 'fx'
short = 'firefox'
pretty = _(u'Firefox')
browser = True
Expand All @@ -165,6 +166,7 @@ class FIREFOX:
class THUNDERBIRD:
id = 18
short = 'thunderbird'
shortername = 'tb'
pretty = _(u'Thunderbird')
browser = True
types = [ADDON_EXTENSION, ADDON_THEME, ADDON_DICT, ADDON_LPAPP,
Expand All @@ -177,6 +179,7 @@ class THUNDERBIRD:
class SEAMONKEY:
id = 59
short = 'seamonkey'
shortername = 'sm'
pretty = _(u'SeaMonkey')
browser = True
types = [ADDON_EXTENSION, ADDON_THEME, ADDON_DICT, ADDON_SEARCH,
Expand All @@ -189,6 +192,7 @@ class SEAMONKEY:
class SUNBIRD:
id = 52
short = 'sunbird'
shortername = 'sb'
pretty = _(u'Sunbird')
types = [ADDON_EXTENSION, ADDON_THEME, ADDON_DICT, ADDON_LPAPP]
guid = '{718e30fb-e89b-41dd-9da7-e25a45638b28}'
Expand All @@ -199,6 +203,7 @@ class SUNBIRD:
class MOBILE:
id = 60
short = 'mobile'
shortername = 'fn'
pretty = _(u'Mobile')
browser = True
types = [ADDON_EXTENSION, ADDON_THEME, ADDON_DICT, ADDON_SEARCH,
Expand All @@ -216,6 +221,7 @@ class MOZILLA:
"""
id = 2
short = 'mz'
shortername = 'mz'
pretty = _(u'Mozilla')
browser = True
types = [ADDON_EXTENSION, ADDON_THEME, ADDON_DICT, ADDON_SEARCH,
Expand Down Expand Up @@ -331,7 +337,7 @@ class LICENSE_CUSTOM(_LicenseBase):
id = -1
name = _(u'Custom License')
url = None
shortname = None
shortname = 'other'


class LICENSE_MPL(_LicenseBase):
Expand Down
56 changes: 56 additions & 0 deletions apps/amo/fixtures/base/platforms.json
@@ -0,0 +1,56 @@
[
{
"pk": 1,
"model": "files.platform",
"fields": {
"icontype": "",
"modified": "2008-04-07 08:16:55",
"created": "2007-03-05 13:09:27"
}
},
{
"pk": 2,
"model": "files.platform",
"fields": {
"icontype": "",
"modified": "2008-04-07 08:16:55",
"created": "2007-03-05 13:09:27"
}
},
{
"pk": 3,
"model": "files.platform",
"fields": {
"icontype": "",
"modified": "2008-04-07 08:16:55",
"created": "2007-03-05 13:09:27"
}
},
{
"pk": 4,
"model": "files.platform",
"fields": {
"icontype": "",
"modified": "2008-04-07 08:16:55",
"created": "2007-03-05 13:09:27"
}
},
{
"pk": 5,
"model": "files.platform",
"fields": {
"icontype": "",
"modified": "2008-04-07 08:16:56",
"created": "2007-03-05 13:09:27"
}
},
{
"pk": 6,
"model": "files.platform",
"fields": {
"icontype": "",
"modified": "2008-04-07 08:16:56",
"created": "2007-03-05 13:09:27"
}
}
]
Binary file added apps/api/fixtures/api/helloworld.xpi
Binary file not shown.
51 changes: 50 additions & 1 deletion apps/api/handlers.py
@@ -1,17 +1,66 @@
import jingo
from django.db import transaction

import commonware.log
from piston.handler import BaseHandler
from piston.utils import rc, throttle
from tower import ugettext as _

from addons.models import Addon
from users.models import UserProfile
from versions.forms import LicenseForm, XPIForm

log = commonware.log.getLogger('z.api')


class UserHandler(BaseHandler):
allowed_methods = ('GET',)
model = UserProfile
fields = ('email',)

def read(self, request):
try:
user = UserProfile.objects.get(user=request.user)
return user
except UserProfile.DoesNotExist:
return None


class AddonsHandler(BaseHandler):
allowed_methods = ('POST',)
model = Addon
fields = ('id', 'name', )
exclude = ('highest_status', 'icon_type')

# Custom handler so translated text doesn't look weird
@classmethod
def name(cls, addon):
return addon.name.localized_string

# We need multiple validation, so don't use @validate decorators.
@transaction.commit_on_success
@throttle(10, 60 * 60) # allow 10 addons an hour
def create(self, request):
license_form = LicenseForm(request.POST)

if not license_form.is_valid():
resp = rc.BAD_REQUEST
error = ', '.join(license_form.errors['__all__'])
resp.write(': ' +
# L10n: {0} is comma separated errors for license.
_(u'Invalid license data provided: {0}').format(error))
log.debug(error)
return resp

new_file_form = XPIForm(request.POST, request.FILES)

if not new_file_form.is_valid():
resp = rc.BAD_REQUEST
resp.write(': ' + _('Addon did not validate.'))
log.debug('Addon did not validate for %s' % request.amo_user)
return resp

license_id = license_form.get_id_or_create()

a = new_file_form.create_addon(user=request.amo_user,
license_id=license_id)
return a
61 changes: 53 additions & 8 deletions apps/api/tests/test_oauth.py
Expand Up @@ -20,11 +20,11 @@
oauth_version="1.0"
"""
import json
import random
import os
import time
import urlparse
from hashlib import md5

from django.conf import settings
from django.test.client import Client

import oauth2 as oauth
Expand All @@ -34,6 +34,7 @@
from piston.models import Consumer, Token

from amo.urlresolvers import reverse
from addons.models import Addon


def _get_args(consumer, token=None, callback=False, verifier=None):
Expand Down Expand Up @@ -86,7 +87,6 @@ def post(self, url, consumer=None, token=None, callback=False,
data=data, **req)



client = OAuthClient()
token_keys = ('oauth_token_secret', 'oauth_token',)

Expand Down Expand Up @@ -115,8 +115,8 @@ def get_access_token(consumer, token, authorize=True, verifier=None):
eq_(r.status_code, 401)


class TestOauth(TestCase):
fixtures = ('base/users',)
class BaseOauth(TestCase):
fixtures = ('base/users', 'base/appversion', 'base/platforms')

def setUp(self):
consumers = []
Expand Down Expand Up @@ -154,7 +154,7 @@ def _oauth_flow(self, consumer, authorize=True, callback=False):
verifier = None

if authorize:
r = self.client.post(url, d)
r = self.client.post(url, d,)

if callback:
redir = r.get('location', None)
Expand All @@ -172,16 +172,19 @@ def _oauth_flow(self, consumer, authorize=True, callback=False):
piston_token = Token.objects.get()
assert not piston_token.is_approved, "Token saved."

token = get_access_token(consumer, token, authorize, verifier)
self.token = get_access_token(consumer, token, authorize, verifier)

r = client.get('api.user', consumer, token)
r = client.get('api.user', consumer, self.token)

if authorize:
data = json.loads(r.content)
eq_(data['email'], 'admin@mozilla.com')
else:
eq_(r.status_code, 401)


class TestBasicOauth(BaseOauth):

def test_accepted(self):
self._oauth_flow(self.accepted_consumer)

Expand All @@ -205,3 +208,45 @@ def test_request_token_fake(self):
c.secret = 'mom'
r = client.get('oauth.request_token', c)
eq_(r.content, 'Invalid consumer.')


class TestAddon(BaseOauth):
def make_create_request(self, data):
consumer = self.accepted_consumer
self._oauth_flow(consumer)
return client.post('api.addons', consumer, self.token, data=data)

def test_create(self):
# License (req'd): MIT, GPLv2, GPLv3, LGPLv2.1, LGPLv3, MIT, BSD, Other
# Custom License (if other, req'd)
# XPI file... (req'd)
# Platform (All by default): 'mac', 'all', 'bsd', 'linux', 'solaris',
# 'windows'
path = 'apps/api/fixtures/api/helloworld.xpi'
xpi = os.path.join(settings.ROOT, path)
f = open(xpi)

data = dict(
license_type='other',
license_text='This is FREE!',
platform='mac',
xpi=f,
)

r = self.make_create_request(data)

eq_(r.status_code, 200, r.content)

data = json.loads(r.content)
id = data['id']
name = data['name']
eq_(name, 'XUL School Hello World')
assert Addon.objects.get(pk=id)

def test_create_nolicense(self):
data = {}

r = self.make_create_request(data)
eq_(r.status_code, 400, r.content)
eq_(r.content, 'Bad Request: '
'Invalid license data provided: License text missing.')
2 changes: 2 additions & 0 deletions apps/api/urls.py
Expand Up @@ -63,9 +63,11 @@ def build_urls(base, appendages):

ad = dict(authentication=authentication.AMOOAuthAuthentication())
user_resource = Resource(handler=handlers.UserHandler, **ad)
addons_resource = Resource(handler=handlers.AddonsHandler, **ad)

piston_patterns = patterns('',
url(r'^user/$', user_resource, name='api.user'),
url(r'^addons/$', addons_resource, name='api.addons'),
)

urlpatterns = patterns('',
Expand Down
22 changes: 22 additions & 0 deletions apps/files/models.py
Expand Up @@ -5,6 +5,7 @@
from django.utils import translation

import amo.models
import amo.utils
from amo.urlresolvers import reverse
from cake.urlresolvers import remora_url

Expand Down Expand Up @@ -33,6 +34,27 @@ def get_url_path(self, app, src):
self.filename, src)
return urlparams(base, confirmed=1)

def generate_filename(self, extension='xpi'):
"""
Files are in the format of:
{addon_name}-{version}-{apps}-{platform}
"""
parts = []
parts.append(
amo.utils.slugify(self.version.addon.name).replace('-', '_'))
parts.append(self.version.version)

if self.version.compatible_apps:
apps = '+'.join([a.shortername for a in
self.version.compatible_apps])
parts.append(apps)

if self.platform_id and self.platform_id != amo.PLATFORM_ALL.id:
parts.append(amo.PLATFORMS[self.platform_id].shortname)

self.filename = '-'.join(parts) + '.' + extension
return self.filename

def latest_xpi_url(self):
# TODO(jbalogh): reverse?
addon = self.version.addon_id
Expand Down
16 changes: 16 additions & 0 deletions apps/files/tests.py
@@ -1,5 +1,6 @@
from django import test

from mock import Mock
from nose.tools import eq_

import amo
Expand Down Expand Up @@ -35,3 +36,18 @@ def test_latest_url(self):
def test_eula_url(self):
f = File.objects.get(id=67442)
eq_(f.eula_url(), '/addon/3615/eula/67442')

def test_generate_filename(self):
f = File.objects.get(id=67442)
eq_(f.generate_filename(), 'delicious_bookmarks-2.1.072-fx.xpi')

def test_generate_filename_platform_specific(self):
f = File.objects.get(id=67442)
f.platform_id = amo.PLATFORM_MAC.id
eq_(f.generate_filename(), 'delicious_bookmarks-2.1.072-fx-mac.xpi')

def test_generate_filename_many_apps(self):
f = File.objects.get(id=67442)
f.version.compatible_apps = (amo.FIREFOX, amo.THUNDERBIRD)
eq_(f.generate_filename(), 'delicious_bookmarks-2.1.072-fx+tb.xpi')

0 comments on commit d27ccef

Please sign in to comment.