Added ability to upload packaged app (bug 777138)
This commit is contained in:
Родитель
53cb2a5e27
Коммит
265daa9835
|
@ -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',
|
||||
|
|
|
@ -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> · </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> · </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('…and {0} more',
|
||||
'…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 <em:id> is invalid.')
|
||||
eq_(sorted(msg['context']),
|
||||
[[u'<foo/>'], u'<em:description>...'])
|
||||
|
||||
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 <em:id> is invalid.')
|
||||
eq_(sorted(msg['context']),
|
||||
[[u'<foo/>'], u'<em:description>...'])
|
||||
|
||||
@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 %}
|
Двоичный файл не отображается.
Двоичный файл не отображается.
|
@ -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)
|
||||
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче