This commit is contained in:
Matt Basta 2011-05-25 16:44:34 -07:00
Родитель 1b909de3a9
Коммит 6be97c5d87
16 изменённых файлов: 806 добавлений и 11 удалений

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

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