Add-on packager
This commit is contained in:
Родитель
1b909de3a9
Коммит
6be97c5d87
|
@ -115,6 +115,17 @@ def gc(test_result=True):
|
|||
else:
|
||||
log.warning('NETAPP_STORAGE not defined.')
|
||||
|
||||
if settings.PACKAGER_PATH:
|
||||
log.debug('Cleaning up old packaged add-ons.')
|
||||
|
||||
cmd = ('find', settings.PACKAGER_PATH,
|
||||
'-name', '*.zip', '-mtime', '+1', '-type', 'f',
|
||||
'-exec', 'rm', '{}', ';')
|
||||
output = Popen(cmd, stdout=PIPE).communicate()[0]
|
||||
|
||||
for line in output.split("\n"):
|
||||
log.debug(line)
|
||||
|
||||
if settings.COLLECTIONS_ICON_PATH:
|
||||
log.debug('Cleaning up uncompressed icons.')
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
"pk": 2,
|
||||
"model": "applications.application",
|
||||
"fields": {
|
||||
"guid": "{a23983c0-fd0e-11dc-95ff-0800200c9a66}",
|
||||
"guid": "{86c18b42-e466-45a9-ae7a-9b95ba6f5640}",
|
||||
"created": "2006-08-21 23:53:19"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -403,5 +403,38 @@
|
|||
"modified": null,
|
||||
"created": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 400,
|
||||
"model": "applications.appversion",
|
||||
"fields": {
|
||||
"version_int": 4000000102000,
|
||||
"application": 59,
|
||||
"version": "4.0",
|
||||
"modified": null,
|
||||
"created": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 401,
|
||||
"model": "applications.appversion",
|
||||
"fields": {
|
||||
"version_int": 4000000102000,
|
||||
"application": 18,
|
||||
"version": "4.1",
|
||||
"modified": null,
|
||||
"created": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 402,
|
||||
"model": "applications.appversion",
|
||||
"fields": {
|
||||
"version_int": 4000000102000,
|
||||
"application": 2,
|
||||
"version": "4.0b2pre",
|
||||
"modified": null,
|
||||
"created": null
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -128,11 +128,12 @@ def monitor(request, format=None):
|
|||
settings.GUARDED_ADDONS_PATH,
|
||||
settings.ADDON_ICONS_PATH,
|
||||
settings.COLLECTIONS_ICON_PATH,
|
||||
settings.PACKAGER_PATH,
|
||||
settings.PREVIEWS_PATH,
|
||||
settings.USERPICS_PATH,
|
||||
settings.SPHINX_CATALOG_PATH,
|
||||
settings.SPHINX_LOG_PATH,
|
||||
dump_apps.Command.JSON_PATH)
|
||||
dump_apps.Command.JSON_PATH,)
|
||||
r = [os.path.join(settings.ROOT, 'locale')]
|
||||
filepaths = [(path, os.R_OK | os.W_OK, "We want read + write")
|
||||
for path in rw]
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import path
|
||||
import re
|
||||
import socket
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from django.forms.models import modelformset_factory
|
||||
from django.forms.formsets import formset_factory, BaseFormSet
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.encoding import force_unicode
|
||||
|
||||
|
@ -20,7 +22,7 @@ from amo.forms import AMOModelForm
|
|||
from amo.widgets import EmailWidget
|
||||
from applications.models import Application, AppVersion
|
||||
from files.models import File, FileUpload, Platform
|
||||
from files.utils import parse_addon
|
||||
from files.utils import parse_addon, VERSION_RE
|
||||
from translations.widgets import TranslationTextarea, TranslationTextInput
|
||||
from translations.fields import TransTextarea, TransField
|
||||
from translations.models import delete_translation
|
||||
|
@ -353,11 +355,7 @@ CompatFormSet = modelformset_factory(
|
|||
form=CompatForm, can_delete=True, extra=0)
|
||||
|
||||
|
||||
class NewAddonForm(happyforms.Form):
|
||||
upload = forms.ModelChoiceField(widget=forms.HiddenInput,
|
||||
queryset=FileUpload.objects.filter(valid=True),
|
||||
error_messages={'invalid_choice': _lazy('There was an error with your '
|
||||
'upload. Please try again.')})
|
||||
class AddonPlatformForm(happyforms.Form):
|
||||
desktop_platforms = forms.ModelMultipleChoiceField(
|
||||
queryset=Platform.objects,
|
||||
widget=forms.CheckboxSelectMultiple(attrs={'class': 'platform'}),
|
||||
|
@ -374,8 +372,6 @@ class NewAddonForm(happyforms.Form):
|
|||
|
||||
def clean(self):
|
||||
if not self.errors:
|
||||
xpi = parse_addon(self.cleaned_data['upload'].path)
|
||||
addons.forms.clean_name(xpi['name'])
|
||||
self._clean_all_platforms()
|
||||
return self.cleaned_data
|
||||
|
||||
|
@ -385,6 +381,19 @@ class NewAddonForm(happyforms.Form):
|
|||
raise forms.ValidationError(_('Need at least one platform.'))
|
||||
|
||||
|
||||
class NewAddonForm(AddonPlatformForm):
|
||||
upload = forms.ModelChoiceField(widget=forms.HiddenInput,
|
||||
queryset=FileUpload.objects.filter(valid=True),
|
||||
error_messages={'invalid_choice': _lazy('There was an error with your '
|
||||
'upload. Please try again.')})
|
||||
|
||||
def clean(self):
|
||||
if not self.errors:
|
||||
xpi = parse_addon(self.cleaned_data['upload'].path)
|
||||
addons.forms.clean_name(xpi['name'])
|
||||
return self.cleaned_data
|
||||
|
||||
|
||||
class NewVersionForm(NewAddonForm):
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
|
@ -601,3 +610,129 @@ class NewsletterForm(forms.Form):
|
|||
choices=(('html', _lazy(u'HTML')),
|
||||
('text', _lazy(u'Text'))))
|
||||
policy = forms.BooleanField()
|
||||
|
||||
|
||||
class PackagerBasicForm(forms.Form):
|
||||
name = forms.CharField()
|
||||
description = forms.CharField(required=False, widget=forms.Textarea)
|
||||
version = forms.CharField(max_length=32)
|
||||
id = forms.CharField()
|
||||
author_name = forms.CharField()
|
||||
contributors = forms.CharField(required=False, widget=forms.Textarea)
|
||||
|
||||
def clean_name(self):
|
||||
name_regex = re.compile('(mozilla|firefox)', re.I)
|
||||
|
||||
if name_regex.match(self.cleaned_data['name']):
|
||||
raise forms.ValidationError(
|
||||
_('Add-on names should not contain Mozilla trademarks.'))
|
||||
return self.cleaned_data['name']
|
||||
|
||||
def clean_id(self):
|
||||
id_regex = re.compile(
|
||||
"""(\{[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}\} | # GUID
|
||||
[a-z0-9-\.\+_]*\@[a-z0-9-\._]+) # Email format""",
|
||||
re.I | re.X)
|
||||
|
||||
if not id_regex.match(self.cleaned_data['id']):
|
||||
raise forms.ValidationError(
|
||||
_('The add-on ID must be a UUID string or an email ' +
|
||||
'address.'))
|
||||
return self.cleaned_data['id']
|
||||
|
||||
def clean_version(self):
|
||||
if not VERSION_RE.match(self.cleaned_data['version']):
|
||||
raise forms.ValidationError(
|
||||
_('The version string is invalid.'))
|
||||
return self.cleaned_data['version']
|
||||
|
||||
|
||||
class PackagerCompatForm(forms.Form):
|
||||
enabled = forms.BooleanField(required=False)
|
||||
min_ver = forms.ModelChoiceField(AppVersion.objects.none(),
|
||||
empty_label=None)
|
||||
max_ver = forms.ModelChoiceField(AppVersion.objects.none(),
|
||||
empty_label=None)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PackagerCompatForm, self).__init__(*args, **kwargs)
|
||||
if not self.initial:
|
||||
return
|
||||
|
||||
self.app = self.initial['application']
|
||||
qs = (AppVersion.objects.filter(application=self.app.id)
|
||||
.order_by('-version_int'))
|
||||
|
||||
self.fields['enabled'].label = self.app.pretty
|
||||
|
||||
# Don't allow version ranges as the minimum version.
|
||||
self.fields['min_ver'].queryset = qs.filter(~Q(version__contains='*'))
|
||||
self.fields['max_ver'].queryset = qs.all()
|
||||
|
||||
def clean(self):
|
||||
if self.errors:
|
||||
return
|
||||
|
||||
data = self.cleaned_data
|
||||
if not data['enabled']:
|
||||
return data
|
||||
|
||||
min_ver = data['min_ver']
|
||||
max_ver = data['max_ver']
|
||||
if not (min_ver and max_ver):
|
||||
raise forms.ValidationError(
|
||||
_('Invalid version range.'))
|
||||
|
||||
if min_ver.version_int > max_ver.version_int:
|
||||
raise forms.ValidationError(
|
||||
_('Min version must be less than Max version.'))
|
||||
|
||||
# Pass back the app name and GUID.
|
||||
data['min_ver'] = str(min_ver)
|
||||
data['max_ver'] = str(max_ver)
|
||||
data['name'] = self.app.pretty
|
||||
data['guid'] = self.app.guid
|
||||
return data
|
||||
|
||||
|
||||
class PackagerCompatBaseFormSet(BaseFormSet):
|
||||
def __init__(self, *args, **kw):
|
||||
super(PackagerCompatBaseFormSet, self).__init__(*args, **kw)
|
||||
self.initial = [{'application': a} for a in amo.APP_USAGE]
|
||||
self._construct_forms()
|
||||
|
||||
def clean(self):
|
||||
if any(self.errors):
|
||||
return
|
||||
|
||||
if not any(cf.cleaned_data.get('enabled') for cf in self.forms):
|
||||
raise forms.ValidationError(
|
||||
_('At least one target application must be selected.'))
|
||||
|
||||
PackagerCompatFormSet = formset_factory(PackagerCompatForm,
|
||||
formset=PackagerCompatBaseFormSet,
|
||||
extra=0)
|
||||
|
||||
|
||||
class PackagerFeaturesForm(forms.Form):
|
||||
about_dialog = forms.BooleanField(
|
||||
required=False,
|
||||
label=_lazy('About dialog'))
|
||||
preferences_dialog = forms.BooleanField(
|
||||
required=False,
|
||||
label=_lazy('Preferences Dialog'))
|
||||
toolbar = forms.BooleanField(
|
||||
required=False,
|
||||
label=_lazy('Toolbar'))
|
||||
toolbar_button = forms.BooleanField(
|
||||
required=False,
|
||||
label=_lazy('Toolbar button'))
|
||||
main_menu_command = forms.BooleanField(
|
||||
required=False,
|
||||
label=_lazy('Main menu command'))
|
||||
context_menu_command = forms.BooleanField(
|
||||
required=False,
|
||||
label=_lazy('Context menu command'))
|
||||
sidebar_support = forms.BooleanField(
|
||||
required=False,
|
||||
label=_lazy('Sidebar support'))
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from amo.utils import slugify
|
||||
from celeryutils import task
|
||||
|
||||
from addons.models import Addon
|
||||
|
@ -163,6 +165,7 @@ def resize_preview(src, thumb_dst, full_dst, **kw):
|
|||
except Exception, e:
|
||||
log.error("Error saving preview: %s" % e)
|
||||
|
||||
|
||||
@task
|
||||
@set_modified_on
|
||||
def resize_preview_store_size(src, instance, **kw):
|
||||
|
@ -183,6 +186,7 @@ def resize_preview_store_size(src, instance, **kw):
|
|||
except Exception, e:
|
||||
log.error("Error saving preview: %s" % e)
|
||||
|
||||
|
||||
@task
|
||||
@write
|
||||
def get_preview_sizes(ids, **kw):
|
||||
|
@ -223,3 +227,28 @@ def convert_purified(ids, **kw):
|
|||
if flag:
|
||||
log.info('Saving addon: %s to purify fields' % addon.pk)
|
||||
addon.save()
|
||||
|
||||
|
||||
@task
|
||||
def packager(data, feature_set, **kw):
|
||||
"""Build an add-on based on input data."""
|
||||
log.info('[1@None] Packaging add-on')
|
||||
|
||||
# "Lock" the file by putting .lock in its name.
|
||||
from devhub.views import packager_path
|
||||
xpi_path = packager_path('%s.lock' % data['uuid'])
|
||||
log.info('Saving package to: %s' % xpi_path)
|
||||
|
||||
from packager.main import packager
|
||||
|
||||
data['slug'] = slugify(data['name']).replace('-', '_')
|
||||
features = set([k for k, v in feature_set.items() if v])
|
||||
|
||||
packager(data, xpi_path, features)
|
||||
|
||||
# Unlock the file and make it available.
|
||||
try:
|
||||
shutil.move(xpi_path, packager_path(data['uuid']))
|
||||
except IOError:
|
||||
log.error('Error unlocking add-on: %s' % xpi_path)
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@
|
|||
<li class="top">
|
||||
<a href="#" class="controller">{{ _('Tools') }}</a>
|
||||
<ul>
|
||||
<li><a href="{{ remora_url('/developers/tools/builder') }}">
|
||||
<li><a href="{{ url('devhub.package_addon') }}">
|
||||
{{ _('Add-on Packager') }}</a></li>
|
||||
<li><a href="{{ url('devhub.validate_addon') }}">
|
||||
{{ _('Add-on Validator') }}</a></li>
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
{% extends "devhub/base.html" %}
|
||||
{% from 'includes/forms.html' import required %}
|
||||
|
||||
{% set title = _('Add-on Packager') %}
|
||||
{% block title %}{{ dev_page_title(title) }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<header>
|
||||
{{ dev_breadcrumbs(addon, items=[(None, title)]) }}
|
||||
<h2>{{ title }}</h2>
|
||||
</header>
|
||||
<form method="post" id="packager" class="item" action="">
|
||||
{{ csrf() }}
|
||||
<p class="summary">
|
||||
{% trans %}
|
||||
Enter some basic information about your add-on below and select which
|
||||
interface components to start with, and your custom-built add-on will be
|
||||
ready for download.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<fieldset>
|
||||
<legend>{{ _('Describe your add-on') }}</legend>
|
||||
<p>
|
||||
{% trans %}
|
||||
First, you need to enter some basic information about your add-on. This
|
||||
will be displayed in the Add-ons Manager when your extension is installed.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
{{ basic_form.non_field_errors() }}
|
||||
<ul>
|
||||
<li class="required packager_input">
|
||||
<label for="name">{{ _('Add-on Name') }} {{ required() }}</label>
|
||||
{{ basic_form.name }}
|
||||
{{ basic_form.name.errors }}
|
||||
</li>
|
||||
<li class="packager_input">
|
||||
<label for="description">{{ _('Description') }}</label>
|
||||
{{ basic_form.description }}
|
||||
{{ basic_form.description.errors }}
|
||||
</li>
|
||||
<li class="required packager_input">
|
||||
<label for="version">{{ _('Add-on Version') }} {{ required() }}</label>
|
||||
{{ basic_form.version }}
|
||||
{{ basic_form.version.errors }}
|
||||
</li>
|
||||
<li class="required packager_input">
|
||||
<label for="id">{{ _('Unique ID') }} {{ required() }}</label>
|
||||
{{ basic_form.id }}
|
||||
{{ basic_form.id.errors }}
|
||||
</li>
|
||||
</ul>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{{ _("Who's working on your add-on?") }}</legend>
|
||||
<p>
|
||||
{% trans %}
|
||||
Tell us about the people or companies behind this add-on. This information
|
||||
appears in the add-on's About dialog.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<ul>
|
||||
<li class="required packager_input">
|
||||
<label for="author_name">{{ _('Primary Author') }} {{ required() }}</label>
|
||||
{{ basic_form.author_name }}
|
||||
{{ basic_form.author_name.errors }}
|
||||
</li>
|
||||
<li class="packager_input">
|
||||
<label for="contributors">{{ _('Contributors') }}</label>
|
||||
{{ basic_form.contributors }}
|
||||
{{ basic_form.contributors.errors }}
|
||||
</li>
|
||||
</ul>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{{ _('Where will your add-on run?') }}</legend>
|
||||
<p>
|
||||
{% trans %}
|
||||
Select the applications and versions that your add-on will support. The
|
||||
versions that you select will be the only versions that your add-on will
|
||||
be installable on. Make sure you only select applications and versions
|
||||
that you intend to test your add-on with.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<div class="supportedapps">
|
||||
<label>{{ _('Supported Applications') }}</label>
|
||||
{{ compat_forms.management_form }}
|
||||
{{ compat_forms.non_form_errors() }}
|
||||
<ul>
|
||||
{% for compat in compat_forms %}
|
||||
<li class="compat_form">
|
||||
<label>
|
||||
{{ compat.enabled }}
|
||||
{{ compat.enabled.label }}
|
||||
</label>
|
||||
<span>
|
||||
{{ compat.min_ver.label }}
|
||||
{{ compat.min_ver }}
|
||||
</span>
|
||||
<span>
|
||||
{{ compat.max_ver.label }}
|
||||
{{ compat.max_ver }}
|
||||
</span>
|
||||
{{ compat.non_field_errors() }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{{ _('Choose pre-built features') }}</legend>
|
||||
<p>
|
||||
{% trans %}
|
||||
Get started quicly by selecting user interface components to include in
|
||||
your add-on package. We'll include documented code for each item you
|
||||
select so your extension will work out of the box.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<ul>
|
||||
{% for feature in features_form %}
|
||||
<li>
|
||||
{{ feature }}
|
||||
{{ feature.label }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</fieldset>
|
||||
<input type="submit" value="{{ _('Submit and Build') }}" />
|
||||
</form>
|
||||
{% endblock content %}
|
|
@ -0,0 +1,45 @@
|
|||
{% extends "devhub/base.html" %}
|
||||
{% from 'includes/forms.html' import required %}
|
||||
|
||||
{% set title = _('Add-on Packager') %}
|
||||
{% block title %}{{ dev_page_title(title) }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<header>
|
||||
{{ dev_breadcrumbs(addon, items=[(None, title)]) }}
|
||||
<h2>{{ title }}</h2>
|
||||
<h3>{{ _('Add-on packaged successfully!') }}</h3>
|
||||
</header>
|
||||
<section class="item">
|
||||
<p>
|
||||
{% trans %}
|
||||
Use the download link below to save a copy of your extension's compressed
|
||||
source. To install as an extension in Firefox or another compatible
|
||||
application, simply rename the <kbd>.zip</kbd> extension to <kbd>.xpi</kbd>.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<div id="packager-download" data-downloadurl="{{ url('devhub.package_addon_json', uuid) }}">
|
||||
<span>{{ _('Please wait...') }}</span>
|
||||
<noscript>
|
||||
{% trans %}
|
||||
JavaScript needs to be enabled to fetch the packaged add-on.
|
||||
{% endtrans %}
|
||||
</noscript>
|
||||
</div>
|
||||
</section>
|
||||
<header>
|
||||
<h3>{{ _('What do I do next?') }}</h3>
|
||||
</header>
|
||||
<section class="item">
|
||||
<p>
|
||||
{% trans gs_url=url('devhub.docs', 'getting-started'),
|
||||
ht_url=url('devhub.docs', 'how-to') %}
|
||||
Now that you've got an extension skeleton, the fun part begins: hacking on
|
||||
the extension to make it do what you want. If you need some help, check out
|
||||
our <a href="{{ gs_url }}">Getting Started</a> page for the basics, or
|
||||
visit the <a href="{{ ht_url }}">How-to Library</a> for tutorials and
|
||||
documentation.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
</section>
|
||||
{% endblock content %}
|
|
@ -0,0 +1,168 @@
|
|||
import json
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
import test_utils
|
||||
|
||||
from nose.tools import eq_
|
||||
from pyquery import PyQuery as pq
|
||||
|
||||
from amo.tests import formset, initial
|
||||
from amo.urlresolvers import reverse
|
||||
from devhub.views import packager_path
|
||||
|
||||
|
||||
class TestAddOnPackager(test_utils.TestCase):
|
||||
fixtures = ['base/apps', 'base/users', 'base/appversion']
|
||||
|
||||
def setUp(self):
|
||||
assert self.client.login(username='regular@mozilla.com',
|
||||
password='password')
|
||||
self.package_addon = reverse('devhub.package_addon')
|
||||
|
||||
ctx = self.client.get(self.package_addon).context['compat_forms']
|
||||
self.compat_form = initial(ctx.initial_forms[1])
|
||||
|
||||
def test_has_versions(self):
|
||||
"""Test that versions are listed in targetApplication fields."""
|
||||
r = self.client.get(self.package_addon)
|
||||
eq_(r.status_code, 200)
|
||||
|
||||
doc = pq(r.content)
|
||||
# Assert that the first dropdown (Firefox) has at least thirty items.
|
||||
assert len(doc('.compat_form select').children()) > 30
|
||||
|
||||
def test_no_mozilla(self):
|
||||
"""
|
||||
Test that the Mozilla browser is not represented in the
|
||||
targetApplication list.
|
||||
"""
|
||||
r = self.client.get(self.package_addon)
|
||||
eq_(r.status_code, 200)
|
||||
|
||||
doc = pq(r.content)
|
||||
for label in doc('.compat_form label'):
|
||||
assert pq(label).text() != 'Mozilla'
|
||||
|
||||
def _form_data(self, data=None, compat_form=True):
|
||||
"""Build the initial data set for the form."""
|
||||
|
||||
initial_data = {'author_name': 'author',
|
||||
'contributors': '',
|
||||
'description': '',
|
||||
'name': 'name',
|
||||
'id': 'foo@bar.com',
|
||||
'version': '1.2.3'}
|
||||
|
||||
if compat_form:
|
||||
initial_data.update(formset(self.compat_form))
|
||||
|
||||
if data:
|
||||
initial_data.update(data)
|
||||
return initial_data
|
||||
|
||||
def test_validate_pass(self):
|
||||
"""
|
||||
Test that a proper set of data will pass validation and pass through
|
||||
to the success view.
|
||||
"""
|
||||
self.compat_form['enabled'] = 'on'
|
||||
self.compat_form['min_ver'] = '86'
|
||||
self.compat_form['max_ver'] = '114'
|
||||
r = self.client.post(self.package_addon, self._form_data(),
|
||||
follow=True)
|
||||
eq_(r.status_code, 200)
|
||||
eq_(pq(pq(r.content)('h3')[0]).text(), 'Add-on packaged successfully!')
|
||||
|
||||
def test_validate_name(self):
|
||||
"""Test that the add-on name is properly validated."""
|
||||
r = self.client.post(self.package_addon,
|
||||
self._form_data({'name': 'Mozilla App'}))
|
||||
self.assertFormError(
|
||||
r, 'basic_form', 'name',
|
||||
'Add-on names should not contain Mozilla trademarks.')
|
||||
|
||||
def test_validate_version(self):
|
||||
"""Test that the add-on version is properly validated."""
|
||||
r = self.client.post(self.package_addon,
|
||||
self._form_data({'version': 'invalid version'}))
|
||||
self.assertFormError(
|
||||
r, 'basic_form', 'version',
|
||||
'The version string is invalid.')
|
||||
|
||||
def test_validate_id(self):
|
||||
"""Test that the add-on id is properly validated."""
|
||||
r = self.client.post(self.package_addon,
|
||||
self._form_data({'id': 'invalid id'}))
|
||||
self.assertFormError(
|
||||
r, 'basic_form', 'id',
|
||||
'The add-on ID must be a UUID string or an email address.')
|
||||
|
||||
def test_validate_version_enabled(self):
|
||||
"""Test that at least one version must be enabled."""
|
||||
# Nothing needs to be done; no apps are enabled by default.
|
||||
r = self.client.post(self.package_addon, self._form_data())
|
||||
assert not r.context['compat_forms'].is_valid()
|
||||
|
||||
def test_validate_version_order(self):
|
||||
"""Test that the min version is lte the max version."""
|
||||
self.compat_form['enabled'] = 'on'
|
||||
self.compat_form['min_ver'] = '114'
|
||||
self.compat_form['max_ver'] = '86'
|
||||
r = self.client.post(self.package_addon,
|
||||
self._form_data())
|
||||
eq_(r.context['compat_forms'].errors[0]['__all__'][0],
|
||||
'Min version must be less than Max version.')
|
||||
|
||||
def test_required_login(self):
|
||||
self.client.logout()
|
||||
r = self.client.get(self.package_addon)
|
||||
eq_(r.status_code, 302)
|
||||
|
||||
|
||||
class TestPackagerJSON(test_utils.TestCase):
|
||||
fixtures = ['base/apps', 'base/users', 'base/appversion']
|
||||
|
||||
def setUp(self):
|
||||
assert self.client.login(username='regular@mozilla.com',
|
||||
password='password')
|
||||
|
||||
def package_json(self, id):
|
||||
return reverse('devhub.package_addon_json', args=[id])
|
||||
|
||||
def _prep_mock_package(self, name):
|
||||
"""Prep a fake package to be downloaded."""
|
||||
path = packager_path(name)
|
||||
with open(path, mode='w') as package:
|
||||
package.write('ready')
|
||||
|
||||
def _unprep_package(self, name):
|
||||
package = packager_path(name)
|
||||
if os.path.exists(package):
|
||||
os.remove(package)
|
||||
|
||||
def test_json_unavailable(self):
|
||||
"""
|
||||
Test that an unavailable message is returned when the file isn't ready
|
||||
to be downloaded yet.
|
||||
"""
|
||||
|
||||
# Ensure a deleted file returns an empty message.
|
||||
self._unprep_package('foobar')
|
||||
r = self.client.get(self.package_json('foobar'))
|
||||
eq_(r.content, 'null')
|
||||
|
||||
# Ensure a completed file returns the file data.
|
||||
self._prep_mock_package('foobar')
|
||||
r = self.client.get(self.package_json('foobar'))
|
||||
data = json.loads(r.content)
|
||||
|
||||
assert 'download_url' in data
|
||||
pack = self.client.get(data['download_url'])
|
||||
eq_(pack.status_code, 200)
|
||||
|
||||
assert 'size' in data
|
||||
assert isinstance(data['size'], (int, float))
|
||||
|
||||
self._unprep_package('foobar')
|
||||
|
|
@ -89,6 +89,16 @@ ajax_patterns = patterns('',
|
|||
url('^image/status$', views.image_status, name='devhub.ajax.image.status')
|
||||
)
|
||||
|
||||
packager_patterns = patterns('',
|
||||
url('^$', views.package_addon, name='devhub.package_addon'),
|
||||
url('^download/(?P<id>[\w\d]+)$', views.package_addon_download,
|
||||
name='devhub.package_addon_download'),
|
||||
url('^json/(?P<id>[\w\d]+)$', views.package_addon_json,
|
||||
name='devhub.package_addon_json'),
|
||||
url('^success/(?P<id>[\w\d]+)$', views.package_addon_success,
|
||||
name='devhub.package_addon_success'),
|
||||
)
|
||||
|
||||
redirect_patterns = patterns('',
|
||||
('^addon/edit/(\d+)',
|
||||
lambda r, id: redirect('devhub.addons.edit', id, permanent=True)),
|
||||
|
@ -118,6 +128,9 @@ urlpatterns = decorate(write, patterns('',
|
|||
url('^addon/validate/?$', views.validate_addon,
|
||||
name='devhub.validate_addon'),
|
||||
|
||||
# Add-on packager
|
||||
url('^addon/package/', include(packager_patterns)),
|
||||
|
||||
# Redirect to /addons/ at the base.
|
||||
url('^addon$', lambda r: redirect('devhub.addons', permanent=True)),
|
||||
url('^addons$', views.dashboard, name='devhub.addons'),
|
||||
|
|
|
@ -39,6 +39,7 @@ from addons import forms as addon_forms
|
|||
from addons.decorators import addon_view
|
||||
from addons.models import Addon, AddonUser
|
||||
from addons.views import BaseFilter
|
||||
from applications.models import Application, AppVersion
|
||||
from devhub.models import ActivityLog, BlogPost, RssKey, SubmitStep
|
||||
from editors.helpers import get_position
|
||||
from files.models import File, FileUpload
|
||||
|
@ -503,6 +504,74 @@ def validate_addon(request):
|
|||
return jingo.render(request, 'devhub/validate_addon.html', {})
|
||||
|
||||
|
||||
def packager_path(name):
|
||||
return os.path.join(settings.PACKAGER_PATH, '%s.zip' % name)
|
||||
|
||||
|
||||
@login_required
|
||||
def package_addon(request):
|
||||
basic_form = forms.PackagerBasicForm(request.POST or None)
|
||||
features_form = forms.PackagerFeaturesForm(request.POST or None)
|
||||
compat_forms = forms.PackagerCompatFormSet(request.POST or None)
|
||||
|
||||
# Process requests, but also avoid short circuiting by using all().
|
||||
if (request.method == 'POST' and
|
||||
all([basic_form.is_valid(),
|
||||
features_form.is_valid(),
|
||||
compat_forms.is_valid()])):
|
||||
|
||||
basic_data = basic_form.cleaned_data
|
||||
compat_data = compat_forms.cleaned_data
|
||||
|
||||
# Generate a unique, non-iterable ID for the package we're building.
|
||||
builder_uuid = uuid.uuid4().hex
|
||||
|
||||
data = {'id': basic_data['id'],
|
||||
'version': basic_data['version'],
|
||||
'name': basic_data['name'],
|
||||
'description': basic_data['description'],
|
||||
'author_name': basic_data['author_name'],
|
||||
'contributors': basic_data['contributors'],
|
||||
'targetapplications': [c for c in compat_data if c['enabled']],
|
||||
'uuid': builder_uuid}
|
||||
tasks.packager.delay(data, features_form.cleaned_data)
|
||||
return redirect('devhub.package_addon_success', builder_uuid)
|
||||
|
||||
return jingo.render(request, 'devhub/package_addon.html',
|
||||
{'basic_form': basic_form,
|
||||
'compat_forms': compat_forms,
|
||||
'features_form': features_form})
|
||||
|
||||
|
||||
@login_required
|
||||
def package_addon_success(request, id):
|
||||
"""Return the success page for the add-on packager."""
|
||||
return jingo.render(request, 'devhub/package_addon_success.html',
|
||||
{'uuid': id})
|
||||
|
||||
|
||||
@json_view
|
||||
@login_required
|
||||
def package_addon_json(request, id):
|
||||
"""Return the URL of the packaged add-on."""
|
||||
path = packager_path(id)
|
||||
if os.path.isfile(path):
|
||||
download_url = reverse('devhub.package_addon_download', args=[id])
|
||||
return {'download_url': download_url,
|
||||
'size': round(os.path.getsize(path) / 1024, 1)}
|
||||
|
||||
|
||||
@login_required
|
||||
def package_addon_download(request, id):
|
||||
"""Serve a packaged add-on."""
|
||||
path = packager_path(id)
|
||||
if not os.path.isfile(path):
|
||||
raise http.Http404()
|
||||
|
||||
return amo.utils.HttpResponseSendFile(request, path,
|
||||
content_type='application/zip')
|
||||
|
||||
|
||||
@login_required
|
||||
@post_required
|
||||
def upload(request):
|
||||
|
|
|
@ -1890,3 +1890,129 @@ button.search-button {
|
|||
-moz-transform: scalex(-1);
|
||||
-webkit-transform: scalex(-1);
|
||||
}
|
||||
/** Add-on Packager **/
|
||||
|
||||
#packager fieldset {
|
||||
border: none;
|
||||
}
|
||||
|
||||
#packager li {
|
||||
border-bottom: 1px dotted #a5c9d5;
|
||||
padding: 0.5em 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#packager li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
#packager li:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
#packager li label:not(.checkbox-label) {
|
||||
width: 10em;
|
||||
}
|
||||
#packager .platforms li label {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#packager li label,
|
||||
#packager li .builder_input {
|
||||
float: left;
|
||||
}
|
||||
|
||||
#packager textarea {
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
#packager li .builder_input {
|
||||
padding: 0.2em;
|
||||
padding-bottom: 0.1em;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
#packager .platforms div {
|
||||
float:left;
|
||||
width:30%;
|
||||
margin-right:2em;
|
||||
margin-bottom:1.5em;
|
||||
}
|
||||
#packager .platforms ul {
|
||||
margin-top:0.5em;
|
||||
}
|
||||
|
||||
#packager .supportedapps {
|
||||
clear:both;
|
||||
margin-top:2em;
|
||||
}
|
||||
|
||||
#packager .compat_form label {
|
||||
display:block;
|
||||
width:10em;
|
||||
float:left;
|
||||
line-height:1.75em;
|
||||
}
|
||||
|
||||
#packager .compat_form span {
|
||||
font-weight: bold;
|
||||
margin-right: 2em;
|
||||
color:#666;
|
||||
}
|
||||
|
||||
#packager .compat_form select {
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
#packager-download {
|
||||
padding: 30px 0;
|
||||
}
|
||||
|
||||
#packager-download span {
|
||||
background: #fff;
|
||||
border: 1px solid #eee;
|
||||
border-bottom-width: 3px;
|
||||
border-top: 0;
|
||||
border-radius: 5px;
|
||||
color: #888;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
#packager-download a {
|
||||
background: #64A61C;
|
||||
background: -moz-linear-gradient(center top , #84C63C 0%, #489615 100%) repeat scroll 0 0 transparent;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 3px rgba(0, 0, 0, 0.1), 0 -4px rgba(0, 0, 0, 0.1) inset;
|
||||
color: #fff;
|
||||
display: block;
|
||||
font-size: 1.3em;
|
||||
margin: 2px auto;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
width: 230px;
|
||||
}
|
||||
|
||||
#packager-download a:hover {
|
||||
box-shadow: 0 3px rgba(0, 0, 0, 0.1), 0 -6px rgba(0, 0, 0, 0.1) inset;
|
||||
margin: 0 auto 2px;
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
#packager-download a:active {
|
||||
box-shadow: 0 3px rgba(0, 0, 0, 0.1), 0 -2px rgba(0, 0, 0, 0.1) inset;
|
||||
margin: 4px auto 2px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
#packager-download small {
|
||||
font-family: georgia, "Bitstream Charter", serif;
|
||||
font-size: 0.7em;
|
||||
font-style: italic;
|
||||
margin: 0 3px;
|
||||
text-align: left;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
$(document).ready(function() {
|
||||
|
||||
$('#packager-download').live('download', function(e) {
|
||||
var el = $(this),
|
||||
url = el.attr('data-downloadurl');
|
||||
|
||||
function fetch_download() {
|
||||
$.getJSON(
|
||||
url,
|
||||
function(json) {
|
||||
if(json !== null && 'download_url' in json) {
|
||||
el.html(format('<a href="{url}">{text}' +
|
||||
'<small>{size}' + gettext('kb') +
|
||||
'</small></a>',
|
||||
{url: json['download_url'],
|
||||
text: gettext('Download ZIP'),
|
||||
size: json['size']}));
|
||||
} else {
|
||||
// Pause before polling again.
|
||||
setTimeout(fetch_download, 2000);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fetch_download();
|
||||
|
||||
});
|
||||
|
||||
$('#packager-download').trigger('download');
|
||||
|
||||
});
|
|
@ -578,6 +578,7 @@ MINIFY_BUNDLES = {
|
|||
'js/zamboni/upload.js',
|
||||
'js/zamboni/devhub.js',
|
||||
'js/zamboni/validator.js',
|
||||
'js/zamboni/packager.js',
|
||||
),
|
||||
'zamboni/editors': (
|
||||
'js/zamboni/editors.js',
|
||||
|
@ -677,6 +678,7 @@ ADDON_ICONS_PATH = UPLOADS_PATH + '/addon_icons'
|
|||
COLLECTIONS_ICON_PATH = UPLOADS_PATH + '/collection_icons'
|
||||
PREVIEWS_PATH = UPLOADS_PATH + '/previews'
|
||||
USERPICS_PATH = UPLOADS_PATH + '/userpics'
|
||||
PACKAGER_PATH = os.path.join(TMP_PATH, 'packager')
|
||||
ADDON_ICONS_DEFAULT_PATH = os.path.join(MEDIA_ROOT, 'img/addon-icons')
|
||||
|
||||
PREVIEW_THUMBNAIL_PATH = (PREVIEWS_PATH + '/thumbs/%s/%d.png')
|
||||
|
@ -781,6 +783,7 @@ CELERY_IMPORTS = ('django_arecibo.tasks',)
|
|||
# We have separate celeryds for processing devhub & images as fast as possible.
|
||||
CELERY_ROUTES = {
|
||||
'devhub.tasks.validator': {'queue': 'devhub'},
|
||||
'devhub.tasks.packager': {'queue': 'devhub'},
|
||||
'bandwagon.tasks.resize_icon': {'queue': 'images'},
|
||||
'users.tasks.resize_photo': {'queue': 'images'},
|
||||
'users.tasks.delete_photo': {'queue': 'images'},
|
||||
|
|
|
@ -35,6 +35,7 @@ UPLOADS_PATH = _polite_tmpdir()
|
|||
MIRROR_STAGE_PATH = _polite_tmpdir()
|
||||
TMP_PATH = _polite_tmpdir()
|
||||
COLLECTIONS_ICON_PATH = _polite_tmpdir()
|
||||
PACKAGER_PATH = _polite_tmpdir()
|
||||
|
||||
# Turn off search engine indexing.
|
||||
USE_ELASTIC = False
|
||||
|
|
Загрузка…
Ссылка в новой задаче