Added ability to upload packaged app (bug 777138)

This commit is contained in:
Rob Hudson 2012-08-13 16:47:15 -07:00
Родитель 53cb2a5e27
Коммит 265daa9835
29 изменённых файлов: 900 добавлений и 629 удалений

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

@ -403,6 +403,10 @@ class AMOPaths(object):
return os.path.join(settings.ROOT,
'mkt/developers/tests/addons/mozball-128.png')
def packaged_app_path(self, name):
return os.path.join(
settings.ROOT, 'mkt/submit/tests/packaged/%s' % name)
def close_to_now(dt):
"""

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

@ -195,15 +195,22 @@ class WebAppParser(object):
return ex
def parse(self, fileorpath, addon=None):
f = get_file(fileorpath)
data = f.read()
path = get_filepath(fileorpath)
if zipfile.is_zipfile(path):
zf = SafeUnzip(path)
zf.is_valid() # Raises forms.ValidationError if problems.
data = zf.extract_path('manifest.webapp')
else:
file_ = get_file(fileorpath)
data = file_.read()
file_.close()
enc_guess = chardet.detect(data)
data = strip_bom(data)
try:
data = json.loads(data.decode(enc_guess['encoding']))
except (ValueError, UnicodeDecodeError), exc:
msg = 'Error parsing webapp %r (encoding: %r %.2f%% sure): %s: %s'
log.error(msg % (f.name, enc_guess['encoding'],
log.error(msg % (fileorpath, enc_guess['encoding'],
enc_guess['confidence'] * 100.0,
exc.__class__.__name__, exc))
raise forms.ValidationError(

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

@ -787,6 +787,7 @@ MINIFY_BUNDLES = {
),
'zamboni/devhub': (
'js/zamboni/truncation.js',
'js/common/upload-base.js',
'js/common/upload-addon.js',
'js/common/upload-image.js',
'js/impala/formset.js',

23
media/css/mkt/submit.less Normal file
Просмотреть файл

@ -0,0 +1,23 @@
@import 'lib';
#submit-choose {
.island {
.border-box();
float: left;
width: 48%;
+ .island {
float: right;
}
}
ul {
color: @medium-gray;
list-style: disc;
margin-left: 15px;
}
p {
text-align: center;
padding-bottom: 0;
}
a {
margin-bottom: -5px;
}
}

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

@ -1,110 +1,3 @@
/* This abstracts the uploading of all files. Currently, it's only
* extended by addonUploader(). Eventually imageUploader() should as well */
(function($) {
var instance_id = 0,
boundary = "BoUnDaRyStRiNg";
function getErrors(results) {
return results.errors;
}
var settings = {'filetypes': [], 'getErrors': getErrors, 'cancel': $()};
$.fn.fileUploader = function( options ) {
return $(this).each(function(){
var $upload_field = $(this),
formData = false,
$form = $upload_field.closest('form'),
errors = false,
aborted = false;
if (options) {
$.extend( settings, options );
}
$upload_field.bind({"change": uploaderStart});
$(settings['cancel']).click(_pd(function(){
$upload_field.trigger('upload_action_abort');
}));
function uploaderStart(e) {
if($upload_field[0].files.length == 0) {
return;
}
var domfile = $upload_field[0].files[0],
url = $upload_field.attr('data-upload-url'),
csrf = $("input[name=csrfmiddlewaretoken]").val(),
file = {'name': domfile.name || domfile.fileName,
'size': domfile.size,
'type': domfile.type};
formData = new z.FormData();
aborted = false;
$upload_field.trigger("upload_start", [file]);
/* Disable uploading while something is uploading */
$upload_field.attr('disabled', true);
$upload_field.parent().find('a').addClass("disabled");
$upload_field.bind("reenable_uploader", function(e) {
$upload_field.attr('disabled', false);
$upload_field.parent().find('a').removeClass("disabled");
});
var exts = new RegExp("\\\.("+settings['filetypes'].join('|')+")$", "i");
if(!file.name.match(exts)) {
errors = [gettext("The filetype you uploaded isn't recognized.")];
$upload_field.trigger("upload_errors", [file, errors]);
$upload_field.trigger("upload_finished", [file]);
return;
}
// We should be good to go!
formData.open("POST", url, true);
formData.append("csrfmiddlewaretoken", csrf);
if(options.appendFormData) {
options.appendFormData(formData);
}
if(domfile instanceof File) { // Needed b/c of tests.
formData.append("upload", domfile);
}
$upload_field.unbind("upload_action_abort").bind("upload_action_abort", function() {
aborted = true;
formData.xhr.abort();
errors = [gettext("You cancelled the upload.")];
$upload_field.trigger("upload_errors", [file, errors]);
$upload_field.trigger("upload_finished", [file]);
});
formData.xhr.upload.addEventListener("progress", function(e) {
if (e.lengthComputable) {
var pct = Math.round((e.loaded * 100) / e.total);
$upload_field.trigger("upload_progress", [file, pct]);
}
}, false);
formData.xhr.onreadystatechange = function(e){
$upload_field.trigger("upload_onreadystatechange",
[file, formData.xhr, aborted]);
};
formData.send();
}
});
}
})(jQuery);
/*
* addonUploader()
* Extends fileUploader()

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

@ -0,0 +1,105 @@
/* This abstracts the uploading of all files. Currently, it's only
* extended by addonUploader(). Eventually imageUploader() should as well */
(function($) {
var instance_id = 0,
boundary = "BoUnDaRyStRiNg";
function getErrors(results) {
return results.errors;
}
var settings = {'filetypes': [], 'getErrors': getErrors, 'cancel': $()};
$.fn.fileUploader = function( options ) {
return $(this).each(function(){
var $upload_field = $(this),
formData = false,
$form = $upload_field.closest('form'),
errors = false,
aborted = false;
if (options) {
$.extend( settings, options );
}
$upload_field.bind({"change": uploaderStart});
$(settings['cancel']).click(_pd(function(){
$upload_field.trigger('upload_action_abort');
}));
function uploaderStart(e) {
if($upload_field[0].files.length == 0) {
return;
}
var domfile = $upload_field[0].files[0],
url = $upload_field.attr('data-upload-url'),
csrf = $("input[name=csrfmiddlewaretoken]").val(),
file = {'name': domfile.name || domfile.fileName,
'size': domfile.size,
'type': domfile.type};
formData = new z.FormData();
aborted = false;
$upload_field.trigger("upload_start", [file]);
/* Disable uploading while something is uploading */
$upload_field.attr('disabled', true);
$upload_field.parent().find('a').addClass("disabled");
$upload_field.bind("reenable_uploader", function(e) {
$upload_field.attr('disabled', false);
$upload_field.parent().find('a').removeClass("disabled");
});
var exts = new RegExp("\\\.("+settings['filetypes'].join('|')+")$", "i");
if(!file.name.match(exts)) {
errors = [gettext("The filetype you uploaded isn't recognized.")];
$upload_field.trigger("upload_errors", [file, errors]);
$upload_field.trigger("upload_finished", [file]);
return;
}
// We should be good to go!
formData.open("POST", url, true);
formData.append("csrfmiddlewaretoken", csrf);
if(options.appendFormData) {
options.appendFormData(formData);
}
if(domfile instanceof File) { // Needed b/c of tests.
formData.append("upload", domfile);
}
$upload_field.unbind("upload_action_abort").bind("upload_action_abort", function() {
aborted = true;
formData.xhr.abort();
errors = [gettext("You cancelled the upload.")];
$upload_field.trigger("upload_errors", [file, errors]);
$upload_field.trigger("upload_finished", [file]);
});
formData.xhr.upload.addEventListener("progress", function(e) {
if (e.lengthComputable) {
var pct = Math.round((e.loaded * 100) / e.total);
$upload_field.trigger("upload_progress", [file, pct]);
}
}, false);
formData.xhr.onreadystatechange = function(e){
$upload_field.trigger("upload_onreadystatechange",
[file, formData.xhr, aborted]);
};
formData.send();
}
});
}
})(jQuery);

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

@ -0,0 +1,326 @@
/*
* packagedAppUploader()
* Extends fileUploader()
* Also, this can only be used once per page. Or you'll have lots of issues with closures and scope :)
*/
(function($) {
/* Normalize results */
function getErrors(results) {
var errors = [];
if (results.validation.messages) {
$.each(results.validation.messages, function(i, v) {
if (v.type == 'error') {
errors.push(v.message);
}
});
}
return errors;
}
$.fn.packagedAppUploader = function( options ) {
var settings = {'filetypes': ['zip'], 'getErrors': getErrors, 'cancel': $()};
if (options) {
$.extend( settings, options );
}
function parseErrorsFromJson(response) {
var json, errors = [];
try {
json = JSON.parse(response);
} catch (err) {
errors = [gettext('There was a problem contacting the server.')];
}
if (!errors.length) {
errors = settings['getErrors'](json);
}
return {
errors: errors,
json: json
}
}
return $(this).each(function() {
var $upload_field = $(this),
file = {};
/* Add some UI */
var ui_parent = $('<div>', {'class': 'invisible-upload prominent cta', 'id': 'upload-file-widget'}),
ui_link = $('<a>', {'class': 'button prominent', 'href': '#', 'text': gettext('Select a file...')}),
ui_details = $('<div>', {'class': 'upload-details', 'text': gettext('Your packaged app should end with .zip')});
$upload_field.attr('disabled', false);
$upload_field.wrap(ui_parent);
$upload_field.before(ui_link);
$upload_field.parent().after(ui_details);
if (!z.capabilities.fileAPI) {
$('.invisible-upload').addClass('legacy');
}
/* Get things started */
var upload_box, upload_title, upload_progress_outside, upload_progress_inside,
upload_status, upload_results, upload_status_percent, upload_status_progress,
upload_status_cancel;
$upload_field.fileUploader(settings);
function textSize(bytes) {
// Based on code by Cary Dunn (http://bit.ly/d8qbWc).
var s = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
if (bytes === 0) {
return bytes + ' ' + s[1];
}
var e = Math.floor( Math.log(bytes) / Math.log(1024) );
return (bytes / Math.pow(1024, Math.floor(e))).toFixed(2)+' '+s[e];
}
function updateStatus(percentage, size) {
if (percentage) {
upload_status.show();
p = Math.round(percentage);
size = (p / 100) * size;
// L10n: {0} is the percent of the file that has been uploaded.
upload_status_percent.text(format(gettext('{0}% complete'), [p]));
// L10n: "{bytes uploaded} of {total filesize}".
upload_status_progress.text(format(gettext('{0} of {1}'),
[textSize(size), textSize(file.size)]));
}
}
/* Bind the events */
$upload_field.bind('upload_start', function(e, _file) {
file = _file;
/* Remove old upload box */
if (upload_box) {
upload_box.remove();
}
/* Remove old errors */
$upload_field.closest('form').find('.errorlist').remove();
/* Don't allow submitting */
$('.addon-upload-dependant').attr('disabled', true);
/* Create elements */
upload_title = $('<strong>', {'id': 'upload-status-text'});
upload_progress_outside = $('<div>', {'id': 'upload-status-bar'});
upload_progress_inside = $('<div>').css('width', 0);
upload_status = $('<div>', {'id': 'uploadstatus'}).hide();
upload_status_percent = $('<span>');
upload_status_progress = $('<span>');
upload_status_cancel_a = $('<a>', {'href': '#', 'text': gettext('Cancel')});
upload_status_cancel = $('<span> &middot; </span>');
upload_results = $('<div>', {'id': 'upload-status-results'});
upload_box = $('<div>', {'class': 'upload-status ajax-loading'}).hide();
/* Set up structure */
upload_box.append(upload_title);
upload_progress_outside.append(upload_progress_inside);
upload_box.append(upload_progress_outside);
upload_status.append(upload_status_percent);
upload_status.append(' <span> &middot; </span> ');
upload_status.append(upload_status_progress);
upload_status.append(upload_status_cancel);
upload_status_cancel.append(upload_status_cancel_a);
upload_box.append(upload_status);
upload_box.append(upload_results);
/* Add to the dom and clean up upload_field */
ui_details.after(upload_box);
/* It's showtime! */
upload_title.html(format(gettext('Uploading {0}'), [escape_(file.name)]));
upload_box.show();
upload_box.addClass('ajax-loading');
upload_status_cancel_a.click(_pd(function() {
$upload_field.trigger('upload_action_abort');
}));
});
$upload_field.bind('upload_progress', function(e, file, pct) {
upload_progress_inside.animate({'width': pct + '%'},
{duration: 300, step:function(i) { updateStatus(i, file.size); } });
});
$upload_field.bind('upload_errors', function(e, file, errors, results) {
var all_errors = $.extend([], errors); // be nice to other handlers
upload_progress_inside.stop().css({'width': '100%'});
$upload_field.val('').attr('disabled', false);
$upload_field.trigger('reenable_uploader');
upload_title.html(format(gettext('Error with {0}'), [escape_(file.name)]));
upload_progress_outside.attr('class', 'bar-fail');
upload_progress_inside.fadeOut();
var error_message = format(ngettext(
'Your app failed validation with {0} error.',
'Your app failed validation with {0} errors.',
all_errors.length), [all_errors.length]);
$('<strong>').text(error_message).appendTo(upload_results);
var errors_ul = $('<ul>', {'id': 'upload_errors'});
$.each(all_errors.splice(0, 5), function(i, error) {
errors_ul.append($('<li>', {'html': error }));
});
if (all_errors.length > 0) {
var message = format(ngettext('&hellip;and {0} more',
'&hellip;and {0} more',
all_errors.length), [all_errors.length]);
errors_ul.append($('<li>', {'html': message}));
}
upload_results.append(errors_ul).addClass('status-fail');
if (results && results.full_report_url) {
// There might not be a link to the full report
// if we get an early error like unsupported type.
upload_results.append($('<a>', {'href': results.full_report_url,
'class': 'view-more',
'target': '_blank',
'text': gettext('See full validation report')}));
}
});
$upload_field.bind('upload_finished', function(e, file, results) {
upload_box.removeClass('ajax-loading');
upload_status_cancel.remove();
});
$upload_field.bind('upload_success', function(e, file, results) {
upload_title.html(format(gettext('Validating {0}'), [escape_(file.name)]));
var animateArgs = {duration: 300, step:function(i) { updateStatus(i, file.size); }, complete: function() {
$upload_field.trigger('upload_success_results', [file, results]);
}};
upload_progress_inside.animate({'width': '100%'}, animateArgs);
});
$upload_field.bind('upload_onreadystatechange', function(e, file, xhr, aborted) {
var errors = [],
$form = $upload_field.closest('form'),
json = {},
errOb;
if (xhr.readyState == 4 && xhr.responseText &&
(xhr.status == 200 ||
xhr.status == 304 ||
xhr.status == 400)) {
errOb = parseErrorsFromJson(xhr.responseText);
errors = errOb.errors;
json = errOb.json;
if (errors.length > 0) {
$upload_field.trigger('upload_errors', [file, errors, json]);
} else {
$form.find('input#id_upload').val(json.upload);
$upload_field.trigger('upload_success', [file, json]);
$upload_field.trigger('upload_progress', [file, 100]);
}
$upload_field.trigger('upload_finished', [file]);
} else if (xhr.readyState == 4 && !aborted) {
// L10n: first argument is an HTTP status code
errors = [format(gettext('Received an empty response from the server; status: {0}'),
[xhr.status])];
$upload_field.trigger('upload_errors', [file, errors]);
}
});
$upload_field.bind('upload_success_results', function(e, file, results) {
if (results.error) {
// This shouldn't happen. But it might.
var error = gettext('Unexpected server error while validating.');
$upload_field.trigger('upload_errors', [file, [error]]);
return;
}
// Validation results? If not, fetch the json again.
if (! results.validation) {
upload_progress_outside.attr('class', 'progress-idle');
// Not loaded yet. Try again!
setTimeout(function() {
$.ajax({
url: results.url,
dataType: 'json',
success: function(r) {
$upload_field.trigger('upload_success_results', [file, r]);
},
error: function(xhr, textStatus, errorThrown) {
var errOb = parseErrorsFromJson(xhr.responseText);
$upload_field.trigger('upload_errors', [file, errOb.errors, errOb.json]);
$upload_field.trigger('upload_finished', [file]);
}
});
}, 1000);
} else {
var errors = getErrors(results),
v = results.validation;
if (errors.length > 0) {
$upload_field.trigger('upload_errors', [file, errors, results]);
return;
}
$upload_field.val('').attr('disabled', false);
/* Allow submitting */
$('.addon-upload-dependant').attr('disabled', false);
upload_title.html(format(gettext('Finished validating {0}'), [escape_(file.name)]));
var message = '';
var warnings = v.warnings + v.notices;
if (warnings > 0) {
message = format(ngettext(
'Your app passed validation with no errors and {0} warning.',
'Your app passed validation with no errors and {0} warnings.',
warnings), [warnings]);
} else {
message = gettext('Your app passed validation with no errors or warnings.');
}
upload_progress_outside.attr('class', 'bar-success');
upload_progress_inside.fadeOut();
$upload_field.trigger('reenable_uploader');
upload_results.addClass('status-pass');
$('<strong>').text(message).appendTo(upload_results);
if (results.full_report_url) {
// There might not be a link to the full report
// if we get an early error like unsupported type.
upload_results.append($('<a>', {'href': results.full_report_url,
'target': '_blank',
'text': gettext('See full validation report')}));
}
}
});
});
};
})(jQuery);

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

@ -40,17 +40,9 @@ $(document).ready(function() {
});
// Add-on uploader
if($('#upload-addon').length) {
if($('#upload-app').length) {
var opt = {'cancel': $('.upload-file-cancel') };
if($('#addon-compat-upload').length) {
opt.appendFormData = function(formData) {
formData.append('app_id',
$('#id_application option:selected').val());
formData.append('version_id',
$('#id_app_version option:selected').val());
};
}
$('#upload-addon').addonUploader(opt);
$('#upload-app').packagedAppUploader(opt);
}
var $webapp_url = $('#upload-webapp-url');

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

@ -0,0 +1 @@
INSERT INTO waffle_switch_mkt (name, active, note) VALUES ('allow-packaged-app-uploads', 0, 'Enables ability to upload packaged apps during app submission.');

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

@ -56,6 +56,7 @@ CSS = {
'css/devreg/submit-manifest.less',
'css/devreg/submit-details.less',
'css/devreg/validation.less',
'css/mkt/submit.less',
# Developer Log In / Registration.
'css/devreg/login.less',
@ -176,6 +177,8 @@ JS = {
# Developer Hub-specific scripts.
'js/zamboni/truncation.js',
'js/common/upload-base.js',
'js/common/upload-packaged-app.js',
'js/common/upload-image.js',
# New stuff.

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

@ -3,7 +3,7 @@ from tower import ugettext_lazy as _
APP_STEPS = [
('terms', _('Developer Agreement')),
('manifest', _('App Manifest')),
('manifest', _('Upload')),
('details', _('Details')),
('payments', _('Payments')),
('done', _('Finished!')),

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

@ -9,30 +9,31 @@ import traceback
import urllib2
import urlparse
import uuid
import zipfile
from datetime import date
from django import forms
from django.conf import settings
from django.core.files.storage import default_storage as storage
from django.core.management import call_command
from django.utils.http import urlencode
import validator.constants as validator_constants
from appvalidator import validate_app, validate_packaged_app
from celeryutils import task
from django_statsd.clients import statsd
from PIL import Image
from tower import ugettext as _
from addons.models import Addon
import amo
from addons.models import Addon
from amo.decorators import set_modified_on, write
from amo.helpers import absolutify
from amo.utils import remove_icons, resize_image, send_mail_jinja, strip_bom
from applications.management.commands import dump_apps
from applications.models import Application, AppVersion
from files.models import FileUpload, File, FileValidation
from files.utils import SafeUnzip
from mkt.webapps.models import AddonExcludedRegion, Webapp
log = logging.getLogger('z.mkt.developers.task')
@ -41,47 +42,14 @@ log = logging.getLogger('z.mkt.developers.task')
def validator(upload_id, **kw):
if not settings.VALIDATE_ADDONS:
return None
log.info('VALIDATING: %s' % upload_id)
upload = FileUpload.objects.get(pk=upload_id)
force_validation_type = None
if upload.is_webapp:
force_validation_type = validator_constants.PACKAGE_WEBAPP
log.info(u'[FileUpload:%s] Validating app.' % upload_id)
try:
result = run_validator(upload.path,
force_validation_type=force_validation_type)
upload.validation = result
upload.save() # We want to hit the custom save().
except:
# Store the error with the FileUpload job, then raise
# it for normal logging.
tb = traceback.format_exception(*sys.exc_info())
upload.update(task_error=''.join(tb))
raise
@task
@write
def compatibility_check(upload_id, app_guid, appversion_str, **kw):
if not settings.VALIDATE_ADDONS:
return None
log.info('COMPAT CHECK for upload %s / app %s version %s'
% (upload_id, app_guid, appversion_str))
upload = FileUpload.objects.get(pk=upload_id)
app = Application.objects.get(guid=app_guid)
appver = AppVersion.objects.get(application=app, version=appversion_str)
upload = FileUpload.objects.get(pk=upload_id)
except FileUpload.DoesNotExist:
log.info(u'[FileUpload:%s] Does not exist.' % upload_id)
return
try:
result = run_validator(upload.path,
for_appversions={app_guid: [appversion_str]},
test_all_tiers=True,
# Ensure we only check compatibility
# against this one specific version:
overrides={'targetapp_minVersion':
{app_guid: appversion_str},
'targetapp_maxVersion':
{app_guid: appversion_str}})
upload.validation = result
upload.compat_with_app = app
upload.compat_with_appver = appver
upload.validation = run_validator(upload.path)
upload.save() # We want to hit the custom save().
except:
# Store the error with the FileUpload job, then raise
@ -96,51 +64,20 @@ def compatibility_check(upload_id, app_guid, appversion_str, **kw):
def file_validator(file_id, **kw):
if not settings.VALIDATE_ADDONS:
return None
log.info('VALIDATING file: %s' % file_id)
file = File.objects.get(pk=file_id)
# Unlike upload validation, let the validator
# raise an exception if there is one.
log.info(u'[File:%s] Validating file.' % file_id)
try:
file = File.objects.get(pk=file_id)
except File.DoesNotExist:
log.info(u'[File:%s] Does not exist.' % file_id)
return
# Unlike upload validation, let the validator raise an exception if there
# is one.
result = run_validator(file.file_path)
return FileValidation.from_json(file, result)
def run_validator(file_path, for_appversions=None, test_all_tiers=False,
overrides=None, force_validation_type=None):
"""A pre-configured wrapper around the addon validator.
*file_path*
Path to addon / extension file to validate.
*for_appversions=None*
An optional dict of application versions to validate this addon
for. The key is an application GUID and its value is a list of
versions.
*test_all_tiers=False*
When False (default) the validator will not continue if it
encounters fatal errors. When True, all tests in all tiers are run.
See bug 615426 for discussion on this default.
*overrides=None*
Normally the validator gets info from install.rdf but there are a
few things we need to override. See validator for supported overrides.
Example: {'targetapp_maxVersion': {'<app guid>': '<version>'}}
*force_validation_type=None*
Set this to a value in validator.constants like PACKAGE_WEBAPP
when you need to force detection of a package. Otherwise the type
will be inferred from the filename extension.
To validate the addon for compatibility with Firefox 5 and 6,
you'd pass in::
for_appversions={amo.FIREFOX.guid: ['5.0.*', '6.0.*']}
Not all application versions will have a set of registered
compatibility tests.
"""
from validator.validate import validate_app
def run_validator(file_path):
"""A pre-configured wrapper around the app validator."""
# TODO(Kumar) remove this when validator is fixed, see bug 620503
from validator.testcases import scripting
@ -148,16 +85,20 @@ def run_validator(file_path, for_appversions=None, test_all_tiers=False,
import validator.constants
validator.constants.SPIDERMONKEY_INSTALLATION = settings.SPIDERMONKEY
apps = dump_apps.Command.JSON_PATH
if not os.path.exists(apps):
call_command('dump_apps')
if not force_validation_type:
force_validation_type = validator.constants.PACKAGE_ANY
with statsd.timer('mkt.developers.validator'):
return validate_app(
storage.open(file_path).read() if file_path else '',
market_urls=settings.VALIDATOR_IAF_URLS)
is_packaged = zipfile.is_zipfile(file_path)
if is_packaged:
log.info(u'Running `validate_packaged_app` for path: %s'
% (file_path))
with statsd.timer('mkt.developers.validate_packaged_app'):
return validate_packaged_app(file_path,
market_urls=settings.VALIDATOR_IAF_URLS,
timeout=settings.VALIDATOR_TIMEOUT)
else:
log.info(u'Running `validate_app` for path: %s' % (file_path))
with statsd.timer('mkt.developers.validate_app'):
return validate_app(storage.open(file_path).read(),
market_urls=settings.VALIDATOR_IAF_URLS)
@task
@ -226,25 +167,6 @@ def get_preview_sizes(ids, **kw):
% (addon.pk, err))
@task
@write
def convert_purified(ids, **kw):
log.info('[%s@%s] Converting fields to purified starting at id: %s...'
% (len(ids), convert_purified.rate_limit, ids[0]))
fields = ['the_reason', 'the_future']
for addon in Addon.objects.filter(pk__in=ids):
flag = False
for field in fields:
value = getattr(addon, field)
if value:
value.clean()
if (value.localized_string_clean != value.localized_string):
flag = True
if flag:
log.info('Saving addon: %s to purify fields' % addon.pk)
addon.save()
def failed_validation(*messages):
"""Return a validation object that looks like the add-on validator."""
m = []
@ -331,22 +253,35 @@ def fetch_icon(webapp, **kw):
image_string = icon_url.split('base64,')[1]
content = base64.decodestring(image_string)
else:
if not urlparse.urlparse(icon_url).scheme:
icon_url = webapp.origin + icon_url
if webapp.is_packaged:
# Get icons from package.
if icon_url.startswith('/'):
icon_url = icon_url[1:]
try:
zf = SafeUnzip(webapp.get_latest_file().file_path)
zf.is_valid()
content = zf.extract_path(icon_url)
except (KeyError, forms.ValidationError): # Not found in archive.
log.error(u'[Webapp:%s] Icon %s not found in archive'
% (webapp, icon_url))
return
else:
if not urlparse.urlparse(icon_url).scheme:
icon_url = webapp.origin + icon_url
try:
response = _fetch_content(icon_url)
except Exception, e:
log.error('Failed to fetch icon for webapp %s: %s'
% (webapp.pk, e.message))
# Set the icon type to empty.
webapp.update(icon_type='')
return
try:
response = _fetch_content(icon_url)
except Exception, e:
log.error(u'[Webapp:%s] Failed to fetch icon for webapp: %s'
% (webapp, e.message))
# Set the icon type to empty.
webapp.update(icon_type='')
return
size_error_message = _('Your icon must be less than %s bytes.')
content = get_content_and_check_size(response,
settings.MAX_ICON_UPLOAD_SIZE,
size_error_message)
size_error_message = _('Your icon must be less than %s bytes.')
content = get_content_and_check_size(response,
settings.MAX_ICON_UPLOAD_SIZE,
size_error_message)
save_icon(webapp, content)

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

@ -23,10 +23,6 @@
{{ timestamp|datetime }}</time>
</dd>
{% endif %}
{% if result_type == 'compat' %}
<dt>{{ _('Tested for compatibility against:') }}</dt>
<dd>{{ amo.APP_IDS[target_app.pk].pretty }} {{ target_version }}</dd>
{% endif %}
</dl>
</div>

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

@ -2,36 +2,12 @@ import mock
from nose.tools import eq_
import amo.tests
from addons.models import Addon
import mkt
from mkt.developers.cron import exclude_new_region, send_new_region_emails
from mkt.developers.tasks import convert_purified
from mkt.webapps.models import AddonExcludedRegion
class TestPurify(amo.tests.TestCase):
fixtures = ['base/addon_3615']
def setUp(self):
self.addon = Addon.objects.get(pk=3615)
def test_no_html(self):
self.addon.the_reason = 'foo'
self.addon.save()
last = Addon.objects.get(pk=3615).modified
convert_purified([self.addon.pk])
addon = Addon.objects.get(pk=3615)
eq_(addon.modified, last)
def test_has_html(self):
self.addon.the_reason = 'foo <script>foo</script>'
self.addon.save()
convert_purified([self.addon.pk])
addon = Addon.objects.get(pk=3615)
assert addon.the_reason.localized_string_clean
class TestSendNewRegionEmails(amo.tests.WebappTestCase):
@mock.patch('mkt.developers.cron._region_email')

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

@ -128,9 +128,20 @@ class TestValidator(amo.tests.TestCase):
error = self.get_upload().task_error
assert error.startswith('Traceback (most recent call last)'), error
@mock.patch('validator.validate.validate_app')
def test_validate_manifest(self, _mock):
@mock.patch('mkt.developers.tasks.validate_app')
@mock.patch('mkt.developers.tasks.storage.open')
def test_validate_manifest(self, _open, _mock):
self.get_upload().update(is_webapp=True)
_open.return_value = StringIO('')
_mock.return_value = '{"errors": 0}'
tasks.validator(self.upload.pk)
assert _mock.called
@mock.patch('mkt.developers.tasks.validate_packaged_app')
@mock.patch('zipfile.is_zipfile')
def test_validate_packaged_app(self, _zipfile, _mock):
self.get_upload().update(is_webapp=True)
_zipfile.return_value = True
_mock.return_value = '{"errors": 0}'
tasks.validator(self.upload.pk)
assert _mock.called
@ -347,11 +358,26 @@ class TestFetchIcon(BaseWebAppTest):
response = mock.Mock()
response.read.return_value = ''
webapp = mock.Mock()
webapp.is_packaged = False
url = 'http://foo.com/bar'
webapp.get_manifest_json.return_value = {'icons': {'128': url}}
tasks.fetch_icon(webapp)
assert url in fetch.call_args[0][0]
@mock.patch('mkt.developers.tasks.SafeUnzip')
@mock.patch('mkt.developers.tasks.save_icon')
def test_packaged_icon(self, save, zip):
response = mock.Mock()
response.read.return_value = ''
zf = mock.Mock()
zip.return_value = zf
webapp = mock.Mock()
webapp.is_packaged = True
url = '/path/to/icon.png'
webapp.get_manifest_json.return_value = {'icons': {'128': url}}
tasks.fetch_icon(webapp)
assert url[1:] in zf.extract_path.call_args[0][0]
class TestRegionEmail(amo.tests.WebappTestCase):

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

@ -2,270 +2,20 @@
import codecs
import json
import os
import shutil
import tempfile
from django.conf import settings
from django.core.files.storage import default_storage as storage
from django import forms
from django.core.files.storage import default_storage as storage
import mock
from nose.plugins.attrib import attr
from nose.tools import eq_
from pyquery import PyQuery as pq
import waffle
import amo
import amo.tests
from addons.models import Addon
from amo.tests import assert_no_validation_errors
from amo.urlresolvers import reverse
from files.helpers import copyfileobj
from files.models import File, FileUpload, FileValidation
from files.tests.test_models import UploadTest as BaseUploadTest
from files.utils import parse_addon, WebAppParser
from users.models import UserProfile
from files.utils import WebAppParser
class TestUploadValidation(BaseUploadTest):
fixtures = ['base/apps', 'base/users',
'developers/invalid-id-uploaded-xpi.json']
def setUp(self):
super(TestUploadValidation, self).setUp()
assert self.client.login(username='regular@mozilla.com',
password='password')
def test_no_html_in_messages(self):
upload = FileUpload.objects.get(name='invalid-id-20101206.xpi')
r = self.client.get(reverse('mkt.developers.upload_detail',
args=[upload.uuid, 'json']))
eq_(r.status_code, 200)
data = json.loads(r.content)
msg = data['validation']['messages'][0]
eq_(msg['message'], 'The value of &lt;em:id&gt; is invalid.')
eq_(sorted(msg['context']),
[[u'&lt;foo/&gt;'], u'&lt;em:description&gt;...'])
def test_date_on_upload(self):
upload = FileUpload.objects.get(name='invalid-id-20101206.xpi')
r = self.client.get(reverse('mkt.developers.upload_detail',
args=[upload.uuid]))
eq_(r.status_code, 200)
doc = pq(r.content)
eq_(doc('.results-intro time').text(), 'December 6, 2010')
class TestUploadErrors(BaseUploadTest):
fixtures = ('base/apps', 'base/addon_3615', 'base/users')
def setUp(self):
super(TestUploadErrors, self).setUp()
self.client.login(username='regular@mozilla.com',
password='password')
@mock.patch.object(waffle, 'flag_is_active')
def test_dupe_uuid(self, flag_is_active):
flag_is_active.return_value = True
addon = Addon.objects.get(pk=3615)
d = parse_addon(self.get_upload('extension.xpi'))
addon.update(guid=d['guid'])
dupe_xpi = self.get_upload('extension.xpi')
res = self.client.get(reverse('mkt.developers.upload_detail',
args=[dupe_xpi.uuid, 'json']))
eq_(res.status_code, 400)
data = json.loads(res.content)
eq_(data['validation']['messages'],
[{'tier': 1, 'message': 'Duplicate UUID found.',
'type': 'error'}])
eq_(data['validation']['ending_tier'], 1)
class TestFileValidation(amo.tests.TestCase):
fixtures = ['base/apps', 'base/users', 'base/platforms',
'developers/addon-validation-1']
def setUp(self):
assert self.client.login(username='del@icio.us', password='password')
self.user = UserProfile.objects.get(email='del@icio.us')
self.file_validation = FileValidation.objects.get(pk=1)
self.file = self.file_validation.file
self.addon = self.file.version.addon
self.addon.update(app_slug=self.addon.slug, type=amo.ADDON_WEBAPP)
self.url = self.addon.get_dev_url('file_validation', [self.file.id])
self.json_url = self.addon.get_dev_url('json_file_validation',
[self.file.id])
def test_app_results_page(self):
r = self.client.get(self.url, follow=True)
eq_(r.status_code, 200)
eq_(r.context['addon'].id, self.addon.id)
def test_only_dev_can_see_results(self):
self.client.logout()
assert self.client.login(username='regular@mozilla.com',
password='password')
eq_(self.client.head(self.url, follow=True).status_code, 403)
def test_only_dev_can_see_json_results(self):
self.client.logout()
assert self.client.login(username='regular@mozilla.com',
password='password')
eq_(self.client.head(self.json_url, follow=True).status_code, 403)
def test_editor_can_see_results(self):
self.client.logout()
assert self.client.login(username='editor@mozilla.com',
password='password')
eq_(self.client.head(self.url, follow=True).status_code, 200)
def test_editor_can_see_json_results(self):
self.client.logout()
assert self.client.login(username='editor@mozilla.com',
password='password')
eq_(self.client.head(self.json_url, follow=True).status_code, 200)
def test_no_html_in_messages(self):
r = self.client.post(self.json_url, follow=True)
eq_(r.status_code, 200)
data = json.loads(r.content)
msg = data['validation']['messages'][0]
eq_(msg['message'], 'The value of &lt;em:id&gt; is invalid.')
eq_(sorted(msg['context']),
[[u'&lt;foo/&gt;'], u'&lt;em:description&gt;...'])
@mock.patch('files.models.File.has_been_validated')
def test_json_results_post(self, has_been_validated):
has_been_validated.__ne__ = mock.Mock()
has_been_validated.__ne__.return_value = True
eq_(self.client.post(self.json_url).status_code, 200)
has_been_validated.__ne__.return_value = False
eq_(self.client.post(self.json_url).status_code, 200)
@mock.patch('files.models.File.has_been_validated')
def test_json_results_get(self, has_been_validated):
has_been_validated.__eq__ = mock.Mock()
has_been_validated.__eq__.return_value = True
eq_(self.client.get(self.json_url).status_code, 200)
has_been_validated.__eq__.return_value = False
eq_(self.client.get(self.json_url).status_code, 405)
#class TestValidateAddon(amo.tests.TestCase):
# fixtures = ['base/users']
#
# def setUp(self):
# super(TestValidateAddon, self).setUp()
# assert self.client.login(username='regular@mozilla.com',
# password='password')
#
# def test_login_required(self):
# self.client.logout()
# r = self.client.get(reverse('mkt.developers.validate_addon'))
# eq_(r.status_code, 302)
#
# def test_context(self):
# r = self.client.get(reverse('mkt.developers.validate_addon'))
# eq_(r.status_code, 200)
# doc = pq(r.content)
# eq_(doc('#upload-addon').attr('data-upload-url'),
# reverse('mkt.developers.standalone_upload'))
class TestValidateFile(BaseUploadTest):
fixtures = ['base/apps', 'base/users', 'base/addon_3615',
'developers/addon-file-100456', 'base/platforms']
def setUp(self):
super(TestValidateFile, self).setUp()
assert self.client.login(username='del@icio.us', password='password')
self.user = UserProfile.objects.get(email='del@icio.us')
self.file = File.objects.get(pk=100456)
# Move the file into place as if it were a real file
self.file_dir = os.path.dirname(self.file.file_path)
os.makedirs(self.file_dir)
shutil.copyfile(self.file_path('invalid-id-20101206.xpi'),
self.file.file_path)
self.addon = self.file.version.addon
self.addon.update(app_slug=self.addon.slug, type=amo.ADDON_WEBAPP)
self.url = self.addon.get_dev_url('file_validation', [self.file.id])
self.json_url = self.addon.get_dev_url('json_file_validation',
[self.file.id])
def tearDown(self):
super(TestValidateFile, self).tearDown()
if os.path.exists(self.file_dir):
shutil.rmtree(self.file_dir)
@attr('validator')
def test_lazy_validate(self):
r = self.client.post(self.json_url,
follow=True)
eq_(r.status_code, 200)
data = json.loads(r.content)
assert_no_validation_errors(data)
msg = data['validation']['messages'][0]
eq_(msg['message'], 'JSON Parse Error')
def test_time(self):
r = self.client.post(self.url, follow=True)
doc = pq(r.content)
assert doc('time').text()
@mock.patch.object(settings, 'EXPOSE_VALIDATOR_TRACEBACKS', False)
@mock.patch('mkt.developers.tasks.run_validator')
def test_validator_errors(self, v):
v.side_effect = ValueError('catastrophic failure in amo-validator')
r = self.client.post(self.json_url, follow=True)
eq_(r.status_code, 200)
data = json.loads(r.content)
eq_(data['validation'], '')
eq_(data['error'].strip(),
'ValueError: catastrophic failure in amo-validator')
@mock.patch('mkt.developers.tasks.run_validator')
def test_linkify_validation_messages(self, v):
v.return_value = json.dumps({
"errors": 0,
"success": True,
"warnings": 1,
"notices": 0,
"message_tree": {},
"messages": [{
"context": ["<code>", None],
"description": [
"Something something, see https://bugzilla.mozilla.org/"],
"column": 0,
"line": 1,
"file": "chrome/content/down.html",
"tier": 2,
"message": "Some warning",
"type": "warning",
"id": [],
"uid": "bb9948b604b111e09dfdc42c0301fe38"
}],
"metadata": {}
})
r = self.client.post(self.json_url, follow=True)
eq_(r.status_code, 200)
data = json.loads(r.content)
assert_no_validation_errors(data)
doc = pq(data['validation']['messages'][0]['description'][0])
eq_(doc('a').text(), 'https://bugzilla.mozilla.org/')
@mock.patch.object(settings, 'EXPOSE_VALIDATOR_TRACEBACKS', False)
@mock.patch('mkt.developers.tasks.run_validator')
def test_hide_validation_traceback(self, run_validator):
run_validator.side_effect = RuntimeError('simulated task error')
r = self.client.post(self.json_url, follow=True)
eq_(r.status_code, 200)
data = json.loads(r.content)
eq_(data['validation'], '')
eq_(data['error'], 'RuntimeError: simulated task error')
class TestWebApps(amo.tests.TestCase):
class TestWebApps(amo.tests.TestCase, amo.tests.AMOPaths):
def setUp(self):
self.webapp_path = tempfile.mktemp(suffix='.webapp')
@ -300,6 +50,30 @@ class TestWebApps(amo.tests.TestCase):
eq_(wp['version'], '1.0')
eq_(wp['default_locale'], 'en-US')
def test_parse_packaged(self):
wp = WebAppParser().parse(self.packaged_app_path('mozball.zip'))
eq_(wp['guid'], None)
eq_(wp['type'], amo.ADDON_WEBAPP)
eq_(wp['name']['en-US'], u'Packaged MozillaBall ょ')
eq_(wp['summary']['en-US'], u'Exciting Open Web development action!')
eq_(wp['summary']['es'],
u'¡Acción abierta emocionante del desarrollo del Web!')
eq_(wp['summary']['it'],
u'Azione aperta emozionante di sviluppo di fotoricettore!')
eq_(wp['version'], '1.0')
eq_(wp['default_locale'], 'en-US')
def test_parse_packaged_BOM(self):
wp = WebAppParser().parse(self.packaged_app_path('mozBOM.zip'))
eq_(wp['guid'], None)
eq_(wp['type'], amo.ADDON_WEBAPP)
eq_(wp['name']['en-US'], u'Packaged MozBOM ょ')
eq_(wp['summary']['en-US'], u'Exciting BOM action!')
eq_(wp['summary']['es'], u'¡Acción BOM!')
eq_(wp['summary']['it'], u'Azione BOM!')
eq_(wp['version'], '1.0')
eq_(wp['default_locale'], 'en-US')
def test_no_locales(self):
wp = WebAppParser().parse(self.webapp(dict(name='foo', version='1.0',
description='summary')))

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

@ -4,8 +4,8 @@ import sys
import traceback
from django import http
from django.conf import settings
from django import forms as django_forms
from django.conf import settings
from django.db import models, transaction
from django.forms.models import model_to_dict
from django.shortcuts import get_object_or_404, redirect
@ -14,35 +14,34 @@ from django.views.decorators.csrf import csrf_view_exempt
import commonware.log
import jingo
from session_csrf import anonymous_csrf
from tower import ugettext_lazy as _lazy, ugettext as _
import waffle
from session_csrf import anonymous_csrf
from tower import ugettext as _, ugettext_lazy as _lazy
from waffle.decorators import waffle_switch
from access import acl
from applications.models import Application, AppVersion
import amo
import amo.utils
from amo import messages
from amo.decorators import json_view, login_required, post_required, write
from amo.helpers import absolutify, urlparams
from amo.utils import escape_all
from amo.urlresolvers import reverse
import paypal
from access import acl
from addons import forms as addon_forms
from addons.decorators import can_become_premium
from addons.forms import DeviceTypeForm
from addons.models import Addon, AddonUser
from addons.views import BaseFilter
from amo import messages
from amo.decorators import json_view, login_required, post_required, write
from amo.helpers import absolutify, urlparams
from amo.urlresolvers import reverse
from amo.utils import escape_all
from devhub.models import AppLog
from files.models import File, FileUpload
from files.utils import parse_addon
from lib.cef_loggers import inapp_cef
from lib.pay_server import client
from market.models import AddonPaymentData, AddonPremium, Refund
from paypal import PaypalError
from paypal.check import Check
from paypal.decorators import handle_paypal_error
import paypal
from paypal import PaypalError
from stats.models import Contribution
from translations.models import delete_translation
from users.models import UserProfile
@ -707,17 +706,13 @@ def validate_addon(request):
def upload(request, addon_slug=None, is_standalone=False):
filedata = request.FILES['upload']
fu = FileUpload.from_post(filedata, filedata.name, filedata.size)
log.info('FileUpload created: %s' % fu.pk)
fu = FileUpload.from_post(filedata, filedata.name, filedata.size,
is_webapp=True)
log.info('Packaged App FileUpload created: %s' % fu.pk)
if request.user.is_authenticated():
fu.user = request.amo_user
fu.save()
if request.POST.get('app_id') and request.POST.get('version_id'):
app = get_object_or_404(Application, pk=request.POST['app_id'])
ver = get_object_or_404(AppVersion, pk=request.POST['version_id'])
tasks.compatibility_check.delay(fu.pk, app.guid, ver.version)
else:
tasks.validator.delay(fu.pk)
tasks.validator.delay(fu.pk)
if addon_slug:
return redirect('mkt.developers.upload_detail_for_addon',
addon_slug, fu.pk)
@ -863,35 +858,18 @@ def json_upload_detail(request, upload, addon_slug=None):
if addon_slug:
addon = get_object_or_404(Addon, slug=addon_slug)
result = upload_validation_context(request, upload, addon=addon)
plat_exclude = []
if result['validation']:
if result['validation']['errors'] == 0:
try:
pkg = parse_addon(upload, addon=addon)
app_ids = set([a.id for a in pkg.get('apps', [])])
supported_platforms = []
for app in (amo.MOBILE, amo.ANDROID):
if app.id in app_ids:
supported_platforms.extend(amo.MOBILE_PLATFORMS.keys())
app_ids.remove(app.id)
if len(app_ids):
# Targets any other non-mobile app:
supported_platforms.extend(amo.DESKTOP_PLATFORMS.keys())
s = amo.SUPPORTED_PLATFORMS.keys()
plat_exclude = set(s) - set(supported_platforms)
plat_exclude = [str(p) for p in plat_exclude]
parse_addon(upload, addon=addon)
except django_forms.ValidationError, exc:
m = []
for msg in exc.messages:
# Simulate a validation error so the UI displays
# it as such
m.append({'type': 'error',
'message': msg, 'tier': 1})
v = make_validation_result(
dict(error='', validation=dict(messages=m)))
# Simulate a validation error so the UI displays it.
m.append({'type': 'error', 'message': msg, 'tier': 1})
v = make_validation_result(dict(error='',
validation=dict(messages=m)))
return json_view.error(v)
result['platforms_to_exclude'] = plat_exclude
return result
@ -901,6 +879,7 @@ def upload_validation_context(request, upload, addon_slug=None, addon=None,
addon = get_object_or_404(Addon, slug=addon_slug)
if not settings.VALIDATE_ADDONS:
upload.task_error = ''
upload.is_webapp = True
upload.validation = json.dumps({'errors': 0, 'messages': [],
'metadata': {}, 'notices': 0,
'warnings': 0})

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

@ -1,5 +1,7 @@
import functools
from django.shortcuts import redirect
def submit_step(outer_step):
"""Wraps the function with a decorator that bounces to the right step."""
@ -20,3 +22,18 @@ def submit_step(outer_step):
wrapper.submitting = True
return wrapper
return decorator
def read_dev_agreement_required(f):
"""
Decorator that checks if the user has read the dev agreement, redirecting
if not.
"""
def decorator(f):
@functools.wraps(f)
def wrapper(request, *args, **kw):
if not request.amo_user.read_dev_agreement:
return redirect('submit.app')
return f(request, *args, **kw)
return wrapper
return decorator(f)

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

@ -0,0 +1,44 @@
{% extends 'developers/base_impala.html' %}
{% set title = _('Submit an App') %}
{% block title %}{{ hub_page_title(title) }}{% endblock %}
{% block content %}
{# Needs real text. See bug 783418 #}
{{ hub_breadcrumbs(items=[(None, _('Submit App'))]) }}
<h1>{{ _('Submit an App') }}</h1>
{{ progress(request, addon=None, step=step) }}
<section id="submit-choose" class="primary">
<h2>{{ _('Choose Your Adventure') }}</h2>
<section id="upload-file">
<div class="island">
<h3>Host-It-Yourself Apps</h3>
<ul>
<li><i>You</i> host your app</li>
<li>CSS, JS, Python, PHP, whatever your server can handle you can use</li>
<li>Changed content gets updated immediately</li>
<li>Deploys must happen manually on your server</li>
<li>Stability dependent on your server</li>
<li>Works everywhere</li>
<li>Apps are awesome</li>
</ul>
<p class="listing-footer"><a href="{{ url('submit.app.manifest') }}" class="button prominent">Proceed</a></p>
</div>
<div class="island">
<h3>Packaged Apps</h3>
<ul>
<li><i>Mozilla</i> hosts your packaged app</li>
<li>Limited to client-side CSS and JS</li>
<li>CSP capabilities</li>
<li>External content must be loaded via AJAX</li>
<li>Access to privileged APIs</li>
<li>Versioned content</li>
</ul>
<p class="listing-footer"><a href="{{ url('submit.app.package') }}" class="button prominent">Proceed</a></p>
</div>
</section>
</section>
{% endblock %}

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

@ -17,8 +17,13 @@
<footer class="listing-footer button-wrapper">
<a href="{{ addon.get_dev_url('edit') }}" class="button prominent">
{{ _('Manage My App') }}</a>
<a href="{{ url('submit.app.manifest') }}" class="button prominent">
{{ _('Submit Another App') }}</a>
{% if waffle.switch('allow-packaged-app-uploads') %}
<a href="{{ url('submit.app.choose') }}" class="button prominent">
{{ _('Submit Another App') }}</a>
{% else %}
<a href="{{ url('submit.app.manifest') }}" class="button prominent">
{{ _('Submit Another App') }}</a>
{% endif %}
</footer>
</div>
</section>

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

@ -0,0 +1,34 @@
{% extends 'developers/base_impala.html' %}
{% set title = _('Upload Your App') %}
{% block title %}{{ hub_page_title(title) }}{% endblock %}
{% block content %}
{{ hub_breadcrumbs(items=[(None, title)]) }}
<h1>{{ title }}</h1>
{{ progress(request, addon=None, step=step) }}
<section id="submit-upload" class="primary">
<h2>{{ _("Where's Your Packaged App?") }}</h2>
<form method="post" id="create-addon" class="item">
{{ csrf() }}
<p>
{% trans %}
Use the fields below to upload your packaged app. After upload, a
series of automated validation tests will be run on your file.
{% endtrans %}
</p>
<section id="upload-file" class="island">
<div class="hidden">
{{ form.upload }}
</div>
<input type="file" id="upload-app" data-upload-url="{{ url('mkt.developers.upload') }}">
{{ form.non_field_errors() }}
<div class="submission-buttons addon-submission-field">
<button class="addon-upload-dependant" id="submit-upload-file-finish" disabled=disabled type="submit">
{{ _('Continue') }}
</button>
</div>
</section>
</form>
</section>
{% endblock content %}

Двоичные данные
mkt/submit/tests/packaged/mozBOM.zip Normal file

Двоичный файл не отображается.

Двоичные данные
mkt/submit/tests/packaged/mozball.zip Normal file

Двоичный файл не отображается.

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

@ -45,7 +45,7 @@ class TestAppSubmissionChecklist(amo.tests.TestCase):
self.cl.update(terms=True, manifest=True, payments=True)
eq_(self.cl.get_next(), 'details')
def test_next_payments(self):
def test_next_skipped_payments(self):
self.cl.update(terms=True, manifest=True, details=True)
eq_(self.cl.get_next(), 'payments')

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

@ -10,8 +10,9 @@ from pyquery import PyQuery as pq
import waffle
import amo
from amo.helpers import urlparams
import amo.tests
import paypal
from amo.helpers import urlparams
from amo.tests import formset, initial
from amo.tests.test_helpers import get_image_path
from amo.urlresolvers import reverse
@ -23,12 +24,12 @@ from apps.users.notifications import app_surveys
from constants.applications import DEVICE_TYPES
from files.tests.test_models import UploadTest as BaseUploadTest
from market.models import Price
import paypal
from translations.models import Translation
from users.models import UserProfile
import mkt
from mkt.submit.models import AppSubmissionChecklist
from mkt.submit.decorators import read_dev_agreement_required
from mkt.webapps.models import AddonExcludedRegion as AER, Webapp
@ -86,7 +87,7 @@ class TestTerms(TestSubmit):
def test_jump_to_step(self):
r = self.client.get(reverse('submit.app'), follow=True)
self.assertRedirects(r, self.url)
self.assert3xx(r, self.url)
def test_page(self):
r = self.client.get(self.url)
@ -100,8 +101,9 @@ class TestTerms(TestSubmit):
self._test_progress_display([], 'terms')
def test_agree(self):
self.create_switch(name='allow-packaged-app-uploads')
r = self.client.post(self.url, {'read_dev_agreement': True})
self.assertRedirects(r, reverse('submit.app.manifest'))
self.assert3xx(r, reverse('submit.app.choose'))
#dt = self.get_user().read_dev_agreement
#assert close_to_now(dt), (
# 'Expected date of agreement read to be close to now. Was %s' % dt)
@ -109,9 +111,10 @@ class TestTerms(TestSubmit):
eq_(UserNotification.objects.count(), 0)
def test_agree_and_sign_me_up(self):
self.create_switch(name='allow-packaged-app-uploads')
r = self.client.post(self.url, {'read_dev_agreement': True,
'newsletter': True})
self.assertRedirects(r, reverse('submit.app.manifest'))
self.assert3xx(r, reverse('submit.app.choose'))
#dt = self.get_user().read_dev_agreement
#assert close_to_now(dt), (
# 'Expected date of agreement read to be close to now. Was %s' % dt)
@ -127,6 +130,18 @@ class TestTerms(TestSubmit):
eq_(self.user.read_dev_agreement, False)
eq_(UserNotification.objects.count(), 0)
def test_read_dev_agreement_required(self):
f = mock.Mock()
f.__name__ = 'function'
request = mock.Mock()
request.amo_user.read_dev_agreement = False
request.get_full_path.return_value = self.url
func = read_dev_agreement_required(f)
res = func(request)
assert not f.called
eq_(res.status_code, 302)
eq_(res['Location'], reverse('submit.app'))
class TestManifest(TestSubmit):
fixtures = ['base/users']
@ -146,14 +161,18 @@ class TestManifest(TestSubmit):
def test_cannot_skip_prior_step(self):
r = self.client.get(self.url, follow=True)
# And we start back at one...
self.assertRedirects(r, reverse('submit.app.terms'))
self.assert3xx(r, reverse('submit.app.terms'))
def test_jump_to_step(self):
# I already read the Terms.
self._step()
# So jump me to the Manifest step.
r = self.client.get(reverse('submit.app'), follow=True)
self.assertRedirects(r, reverse('submit.app.manifest'))
self.assert3xx(r, reverse('submit.app.manifest'))
# Now with waffles!
self.create_switch(name='allow-packaged-app-uploads')
r = self.client.get(reverse('submit.app'), follow=True)
self.assert3xx(r, reverse('submit.app.choose'))
def test_page(self):
self._step()
@ -187,12 +206,10 @@ class BaseWebAppTest(BaseUploadTest, UploadAddon, amo.tests.TestCase):
def setUp(self):
super(BaseWebAppTest, self).setUp()
self.manifest = os.path.join(settings.ROOT, 'mkt', 'submit', 'tests',
'webapps', 'mozball.webapp')
self.manifest = self.manifest_path('mozball.webapp')
self.manifest_url = 'http://allizom.org/mozball.webapp'
self.upload = self.get_upload(abspath=self.manifest)
self.upload.name = self.manifest_url
self.upload.save()
self.upload.update(name=self.manifest_url, is_webapp=True)
self.url = reverse('submit.app.manifest')
assert self.client.login(username='regular@mozilla.com',
password='password')
@ -211,7 +228,7 @@ class TestCreateWebApp(BaseWebAppTest):
def test_post_app_redirect(self):
r = self.post()
webapp = Webapp.objects.get()
self.assertRedirects(r,
self.assert3xx(r,
reverse('submit.app.details', args=[webapp.app_slug]))
def test_no_hint(self):
@ -327,6 +344,49 @@ class TestCreateWebAppFromManifest(BaseWebAppTest):
eq_(rs.status_code, 302)
class BasePackagedAppTest(BaseUploadTest, UploadAddon, amo.tests.TestCase):
fixtures = ['base/apps', 'base/users', 'base/platforms']
def setUp(self):
super(BasePackagedAppTest, self).setUp()
self.package = self.packaged_app_path('mozball.zip')
self.upload = self.get_upload(abspath=self.package)
self.upload.update(name='mozball.zip', is_webapp=True)
self.url = reverse('submit.app.package')
assert self.client.login(username='regular@mozilla.com',
password='password')
# Complete first step.
self.client.post(reverse('submit.app.terms'),
{'read_dev_agreement': True})
def post_addon(self):
eq_(Addon.objects.count(), 0)
self.post()
return Addon.objects.get()
class TestCreatePackagedApp(BasePackagedAppTest):
def test_post_app_redirect(self):
res = self.post()
webapp = Webapp.objects.get()
self.assert3xx(res,
reverse('submit.app.details', args=[webapp.app_slug]))
def test_app_from_uploaded_package(self):
addon = self.post_addon()
eq_(addon.type, amo.ADDON_WEBAPP)
eq_(addon.current_version.version, '1.0')
eq_(addon.is_packaged, True)
eq_(addon.guid, None)
eq_(unicode(addon.name), u'Packaged MozillaBall ょ')
eq_(addon.slug, 'app-%s' % addon.id)
eq_(addon.app_slug, u'packaged-mozillaball-ょ')
eq_(addon.summary, u'Exciting Open Web development action!')
eq_(Translation.objects.get(id=addon.summary.id, locale='it'),
u'Azione aperta emozionante di sviluppo di fotoricettore!')
class TestDetails(TestSubmit):
fixtures = ['base/apps', 'base/users', 'webapps/337141-steamcube']
@ -381,8 +441,8 @@ class TestDetails(TestSubmit):
payments_url = reverse('submit.app.payments',
args=[self.webapp.app_slug])
r = self.client.get(payments_url, follow=True)
self.assertRedirects(r, reverse('submit.app.details',
args=[self.webapp.app_slug]))
self.assert3xx(r, reverse('submit.app.details',
args=[self.webapp.app_slug]))
def test_resume_later(self):
self._step()
@ -391,7 +451,7 @@ class TestDetails(TestSubmit):
premium_type=amo.ADDON_PREMIUM)
res = self.client.get(reverse('submit.app.resume',
args=[self.webapp.app_slug]))
self.assertRedirects(res, self.webapp.get_dev_url('paypal_setup'))
self.assert3xx(res, self.webapp.get_dev_url('paypal_setup'))
def test_not_owner(self):
self._step()
@ -825,14 +885,14 @@ class TestPayments(TestSubmit):
res = self.client.post(self.get_url('payments'),
{'premium_type': amo.ADDON_FREE})
eq_(res.status_code, 302)
self.assertRedirects(res, self.get_url('done'))
self.assert3xx(res, self.get_url('done'))
eq_(self.get_webapp().status, expected_status)
def test_valid_pending(self):
res = self.client.post(self.get_url('payments'),
{'premium_type': amo.ADDON_FREE})
eq_(res.status_code, 302)
self.assertRedirects(res, self.get_url('done'))
self.assert3xx(res, self.get_url('done'))
eq_(self.get_webapp().status, amo.WEBAPPS_UNREVIEWED_STATUS)
def test_premium(self):
@ -840,19 +900,19 @@ class TestPayments(TestSubmit):
res = self.client.post(self.get_url('payments'),
{'premium_type': type_})
eq_(res.status_code, 302)
self.assertRedirects(res, self.get_url('payments.upsell'))
self.assert3xx(res, self.get_url('payments.upsell'))
def test_free_inapp(self):
res = self.client.post(self.get_url('payments'),
{'premium_type': amo.ADDON_FREE_INAPP})
eq_(res.status_code, 302)
self.assertRedirects(res, self.get_url('payments.paypal'))
self.assert3xx(res, self.get_url('payments.paypal'))
def test_premium_other(self):
res = self.client.post(self.get_url('payments'),
{'premium_type': amo.ADDON_OTHER_INAPP})
eq_(res.status_code, 302)
self.assertRedirects(res, self.get_url('done'))
self.assert3xx(res, self.get_url('done'))
def test_price(self):
self.webapp.update(premium_type=amo.ADDON_PREMIUM)
@ -860,7 +920,7 @@ class TestPayments(TestSubmit):
{'price': self.price.pk})
eq_(res.status_code, 302)
eq_(self.get_webapp().premium.price.pk, self.price.pk)
self.assertRedirects(res, self.get_url('payments.paypal'))
self.assert3xx(res, self.get_url('payments.paypal'))
def _make_upsell(self):
free = Addon.objects.create(type=amo.ADDON_WEBAPP)
@ -909,7 +969,7 @@ class TestPayments(TestSubmit):
eq_(self.get_webapp().upsold.free.pk, free.pk)
eq_(self.get_webapp().upsold.premium.pk, self.get_webapp().pk)
eq_(res.status_code, 302)
self.assertRedirects(res, self.get_url('payments.paypal'))
self.assert3xx(res, self.get_url('payments.paypal'))
def test_no_upsell(self):
self.webapp.update(premium_type=amo.ADDON_PREMIUM)
@ -941,7 +1001,7 @@ class TestPayments(TestSubmit):
'email': 'foo@bar.com'})
eq_(self.get_webapp().paypal_id, 'foo@bar.com')
eq_(res.status_code, 302)
self.assertRedirects(res, self.get_url('payments.bounce'))
self.assert3xx(res, self.get_url('payments.bounce'))
@mock.patch('mkt.submit.views.client')
@mock.patch('mkt.submit.views.waffle.flag_is_active')
@ -954,7 +1014,7 @@ class TestPayments(TestSubmit):
eq_(client.patch_seller_paypal.call_args[1]['data']['paypal_id'],
'foo@bar.com')
client.post_permissions_url.return_value = {'token': 'http://foo/'}
self.assertRedirects(res, self.get_url('payments.bounce'))
self.assert3xx(res, self.get_url('payments.bounce'))
@mock.patch('mkt.submit.views.client')
def test_bounce_solitude(self, client):
@ -976,7 +1036,7 @@ class TestPayments(TestSubmit):
res = self.client.post(self.get_url('payments.paypal'),
{'business_account': 'later'})
eq_(res.status_code, 302)
self.assertRedirects(res, self.get_url('done'))
self.assert3xx(res, self.get_url('done'))
def get_acquire_url(self):
url = self.webapp.get_dev_url('acquire_refund_permission')
@ -992,7 +1052,7 @@ class TestPayments(TestSubmit):
get_permissions_token.return_value = 'foo'
get_personal_data.return_value = {'email': 'a@a.com'}
res = self.client.get(self.get_acquire_url())
self.assertRedirects(res, self.get_url('payments.confirm'))
self.assert3xx(res, self.get_url('payments.confirm'))
@mock.patch('mkt.submit.views.client')
def test_confirm_solitude(self, client):
@ -1025,7 +1085,7 @@ class TestPayments(TestSubmit):
get_permissions_token.return_value = 'foo'
get_personal_data.return_value = {'email': 'a@a.com'}
res = self.client.get(self.get_acquire_url())
self.assertRedirects(res, self.get_url('payments.paypal'))
self.assert3xx(res, self.get_url('payments.paypal'))
@mock.patch('paypal.get_permissions_token')
def test_bounce_result_fails_paypal_error(self, get_permissions_token):

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

@ -31,6 +31,8 @@ urlpatterns = decorate(write, patterns('',
# App submission.
url('^$', views.submit, name='submit.app'),
url('^terms$', views.terms, name='submit.app.terms'),
url('^choose$', views.choose, name='submit.app.choose'),
url('^manifest$', views.manifest, name='submit.app.manifest'),
url('^package$', views.package, name='submit.app.package'),
('', include(submit_apps_patterns)),
))

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

@ -5,10 +5,12 @@ from django.forms.models import model_to_dict
from django.shortcuts import redirect
from django.utils.translation.trans_real import to_language
import commonware.log
import jingo
import waffle
import amo
import paypal
from amo.decorators import login_required
from amo.helpers import absolutify, urlparams
from amo.urlresolvers import reverse
@ -16,7 +18,6 @@ from addons.forms import DeviceTypeForm
from addons.models import Addon, AddonUser
from lib.pay_server import client
from market.models import AddonPaymentData
import paypal
from files.models import Platform
from users.models import UserProfile
@ -28,7 +29,10 @@ from mkt.submit.forms import AppDetailsBasicForm, PaypalSetupForm
from mkt.submit.models import AppSubmissionChecklist
from . import forms
from .decorators import submit_step
from .decorators import read_dev_agreement_required, submit_step
log = commonware.log.getLogger('z.submit')
@login_required
@ -37,6 +41,8 @@ def submit(request):
# If dev has already agreed, continue to next step.
user = UserProfile.objects.get(pk=request.user.id)
if user.read_dev_agreement:
if waffle.switch_is_active('allow-packaged-app-uploads'):
return redirect('submit.app.choose')
return redirect('submit.app.manifest')
else:
return redirect('submit.app.terms')
@ -46,16 +52,18 @@ def submit(request):
@submit_step('terms')
def terms(request):
# If dev has already agreed, continue to next step.
# TODO: When this code is finalized, use request.amo_user instead.
user = UserProfile.objects.get(pk=request.user.id)
if user.read_dev_agreement:
# TODO: Have decorator redirect to next step.
if request.amo_user.read_dev_agreement:
if waffle.switch_is_active('allow-packaged-app-uploads'):
return redirect('submit.app.choose')
return redirect('submit.app.manifest')
agreement_form = forms.DevAgreementForm(
request.POST or {'read_dev_agreement': True}, instance=user)
request.POST or {'read_dev_agreement': True},
instance=request.amo_user)
if request.POST and agreement_form.is_valid():
agreement_form.save()
if waffle.switch_is_active('allow-packaged-app-uploads'):
return redirect('submit.app.choose')
return redirect('submit.app.manifest')
return jingo.render(request, 'submit/terms.html', {
'step': 'terms',
@ -64,21 +72,27 @@ def terms(request):
@login_required
@read_dev_agreement_required
@submit_step('manifest')
def choose(request):
if not waffle.switch_is_active('allow-packaged-app-uploads'):
return redirect('submit.app.manifest')
return jingo.render(request, 'submit/choose.html', {
'step': 'manifest',
})
@login_required
@read_dev_agreement_required
@submit_step('manifest')
@transaction.commit_on_success
def manifest(request):
# TODO: Have decorator handle the redirection.
user = UserProfile.objects.get(pk=request.user.id)
if not user.read_dev_agreement:
# And we start back at one...
return redirect('submit.app')
form = forms.NewWebappForm(request.POST or None)
if request.method == 'POST' and form.is_valid():
data = form.cleaned_data
addon = Addon.from_upload(
form.cleaned_data['upload'],
[Platform.objects.get(id=amo.PLATFORM_ALL.id)])
plats = [Platform.objects.get(id=amo.PLATFORM_ALL.id)]
addon = Addon.from_upload(data['upload'], plats)
if addon.has_icon_in_manifest():
# Fetch the icon, do polling.
addon.update(icon_type='image/png')
@ -100,6 +114,37 @@ def manifest(request):
})
@login_required
@read_dev_agreement_required
@submit_step('manifest')
def package(request):
form = forms.NewWebappForm(request.POST or None)
if request.method == 'POST' and form.is_valid():
addon = Addon.from_upload(
form.cleaned_data['upload'],
[Platform.objects.get(id=amo.PLATFORM_ALL.id)])
addon.get_latest_file().update(is_packaged=True)
if addon.has_icon_in_manifest():
# Fetch the icon, do polling.
addon.update(icon_type='image/png')
tasks.fetch_icon.delay(addon)
else:
# In this case there is no need to do any polling.
addon.update(icon_type='')
AddonUser(addon=addon, user=request.amo_user).save()
AppSubmissionChecklist.objects.create(addon=addon, terms=True,
manifest=True)
return redirect('submit.app.details', addon.app_slug)
return jingo.render(request, 'submit/upload.html', {
'form': form,
'step': 'manifest',
})
@dev_required
@submit_step('details')
def details(request, addon_id, addon):

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

@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
import json
import os
import urlparse
import uuid
import zipfile
from django.conf import settings
from django.core.files.storage import default_storage as storage
@ -17,13 +19,13 @@ from tower import ugettext as _
import amo
import amo.models
import amo.utils
from addons import query
from addons.models import (Addon, AddonDeviceType, Category,
update_name_table, update_search_index)
from amo.decorators import skip_cache
from amo.storage_utils import copy_stored_file
from amo.urlresolvers import reverse
from amo.utils import smart_path
from constants.applications import DEVICE_TYPES
from files.models import nfd_str
from files.utils import parse_addon
@ -210,15 +212,21 @@ class Webapp(Addon):
return 'icons' in data
def get_manifest_json(self):
import json
file_path = self.get_latest_file().file_path
try:
# The first file created for each version of the web app
# is the manifest.
with storage.open(self.get_latest_file().file_path, 'r') as mf:
return json.load(mf)
if zipfile.is_zipfile(file_path):
zf = zipfile.ZipFile(file_path)
data = zf.open('manifest.webapp').read()
zf.close()
return json.loads(data)
else:
file = storage.open(file_path, 'r')
data = file.read()
return json.loads(data)
except Exception, e:
log.error('Failed to open saved manifest %r for webapp %s, %s.'
% (self.manifest_url, self.pk, e))
log.error(u'[Webapp:%s] Failed to open saved file %r, %s.'
% (self, file_path, e))
raise
def share_url(self):
@ -233,7 +241,7 @@ class Webapp(Addon):
data = parse_addon(upload, self)
version = self.versions.latest()
version.update(version=data['version'])
path = amo.utils.smart_path(nfd_str(upload.path))
path = smart_path(nfd_str(upload.path))
file = version.files.latest()
file.filename = file.generate_filename(extension='.webapp')
file.size = int(max(1, round(storage.size(path) / 1024, 0)))
@ -435,6 +443,19 @@ class Webapp(Addon):
except IndexError:
return None
@amo.cached_property
def is_packaged(self):
"""
Whether this app's latest version is a packaged app.
Note: This isn't using `current_version` since current version only
gets valid versions and we need to use this during the submission flow.
"""
version = self.versions.latest()
if version:
return version.all_files[0].is_packaged
return False
@amo.cached_property
def has_packaged_files(self):
"""

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

@ -250,10 +250,12 @@ class TestWebapp(TestCase):
eq_(sorted(Webapp.objects.get(id=w2.id).get_regions()),
sorted(w2_regions))
def test_has_packaged_files(self):
def test_package_helpers(self):
app1 = app_factory()
eq_(app1.is_packaged, False)
eq_(app1.has_packaged_files, False)
app2 = app_factory(file_kw=dict(is_packaged=True))
eq_(app2.is_packaged, True)
eq_(app2.has_packaged_files, True)