New listed and unlisted addon flow (#3697)

This commit is contained in:
Andrew Williamson 2016-10-13 16:45:34 +01:00 коммит произвёл GitHub
Родитель 2762f18b70
Коммит df7b455edd
16 изменённых файлов: 404 добавлений и 822 удалений

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

@ -18,6 +18,7 @@ from olympia import amo, paypal
from olympia.addons.forms import AddonFormBasic
from olympia.addons.models import (
Addon, AddonDependency, AddonUser, Charity, Preview)
from olympia.amo.fields import HttpHttpsOnlyURLField
from olympia.amo.forms import AMOModelForm
from olympia.amo.urlresolvers import reverse
from olympia.applications.models import AppVersion
@ -530,13 +531,6 @@ class NewAddonForm(AddonUploadForm):
coerce=int,
error_messages={'required': 'Need at least one platform.'}
)
is_unlisted = forms.BooleanField(
initial=False,
required=False,
label=_lazy(u'Do not list my add-on on this site'),
help_text=_lazy(
u'Check this option if you intend to distribute your add-on on '
u'your own and only need it to be signed by Mozilla.'))
def clean(self):
if not self.errors:
@ -546,6 +540,16 @@ class NewAddonForm(AddonUploadForm):
return self.cleaned_data
class StandaloneValidationForm(AddonUploadForm):
is_unlisted = forms.BooleanField(
initial=False,
required=False,
label=_lazy(u'Do not list my add-on on this site'),
help_text=_lazy(
u'Check this option if you intend to distribute your add-on on '
u'your own and only need it to be signed by Mozilla.'))
class NewVersionForm(NewAddonForm):
beta = forms.BooleanField(
required=False,
@ -674,13 +678,24 @@ FileFormSet = modelformset_factory(File, formset=BaseFileFormSet,
form=FileForm, can_delete=True, extra=0)
class Step3Form(AddonFormBasic):
description = TransField(widget=TransTextarea, required=False)
class DescribeForm(AddonFormBasic):
tags = None
support_url = TransField.adapt(HttpHttpsOnlyURLField)(required=False)
support_email = TransField.adapt(forms.EmailField)(required=False)
class Meta:
model = Addon
fields = ('name', 'slug', 'summary', 'description', 'is_experimental')
fields = ('name', 'slug', 'summary', 'is_experimental', 'support_url',
'support_email')
class ReviewerNotesForm(happyforms.ModelForm):
approvalnotes = forms.CharField(
widget=TranslationTextarea(attrs={'rows': 4}), required=False)
class Meta:
model = Version
fields = ('approvalnotes',)
class PreviewForm(happyforms.ModelForm):
@ -801,3 +816,23 @@ def DependencyFormSet(*args, **kw):
FormSet = modelformset_factory(AddonDependency, formset=_FormSet,
form=_Form, extra=0, can_delete=True)
return FormSet(*args, **kw)
class DistributionChoiceForm(happyforms.Form):
LISTED_LABEL = '%s <span class="helptext">%s</span>' % (
_(u'On this site.'),
_(u'Your submission will be listed on this site and the Firefox '
u'Add-ons Manager for millions of users, after it passes code '
u'review. Automatic updates are handled by this site. This add-on '
u'will also be considered for Mozilla promotions and contests. '
u'Self-distribution of the reviewed files is also possible.'))
UNLISTED_LABEL = '%s <span class="helptext">%s</span>' % (
_(u'On my own.'),
_(u'This version will be immediately signed for self-distribution. '
u'Updates are handled manually via an updateURL or external '
u'application updates.'))
choices = forms.ChoiceField(
choices=(('listed', mark_safe(LISTED_LABEL)),
('unlisted', mark_safe(UNLISTED_LABEL)),),
widget=forms.RadioSelect(attrs={'class': 'channel'}))

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

@ -13,10 +13,8 @@
{{ dev_breadcrumbs(items=[(None, page_title)]) }}
<h2 class="is_addon">{{ title }}</h2>
</header>
<section class="secondary" role="complementary">
{% include "devhub/addons/submit/sidebar.html" %}
</section>
<section class="primary addon-submission-process" role="main">
<section class="addon-submission-process" role="main">
{% block primary %}{% endblock %}
</section>
{% endblock content %}

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

@ -1,23 +1,19 @@
{% from "devhub/includes/macros.html" import some_html_tip, select_cats %}
{% extends "devhub/addons/submit/base.html" %}
{% block title %}{{ dev_page_title(_('Step 3'), addon) }}{% endblock %}
{% block title %}{{ dev_page_title(_('Describe Add-on'), addon) }}{% endblock %}
{% block primary %}
<h3>{{ _('Step 3. Describe') }}</h3>
<h3>{{ _('Describe Add-on') }}</h3>
<form method="post" id="submit-describe" class="item{% if not addon.is_listed %} unlisted{% endif %}">
{{ csrf() }}
<div class="addon-submission-field">
<label for="id_name">{{ _("Name and version:") }}</label>
<label for="id_name">{{ _("Name:") }}</label>
{{ form.name }}
{% set version = addon.current_version %}
<input type="text" disabled id="current_version"
value="{{ version.version }}" size="6">
{{ form.name.errors }}
</div>
<div class="addon-submission-field slug-edit">
<label>{{ _("The detail page will be:") }}</label>
<label>{{ _("Add-on URL:") }}</label>
<div id="slug_edit" class="edit_with_prefix edit_initially_hidden">
<span>{{ settings.SITE_URL }}</span>{{ form.slug }}
<div class="edit-addon-details">
@ -31,54 +27,102 @@
{{ form.slug.errors }}
</div>
<div class="addon-submission-field">
<label>{{ _('Provide a brief summary:') }}</label>
<label>{{ _('Summary:') }}</label>
{{ form.summary }}
{{ form.summary.errors }}
<div class="edit-addon-details">
{% if addon.is_listed %}
{% trans %}
This summary will be shown in listings and searches.
{% endtrans %}
{% endif %}
{{ _('This summary will be shown in listings and searches.') }}
<div class="char-count"
data-for-startswith="{{ form.summary.auto_id }}_"
data-maxlength="{{ form.summary.field.max_length }}"></div>
</div>
</div>
{% if addon.is_listed %}
<div class="addon-submission-field">
<label for="{{ form.is_experimental.auto_id }}">
{{ form.is_experimental }}
{{ _('This add-on is experimental') }}
<span class="tip tooltip"
title="{{ _('Check this option if your add-on is experimental '
'or otherwise not ready for general use. The '
'add-on will be listed but will have reduced '
'visibility. You can change this setting later.')
}}">?</span>
<div class="addon-submission-field">
<label for="{{ form.is_experimental.auto_id }}">
{{ form.is_experimental }}
{{ _('This add-on is experimental') }}
<span class="tip tooltip"
title="{{ _('Check this option if your add-on is experimental '
'or otherwise not ready for general use. The '
'add-on will be listed but will have reduced '
'visibility. You can change this setting later.')
}}">?</span>
</label>
</div>
<div id="addon-categories-edit" class="addon-submission-field"
data-max-categories="{{ amo.MAX_CATEGORIES }}">
{{ cat_form.non_form_errors() }}
{{ cat_form.management_form }}
{% for form in cat_form.initial_forms %}
{{ select_cats(amo.MAX_CATEGORIES, form) }}
{% endfor %}
</div>
</div>
<div id="addon-categories-edit" class="addon-submission-field"
data-max-categories="{{ amo.MAX_CATEGORIES }}">
{{ cat_form.non_form_errors() }}
{{ cat_form.management_form }}
{% for form in cat_form.initial_forms %}
{{ select_cats(amo.MAX_CATEGORIES, form) }}
{% endfor %}
</div>
<div class="addon-submission-field">
<label for="{{ form.support_email.auto_id }}">
{{ _('Support email:') }}
</label>
{{ form.support_email }}
{{ form.support_email.errors }}
</div>
<div class="addon-submission-field">
<label for="{{ form.support_url.auto_id }}">
{{ _('Support website:') }}
</label>
{{ form.support_url }}
{{ form.support_url.errors }}
</div>
{% if license_form %}
<b>{{ _('License:') }}</b>
<div class="addon-submission-field">
<label>{{ _('Provide a more detailed description:') }}</label>
{{ form.description }}
{{ form.description.errors }}
<div class="edit-addon-details">
{{ _("The description will appear on the detail page.") }}
{{ some_html_tip() }}
</div>
{% trans %}
Please choose a license appropriate for the rights you grant on your
source code.
{% endtrans %}
{{ license_form.builtin.errors }}
{{ license_form.builtin }}
{% set show_other = (license_form.initial.builtin == license_other_val or
(license_form.errors and not license_form.builtin.errors)) %}
<div class="license-other {{ 'js-hidden' if not show_other }}"
data-val="{{ license_other_val }}">
{{ license_form.non_field_errors() }}
{{ license_form.name.errors }}
{{ license_form.name.label_tag() }}
{{ license_form.name }}
{{ license_form.text.errors }}
{{ license_form.text.label_tag() }}
{{ license_form.text }}
{{ some_html_tip() }}
</div>
</div>
{% endif %}
{% set values = policy_form.data if policy_form.is_bound else policy_form.initial %}
<div class="optional-terms">
<div class="addon-submission-field">
{{ policy_form.has_priv }}
{{ policy_form.has_priv.label_tag() }}
<span class="tip tooltip"
title="{{ _("If your add-on transmits any data from the user's computer, "
"a privacy policy is required that explains what data is sent "
"and how it is used.")
}}">?</span>
<div class="priv {{ 'hidden' if not values.has_priv }}">
{{ policy_form.privacy_policy.errors }}
{{ policy_form.privacy_policy.label_tag() }}
{{ policy_form.privacy_policy }}
</div>
</div>
</div>
<div class="addon-submission-field">
<label for="{{ reviewer_form.approvalnotes.auto_id }}">
{{ _('Notes to Reviewer:') }}
</label>
<p>{{ _('Is there anything our reviewers should bear in mind when reviewing this add-on?') }}</p>
{{ reviewer_form.approvalnotes }}
<p>{{ _('These notes will only be visible to you and our reviewers.') }}</p>
</div>
<div class="submission-buttons addon-submission-field">
<button type="submit">
{{ _('Continue') }}
{{ _('Submit Add-on for Review') }}
</button>
</div>
</form>

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

@ -0,0 +1,23 @@
{% extends "devhub/addons/submit/base.html" %}
{% block title %}{{ dev_page_title(_('Where to Host Add-on'), addon) }}{% endblock %}
{% block primary %}
<form method="post" class="item addon-submit-distribute">
{{ csrf() }}
<h3>{{ _('Where to Host Add-on') }}</h3>
<div>
{{ distribution_form.choices }}
</div>
<p><a href="https://developer.mozilla.org/en-US/Add-ons/Distribution"
target="_blank" rel="noopener noreferrer">
{{ _('More information on Add-on Distribution and Signing') }}</a></p>
<div class="submission-buttons addon-submission-field">
<button type="submit">
{{ _('Continue') }}
</button>
</div>
</section>
</form>
{% endblock primary %}

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

@ -5,82 +5,46 @@
{% endblock %}
{% block primary %}
<h3>{{ _("You're done!") }}</h3>
{% set upload_version = addon.current_version %}
{% if addon.is_listed %}
<h3>{{ _("Version Submitted for Review") }}</h3>
<p>
{% if addon.status == amo.STATUS_NOMINATED %}
{{ _('Your add-on has been submitted to the New Add-on queue.') }}
{% endif %}
{{ _("Youre done! This version has been submitted for review. You will be "
"notified when the review has been completed, or if our reviewers have "
"any questions about your submission.") }}
</p>
<p>
{{ _("You'll receive an email once it has been reviewed by an editor and "
"signed. Once it has been signed you will be able to install it:") }}
{{ _("Your listing will be more successful by adding a detailed description "
"and screenshots. Get your listing ready for publication:") }}
</p>
<p>
<a id="submitted-addon-url" href="{{ addon.get_url_path() }}">
{{ addon.get_url_path()|absolutify|display_url }}</a>
<a class="button" href="{{ addon.get_dev_url() }}">
{{ _("Manage Listing") }}</a>
</p>
<div class="done-next-steps">
<p><strong>{{ _('Next steps:') }}</strong></p>
<ul>
{% if is_platform_specific %}
{% set files_url = url('devhub.versions.edit',
addon.slug, addon.current_version.id) %}
<li>{{ _('<a href="{0}">Upload</a> another platform-specific file to this version.')|fe(files_url) }}</li>
{% endif %}
<li>{{ _('Provide more details by <a href="{0}">editing its listing</a>.')|fe(addon.get_dev_url()) }}</li>
<li>{{ _('Tell your users why you created this in your <a href="{0}">Developer Profile</a>.')|fe(addon.get_dev_url('profile')) }}</li>
<li>{{ _('View and subscribe to your add-on\'s <a href="{0}">activity feed</a> to stay updated on reviews, collections, and more.')|fe(url('devhub.feed', addon.slug)) }}</li>
<li>{{ _('View approximate review queue <a href="{0}">wait times</a>.')|fe('https://blog.mozilla.org/addons/tag/add-on-reviews/') }}</li>
</ul>
</div>
<div id="editor-pitch" class="action-needed">
<h3>{{ _('Get Ahead in the Review Queue!') }}</h3>
<p>
{{ _('Become an AMO Reviewer today and get your add-ons reviewed faster.') }}
<a class="button learn-more" href="https://wiki.mozilla.org/AMO:Editors">
{{ _('Learn More') }}</a>
{{ _("You can also edit this version you just submitted by adding version "
"notes, files for other platforms, or source code if your submission "
"includes minified, obfuscated or compiled code.") }}
</p>
<p>
{% set version_edit_url = url('devhub.versions.edit', addon.slug, upload_version.id) %}
<a class="button" href="{{ version_edit_url }}">{{
_("Edit version {0}")|fe(upload_version.version) }}</a>
</p>
</div>
{% else %}
{% set signed = addon.status == amo.STATUS_PUBLIC %}
{% if signed %}
<p>
{{ _('Your add-on has been signed and it\'s ready to use. You can download it here:') }}
</p>
<p>
{% set version_url = url('devhub.versions.edit', addon.slug, addon.current_version.id) %}
<a id="download-addon-url" href="{{ version_url }}">
{{ version_url|absolutify|display_url }}</a>
</p>
{% else %}
<p>
{% if addon.status == amo.STATUS_NOMINATED %}
{{ _('Your add-on has been submitted to the Unlisted New Add-on queue.') }}
{% endif %}
</p>
<p>
{{ _('You\'ll receive an email once it has been reviewed by an editor and signed.') }}
</p>
{% endif %}
<h3>{{ _("Version Signed") }}</h3>
<p>
<strong>{{ _('Your add-on will not be publicly available on this website.') }}</strong>
{{ _("Youre done! This version is signed and ready to for self-distribution."
"You can download it by clicking the button below.") }}
</p>
<p>
{% set file = upload_version.all_files[0] %}
<a class="button" id="download-addon-url" href="{{ file.get_url_path('devhub') }}">{{
_("Download {0}")|fe(file.pretty_filename()) }}</a>
</p>
<p>
<a class="button" href="{{ url('devhub.addons') }}">
{{ _("Return to My Submissions") }}</a>
</p>
<div class="done-next-steps">
<p><strong>{{ _('Next steps:') }}</strong></p>
<ul>
{% if is_platform_specific %}
{% set files_url = url('devhub.versions.edit',
addon.slug, addon.current_version.id) %}
<li>{{ _('<a href="{0}">Upload</a> another platform-specific file to this version.')|fe(files_url) }}</li>
{% endif %}
<li>{{ _('You can upload new versions of your add-on in the <a href="{0}">add-on\'s developer page</a>.')|fe(addon.get_dev_url()) }}</li>
{% if not signed %}
<li>{{ _('View approximate review queue <a href="{0}">wait times</a>.')|fe('https://blog.mozilla.org/addons/tag/add-on-reviews/') }}</li>
{% endif %}
</ul>
</div>
{% endif %}
{% endblock %}

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

@ -1,63 +0,0 @@
{% from "devhub/includes/macros.html" import some_html_tip %}
{% extends "devhub/addons/submit/base.html" %}
{% block title %}{{ dev_page_title(_('Step 5'), addon) }}{% endblock %}
{% block primary %}
<h3>{{ _('Step 5. Select a License') }}</h3>
{% if license_form %}
<p>{% trans %}
We require all add-ons to indicate the terms under which their source code is
licensed. Please select a license from the list below or enter a custom license.
{% endtrans %}</p>
{% endif %}
<form method="post" class="item devhub-form submit-license">
{{ csrf() }}
{% if license_form %}
<b>{{ _('Select a license for your add-on:') }}</b>
<div class="addon-submission-field">
{{ license_form.builtin.errors }}
{{ license_form.builtin }}
{% set show_other = (license_form.initial.builtin == license_other_val or
(license_form.errors and not license_form.builtin.errors)) %}
<div class="license-other {{ 'js-hidden' if not show_other }}"
data-val="{{ license_other_val }}">
{{ license_form.non_field_errors() }}
{{ license_form.name.errors }}
{{ license_form.name.label_tag() }}
{{ license_form.name }}
{{ license_form.text.errors }}
{{ license_form.text.label_tag() }}
{{ license_form.text }}
{{ some_html_tip() }}
</div>
</div>
{% endif %}
{% set values = policy_form.data if policy_form.is_bound else policy_form.initial %}
<div class="optional-terms">
<div>
{{ policy_form.has_eula }}
{{ policy_form.has_eula.label_tag() }}
<div class="eula {{ 'hidden' if not values.has_eula }}">
{{ policy_form.eula.errors }}
{{ policy_form.eula.label_tag() }}
{{ policy_form.eula }}
{{ some_html_tip() }}
</div>
</div>
<div class="addon-submission-field">
{{ policy_form.has_priv }}
{{ policy_form.has_priv.label_tag() }}
<div class="priv {{ 'hidden' if not values.has_priv }}">
{{ policy_form.privacy_policy.errors }}
{{ policy_form.privacy_policy.label_tag() }}
{{ policy_form.privacy_policy }}
</div>
</div>
</div>
<div class="submission-buttons addon-submission-field">
<button type="submit">{{ _('Continue') }}</button>
</div>
</form>
{% endblock %}

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

@ -1,18 +0,0 @@
{% extends "devhub/addons/submit/base.html" %}
{% block title %}{{ dev_page_title(_('Step 4'), addon) }}{% endblock %}
{% block primary %}
<h3>{{ _('Step 4. Add Images') }}</h3>
<form method="post" class="item" enctype="multipart/form-data" id="submit-media">
<p>
{% trans %}
Custom icons and screenshots draw attention and help users
understand what it does. We strongly recommend uploading a custom icon, as in
some areas of the website, only the icon and name will appear.
{% endtrans %}
</p>
{% set context = 'submit' %}
{% include "devhub/addons/forms_shared/media.html" %}
</form>
{% endblock primary %}

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

@ -1,44 +0,0 @@
{# Steps below HAS_ADDON don't have an addon associated. Once we have an
addon we can't go back below that line. #}
{% set HAS_ADDON = 3 %}
{# List of steps: (text, class). A class of 'all' means the step is relevant
to listed or unlisted addons, a class of 'listed' means it's only relevant
to listed addons, not unlisted ones. #}
{% set NAV = ((_('Getting Started'), 'all'),
(_('Upload your add-on'), 'all'),
(_('Describe your add-on'), 'all'),
(_('Add images'), 'listed'),
(_('Select a license'), 'listed'),
(_("You're done!"), 'all')) %}
{% set MAX = 6 %}
{% set BASE = 'devhub.submit.%s' %}
<div class="highlight">
<hgroup>
<h3>{{ _('Submission Process') }}</h3>
</hgroup>
<ol class="submit-addon-progress{% if addon and not addon.is_listed %} unlisted{% endif %}">
{% for text, class in NAV %}
<li class="{{ class }}{% if step.current == loop.index %} current{% endif %}">
{% if step.current < HAS_ADDON %}
{# 1. Don't link to future steps.
2. If step 1 is complete (agreement accepted), don't link to it. #}
{% if loop.index > step.current or (loop.index == 1 and step.current > 1)%}
{{ text }}
{% else %}
<a href="{{ url(BASE % loop.index) }}">{{ text }}</a>
{% endif %}
{% else %}
{# 1. We have an addon, so don't link to non-addon steps.
2. Don't link steps above the max step the addon has reached.
3. If step.max == MAX the addon is done, so we only show the final page. #}
{% if loop.index < HAS_ADDON or loop.index > step.max or step.max == MAX %}
{{ text }}
{% else %}
<a href="{{ url(BASE % loop.index, addon.slug) }}">{{ text }}</a>
{% endif %}
{% endif %}
</li>
{% endfor %}
</ol>
</div>

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

@ -1,10 +1,10 @@
{% extends "devhub/addons/submit/base.html" %}
{% block title %}{{ dev_page_title(_('Step 1'), addon) }}{% endblock %}
{% block title %}{{ dev_page_title(_('Getting Started'), addon) }}{% endblock %}
{% block primary %}
<h3>{{ _('Step 1. Getting Started') }}</h3>
<h3>{{ _('Getting Started') }}</h3>
{% include "devhub/agreement.html" %}

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

@ -1,12 +1,13 @@
{% extends "devhub/addons/submit/base.html" %}
{% block title %}{{ dev_page_title(_('Step 2'), addon) }}{% endblock %}
{% block title %}{{ dev_page_title(_('Upload Add-on'), addon) }}{% endblock %}
{% block primary %}
<form method="post" id="create-addon" class="item new-addon-file" enctype="multipart/form-data">
<form method="post" id="create-addon" class="item new-addon-file" enctype="multipart/form-data"
data-addon-is-listed="{% if listed %}true{% else %}false{% endif %}">
{{ csrf() }}
<h3>{{ _('Step 2. Upload Your Add-on') }}</h3>
<h3>{{ _('Upload Add-on') }}</h3>
<p>
{% trans %}
Use the fields below to upload your add-on package and select any platform
@ -19,21 +20,13 @@
<div class="hidden">
{{ new_addon_form.upload }}
</div>
<div class="list-addon">
<label>{{ _('Do you want your add-on to be distributed on this site?') }}</label>
<div>
<label for="{{ new_addon_form.is_unlisted.auto_id }}">
{{ new_addon_form.is_unlisted }}
{{ new_addon_form.is_unlisted.label }}
<span class="tip tooltip"
title="{{ new_addon_form.is_unlisted.help_text }}">?</span>
</label>
</div>
</div>
<input type="file" id="upload-addon"
{% if listed %}
data-upload-url="{{ url('devhub.upload') }}"
data-upload-url-listed="{{ url('devhub.upload') }}"
data-upload-url-unlisted="{{ url('devhub.upload_unlisted') }}">
{% else %}
data-upload-url="{{ url('devhub.upload_unlisted') }}"
{% endif %}
>
{{ new_addon_form.non_field_errors() }}

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

@ -17,14 +17,13 @@ import mock
import pytest
import waffle
from jingo.helpers import datetime as datetime_filter
from PIL import Image
from pyquery import PyQuery as pq
from olympia import amo, paypal, files
from olympia.amo.tests import TestCase, version_factory
from olympia.addons.models import (
Addon, AddonCategory, AddonFeatureCompatibility, Category, Charity)
from olympia.amo.helpers import absolutify, user_media_path, url as url_reverse
from olympia.amo.helpers import url as url_reverse
from olympia.amo.tests import addon_factory, formset, initial
from olympia.amo.tests.test_helpers import get_image_path
from olympia.amo.urlresolvers import reverse
@ -1268,7 +1267,7 @@ class TestAPIKeyPage(TestCase):
assert 'revoked' in mail.outbox[0].body
class TestSubmitStep1(TestSubmitBase):
class TestSubmitStepAgreement(TestSubmitBase):
def test_step1_submit(self):
self.user.update(read_dev_agreement=None)
response = self.client.get(reverse('devhub.submit.1'))
@ -1298,43 +1297,48 @@ class TestSubmitStep1(TestSubmitBase):
self.assert3xx(response, reverse('devhub.submit.2'))
class TestSubmitStep2(TestCase):
# More tests in TestCreateAddon.
class TestSubmitStepDistribute(TestCase):
fixtures = ['base/users']
def setUp(self):
super(TestSubmitStep2, self).setUp()
super(TestSubmitStepDistribute, self).setUp()
self.client.login(email='regular@mozilla.com')
self.user = UserProfile.objects.get(email='regular@mozilla.com')
def test_step_2_seen(self):
def test_check_agreement_okay(self):
r = self.client.post(reverse('devhub.submit.1'))
self.assert3xx(r, reverse('devhub.submit.2'))
r = self.client.get(reverse('devhub.submit.2'))
assert r.status_code == 200
def test_step_2_not_seen(self):
def test_redirect_back_to_agreement(self):
# We require a cookie that gets set in step 1.
self.user.update(read_dev_agreement=None)
r = self.client.get(reverse('devhub.submit.2'), follow=True)
self.assert3xx(r, reverse('devhub.submit.1'))
def test_step_2_listed_checkbox(self):
# There is a checkbox for the "is_listed" addon field.
self.client.post(reverse('devhub.submit.1'))
response = self.client.get(reverse('devhub.submit.2'))
assert response.status_code == 200
doc = pq(response.content)
assert doc('.list-addon input#id_is_unlisted[type=checkbox]')
def test_listed_redirects_to_next_step(self):
response = self.client.post(reverse('devhub.submit.2'),
{'choices': 'listed'})
self.assert3xx(response, reverse('devhub.submit.3', args=['listed']))
def test_unlisted_redirects_to_next_step(self):
response = self.client.post(reverse('devhub.submit.2'),
{'choices': 'unlisted'})
self.assert3xx(response, reverse('devhub.submit.3',
args=['unlisted']))
class TestSubmitStep3(TestSubmitBase):
# Tests for Upload step in TestCreateAddon
class TestSubmitStepDescribe(TestSubmitBase):
def setUp(self):
super(TestSubmitStep3, self).setUp()
self.url = reverse('devhub.submit.3', args=['a3615'])
SubmitStep.objects.create(addon_id=3615, step=3)
super(TestSubmitStepDescribe, self).setUp()
self.url = reverse('devhub.submit.4', args=['a3615'])
SubmitStep.objects.create(addon_id=3615, step=4)
AddonCategory.objects.filter(
addon=self.get_addon(),
@ -1345,77 +1349,79 @@ class TestSubmitStep3(TestSubmitBase):
ctx = self.client.get(self.url).context['cat_form']
self.cat_initial = initial(ctx.initial_forms[0])
self.next_step = reverse('devhub.submit.5', args=['a3615'])
License.objects.create(builtin=3, on_form=True)
self.get_addon().update(status=amo.STATUS_NULL)
def get_dict(self, **kw):
def get_dict(self, minimal=True, **kw):
result = {}
describe_form = {'name': 'Test name', 'slug': 'testname',
'summary': 'Hello!', 'is_experimental': True}
if not minimal:
describe_form.update({'support_url': 'http://stackoverflow.com',
'support_email': 'black@hole.org'})
cat_initial = kw.pop('cat_initial', self.cat_initial)
fs = formset(cat_initial, initial_count=1)
result = {'name': 'Test name', 'slug': 'testname',
'description': 'desc', 'summary': 'Hello!',
'is_experimental': True}
cat_form = formset(cat_initial, initial_count=1)
license_form = {'builtin': 3}
policy_form = {} if minimal else {
'has_priv': True, 'privacy_policy': 'Ur data belongs to us now.'}
reviewer_form = {} if minimal else {'approvalnotes': 'approove plz'}
result.update(describe_form)
result.update(cat_form)
result.update(license_form)
result.update(policy_form)
result.update(reviewer_form)
result.update(**kw)
result.update(fs)
return result
def test_submit_success(self):
def is_success(self, data):
assert self.get_addon().status == amo.STATUS_NULL
r = self.client.post(self.url, data)
assert r.status_code == 302
assert self.get_addon().status == amo.STATUS_NOMINATED
pytest.raises(SubmitStep.DoesNotExist, self.get_step)
return r
def test_submit_success_minimal(self):
# Set/change the required fields only
r = self.client.get(self.url)
assert r.status_code == 200
# Post and be redirected - trying to sneak
# in fields that shouldn't be modified via this form.
d = self.get_dict(homepage='foo.com',
support_email='foo@mozilla.com',
support_url='baz.com',
tags='whatevs, whatever')
r = self.client.post(self.url, d)
assert r.status_code == 302
assert self.get_step().step == 4
self.is_success(d)
addon = self.get_addon()
# This fields should not have been modified.
assert addon.homepage != 'foo.com'
assert addon.support_email != 'foo@mozilla.com'
assert addon.support_url != 'baz.com'
assert len(addon.tags.values_list()) == 0
# These are the field that are expected to be
# edited here.
# These are the fields that are expected to be edited here.
assert addon.name == 'Test name'
assert addon.slug == 'testname'
assert addon.description == 'desc'
assert addon.summary == 'Hello!'
assert addon.is_experimental
# Test add-on log activity.
log_items = ActivityLog.objects.for_addons(addon)
assert not log_items.filter(action=amo.LOG.EDIT_DESCRIPTIONS.id), (
"Creating a description needn't be logged.")
assert not log_items.filter(action=amo.LOG.EDIT_PROPERTIES.id), (
"Setting properties on submit needn't be logged.")
def test_submit_unlisted_addon(self):
self.addon.update(is_listed=False)
response = self.client.get(self.url)
assert response.status_code == 200
# Post and be redirected.
response = self.client.post(self.url, {'name': 'unlisted addon',
'slug': 'unlisted-addon',
'summary': 'summary'})
assert response.status_code == 302
assert response.url.endswith(reverse('devhub.submit.6',
args=['unlisted-addon']))
# Unlisted addons don't need much info, and their queue is chosen
# automatically on step 2, so we skip steps 4, 5 and 6. We thus have no
# more steps at that point.
assert not SubmitStep.objects.filter(addon=self.addon).exists()
def test_submit_success_optional_fields(self):
# Set/change the optional fields too
# Post and be redirected
d = self.get_dict(minimal=False)
self.is_success(d)
addon = self.get_addon()
assert addon.name == 'unlisted addon'
assert addon.slug == 'unlisted-addon'
assert addon.summary == 'summary'
# Test add-on log activity.
log_items = ActivityLog.objects.for_addons(addon)
assert not log_items.filter(action=amo.LOG.EDIT_DESCRIPTIONS.id), (
"Creating a description needn't be logged.")
# These are the fields that are expected to be edited here.
assert addon.support_url == 'http://stackoverflow.com'
assert addon.support_email == 'black@hole.org'
assert addon.privacy_policy == 'Ur data belongs to us now.'
def test_submit_name_unique(self):
# Make sure name is unique.
@ -1431,22 +1437,7 @@ class TestSubmitStep3(TestSubmitBase):
assert get_addon_count('Cooliris') == 1
# It's allowed for the '3615' listed add-on to reuse the same name as
# the other 'Cooliris' unlisted add-on.
response = self.client.post(self.url, self.get_dict(name='Cooliris'))
assert response.status_code == 302
assert get_addon_count('Cooliris') == 2
def test_submit_unlisted_name_not_unique(self):
"""Unlisted add-ons names aren't unique."""
# Change the existing add-on with the 'Cooliris' name to be unlisted.
Addon.objects.get(name__localized_string='Cooliris').update(
is_listed=False)
# Change the '3615' add-on to be unlisted.
Addon.objects.get(pk=3615).update(is_listed=False)
assert get_addon_count('Cooliris') == 1
# It's allowed for the '3615' unlisted add-on to reuse the same name as
# the other 'Cooliris' unlisted add-on.
response = self.client.post(self.url, self.get_dict(name='Cooliris'))
assert response.status_code == 302
self.is_success(self.get_dict(name='Cooliris'))
assert get_addon_count('Cooliris') == 2
def test_submit_name_unique_strip(self):
@ -1516,7 +1507,7 @@ class TestSubmitStep3(TestSubmitBase):
assert [c.id for c in self.get_addon().all_categories] == [22]
self.cat_initial['categories'] = [22, 1]
self.client.post(self.url, self.get_dict())
self.is_success(self.get_dict())
addon_cats = self.get_addon().categories.values_list('id', flat=True)
assert sorted(addon_cats) == [1, 22]
@ -1542,232 +1533,33 @@ class TestSubmitStep3(TestSubmitBase):
category_ids_new = [cat.id for cat in self.get_addon().all_categories]
assert category_ids_new == [22]
def test_check_version(self):
r = self.client.get(self.url)
doc = pq(r.content)
version = doc("#current_version").val()
assert version == self.addon.current_version.version
class TestSubmitStep4(TestSubmitBase):
def setUp(self):
super(TestSubmitStep4, self).setUp()
SubmitStep.objects.create(addon_id=3615, step=4)
self.url = reverse('devhub.submit.4', args=['a3615'])
self.next_step = reverse('devhub.submit.5', args=['a3615'])
self.icon_upload = reverse('devhub.addons.upload_icon',
args=['a3615'])
self.preview_upload = reverse('devhub.addons.upload_preview',
args=['a3615'])
def test_get(self):
assert self.client.get(self.url).status_code == 200
def test_post(self):
data = dict(icon_type='')
data_formset = self.formset_media(**data)
r = self.client.post(self.url, data_formset)
assert r.status_code == 302
assert self.get_step().step == 5
def formset_new_form(self, *args, **kw):
ctx = self.client.get(self.url).context
blank = initial(ctx['preview_form'].forms[-1])
blank.update(**kw)
return blank
def formset_media(self, *args, **kw):
kw.setdefault('initial_count', 0)
kw.setdefault('prefix', 'files')
fs = formset(*[a for a in args] + [self.formset_new_form()], **kw)
return dict([(k, '' if v is None else v) for k, v in fs.items()])
def test_icon_upload_attributes(self):
doc = pq(self.client.get(self.url).content)
field = doc('input[name=icon_upload]')
assert field.length == 1
assert sorted(field.attr('data-allowed-types').split('|')) == (
['image/jpeg', 'image/png'])
assert field.attr('data-upload-url') == self.icon_upload
def test_edit_media_defaulticon(self):
data = dict(icon_type='')
data_formset = self.formset_media(**data)
self.client.post(self.url, data_formset)
addon = self.get_addon()
assert addon.get_icon_url(64).endswith('icons/default-64.png')
for k in data:
assert unicode(getattr(addon, k)) == data[k]
def test_edit_media_preuploadedicon(self):
data = dict(icon_type='icon/appearance')
data_formset = self.formset_media(**data)
self.client.post(self.url, data_formset)
addon = self.get_addon()
assert '/'.join(addon.get_icon_url(64).split('/')[-2:]) == (
'addon-icons/appearance-64.png')
for k in data:
assert unicode(getattr(addon, k)) == data[k]
def test_edit_media_uploadedicon(self):
with open(get_image_path('mozilla.png'), 'rb') as filehandle:
data = {'upload_image': filehandle}
response = self.client.post(self.icon_upload, data)
response_json = json.loads(response.content)
addon = self.get_addon()
# Now, save the form so it gets moved properly.
data = dict(icon_type='image/png',
icon_upload_hash=response_json['upload_hash'])
data_formset = self.formset_media(**data)
self.client.post(self.url, data_formset)
addon = self.get_addon()
# Sad we're hardcoding /3/ here, but that's how the URLs work
_url = addon.get_icon_url(64).split('?')[0]
assert _url.endswith('addon_icons/3/%s-64.png' % addon.id)
assert data['icon_type'] == 'image/png'
# Check that it was actually uploaded
dirname = os.path.join(user_media_path('addon_icons'),
'%s' % (addon.id / 1000))
dest = os.path.join(dirname, '%s-32.png' % addon.id)
assert storage.exists(dest)
assert Image.open(storage.open(dest)).size == (32, 12)
def test_edit_media_uploadedicon_noresize(self):
with open('static/img/notifications/error.png', 'rb') as filehandle:
data = {'upload_image': filehandle}
response = self.client.post(self.icon_upload, data)
response_json = json.loads(response.content)
addon = self.get_addon()
# Now, save the form so it gets moved properly.
data = dict(icon_type='image/png',
icon_upload_hash=response_json['upload_hash'])
data_formset = self.formset_media(**data)
self.client.post(self.url, data_formset)
addon = self.get_addon()
# Sad we're hardcoding /3/ here, but that's how the URLs work
_url = addon.get_icon_url(64).split('?')[0]
assert _url.endswith('addon_icons/3/%s-64.png' % addon.id)
assert data['icon_type'] == 'image/png'
# Check that it was actually uploaded
dirname = os.path.join(user_media_path('addon_icons'),
'%s' % (addon.id / 1000))
dest = os.path.join(dirname, '%s-64.png' % addon.id)
assert storage.exists(dest)
assert Image.open(storage.open(dest)).size == (48, 48)
def test_client_lied(self):
with open(get_image_path('non-animated.gif'), 'rb') as filehandle:
data = {'upload_image': filehandle}
res = self.client.post(self.preview_upload, data)
response_json = json.loads(res.content)
assert response_json['errors'][0] == (
u'Images must be either PNG or JPG.')
def test_client_error_triggers_tmp_image_cleanup(self):
with open(get_image_path('non-animated.gif'), 'rb') as filehandle:
data = {'upload_image': filehandle, 'upload_type': 'preview'}
self.client.post(self.preview_upload, data)
assert not os.listdir(os.path.join(settings.TMP_PATH, 'preview'))
def test_image_animated(self):
with open(get_image_path('animated.png'), 'rb') as filehandle:
data = {'upload_image': filehandle}
res = self.client.post(self.preview_upload, data)
response_json = json.loads(res.content)
assert response_json['errors'][0] == u'Images cannot be animated.'
def test_icon_non_animated(self):
with open(get_image_path('non-animated.png'), 'rb') as filehandle:
data = {'icon_type': 'image/png', 'icon_upload': filehandle}
data_formset = self.formset_media(**data)
res = self.client.post(self.url, data_formset)
assert res.status_code == 302
assert self.get_step().step == 5
class TestSubmitStep5(TestSubmitBase):
"""License submission."""
def setUp(self):
super(TestSubmitStep5, self).setUp()
SubmitStep.objects.create(addon_id=self.addon.id, step=5)
self.url = reverse('devhub.submit.5', args=['a3615'])
self.next_step = reverse('devhub.submit.6', args=['a3615'])
License.objects.create(builtin=3, on_form=True)
def test_get(self):
assert self.client.get(self.url).status_code == 200
def test_set_license(self):
r = self.client.post(self.url, {'builtin': 3})
self.assert3xx(r, self.next_step)
def test_set_license_no_log(self):
self.is_success(self.get_dict(builtin=3))
assert self.get_addon().current_version.license.builtin == 3
pytest.raises(SubmitStep.DoesNotExist, self.get_step)
log_items = ActivityLog.objects.for_addons(self.get_addon())
assert not log_items.filter(action=amo.LOG.CHANGE_LICENSE.id), (
"Initial license choice:6 needn't be logged.")
assert not log_items.filter(action=amo.LOG.CHANGE_LICENSE.id)
def test_license_error(self):
r = self.client.post(self.url, {'builtin': 4})
r = self.client.post(self.url, self.get_dict(builtin=4))
assert r.status_code == 200
self.assertFormError(r, 'license_form', 'builtin',
'Select a valid choice. 4 is not one of '
'the available choices.')
assert self.get_step().step == 5
assert self.get_step().step == 4
def test_set_eula(self):
self.get_addon().update(eula=None, privacy_policy=None)
r = self.client.post(self.url, dict(builtin=3, has_eula=True,
eula='xxx'))
self.assert3xx(r, self.next_step)
assert unicode(self.get_addon().eula) == 'xxx'
pytest.raises(SubmitStep.DoesNotExist, self.get_step)
def test_set_eula_nomsg(self):
def test_set_privacy_nomsg(self):
"""
You should not get punished with a 500 for not writing your EULA...
You should not get punished with a 500 for not writing your policy...
but perhaps you should feel shame for lying to us. This test does not
test for shame.
"""
self.get_addon().update(eula=None, privacy_policy=None)
r = self.client.post(self.url, dict(builtin=3, has_eula=True))
self.assert3xx(r, self.next_step)
pytest.raises(SubmitStep.DoesNotExist, self.get_step)
self.is_success(self.get_dict(has_priv=True))
def test_status_is_nominated(self):
def test_nomination_date_set_only_once(self):
self.get_version().update(nomination=None)
r = self.client.post(self.url, {'builtin': 3})
assert r.status_code == 302
addon = self.get_addon()
assert addon.status == amo.STATUS_NOMINATED
self.is_success(self.get_dict())
self.assertCloseToNow(self.get_version().nomination)
pytest.raises(SubmitStep.DoesNotExist, self.get_step)
# Check nomination date is only set once, see bug 632191.
nomdate = datetime.now() - timedelta(days=5)
@ -1778,11 +1570,11 @@ class TestSubmitStep5(TestSubmitBase):
nomdate.timetuple()[0:5])
class TestSubmitStep6(TestSubmitBase):
class TestSubmitStepDone(TestSubmitBase):
def setUp(self):
super(TestSubmitStep6, self).setUp()
self.url = reverse('devhub.submit.6', args=[self.addon.slug])
super(TestSubmitStepDone, self).setUp()
self.url = reverse('devhub.submit.5', args=[self.addon.slug])
@mock.patch.object(settings, 'SITE_URL', 'http://b.ro')
@mock.patch('olympia.devhub.tasks.send_welcome_email.delay')
@ -1808,7 +1600,7 @@ class TestSubmitStep6(TestSubmitBase):
assert not send_welcome_email_mock.called
@mock.patch('olympia.devhub.tasks.send_welcome_email.delay', new=mock.Mock)
def test_finish_submitting_addon(self):
def test_finish_submitting_listed_addon(self):
assert self.addon.current_version.supported_platforms == (
[amo.PLATFORM_ALL])
@ -1816,79 +1608,36 @@ class TestSubmitStep6(TestSubmitBase):
assert r.status_code == 200
doc = pq(r.content)
a = doc('a#submitted-addon-url')
url = self.addon.get_url_path()
assert a.attr('href') == url
assert a.text() == absolutify(url)
next_steps = doc('.done-next-steps li a')
# edit listing of freshly submitted add-on...
assert next_steps.eq(0).attr('href') == self.addon.get_dev_url()
# edit your developer profile...
assert next_steps.eq(1).attr('href') == (
self.addon.get_dev_url('profile'))
content = doc('.addon-submission-process')
links = content('a')
assert len(links) == 2
# First link is to edit listing
assert links[0].attrib['href'] == self.addon.get_dev_url()
# Second link is to edit the version
assert links[1].attrib['href'] == reverse(
'devhub.versions.edit',
args=[self.addon.slug, self.addon.current_version.id])
assert links[1].text == (
'Edit version %s' % self.addon.current_version.version)
@mock.patch('olympia.devhub.tasks.send_welcome_email.delay', new=mock.Mock)
def test_finish_submitting_unlisted_addon(self):
self.addon.update(is_listed=False, status=amo.STATUS_NOMINATED)
r = self.client.get(self.url)
assert r.status_code == 200
doc = pq(r.content)
# For unlisted add-ons, there's only the devhub page link displayed and
# a link to the forum page on the wait times.
content = doc('.done-next-steps')
assert len(content('a')) == 2
assert content('a').eq(0).attr('href') == self.addon.get_dev_url()
@mock.patch('olympia.devhub.tasks.send_welcome_email.delay', new=mock.Mock)
def test_finish_submitting_unlisted_addon_signed(self):
self.addon.update(is_listed=False, status=amo.STATUS_PUBLIC)
r = self.client.get(self.url)
assert r.status_code == 200
doc = pq(r.content)
# For unlisted addon that are already signed, show a url to the devhub
# versions page and to the addon listing.
content = doc('.addon-submission-process')
links = content('a')
assert len(links) == 2
assert links[0].attrib['href'] == reverse(
'devhub.versions.edit',
args=[self.addon.slug, self.addon.current_version.id])
assert links[1].attrib['href'] == self.addon.get_dev_url()
@mock.patch('olympia.devhub.tasks.send_welcome_email.delay', new=mock.Mock)
def test_finish_submitting_platform_specific_addon(self):
# mac-only Add-on:
addon = Addon.objects.get(name__localized_string='Cooliris')
addon.addonuser_set.create(user_id=55021)
r = self.client.get(reverse('devhub.submit.6', args=[addon.slug]))
assert r.status_code == 200
next_steps = pq(r.content)('.done-next-steps li a')
# upload more platform specific files...
assert next_steps.eq(0).attr('href') == (
reverse('devhub.versions.edit',
kwargs=dict(addon_id=addon.slug,
version_id=addon.current_version.id)))
# edit listing of freshly submitted add-on...
assert next_steps.eq(1).attr('href') == addon.get_dev_url()
@mock.patch('olympia.devhub.tasks.send_welcome_email.delay', new=mock.Mock)
def test_finish_addon_for_full_review(self):
self.addon.update(status=amo.STATUS_NOMINATED)
response = self.client.get(self.url)
assert response.status_code == 200
doc = pq(response.content)
intro = doc('.addon-submission-process p').text().strip()
assert 'New Add-on' in intro
# First link is to the file download.
file_ = self.addon.latest_version.all_files[0]
assert links[0].attrib['href'] == file_.get_url_path('devhub')
assert links[0].text == (
'Download %s' % file_.filename)
# Second back to my submissions.
assert links[1].attrib['href'] == reverse('devhub.addons')
@mock.patch('olympia.devhub.tasks.send_welcome_email.delay', new=mock.Mock)
def test_incomplete_addon_no_versions(self):
@ -1897,25 +1646,6 @@ class TestSubmitStep6(TestSubmitBase):
r = self.client.get(self.url, follow=True)
self.assert3xx(r, self.addon.get_dev_url('versions'), 302)
@mock.patch('olympia.devhub.tasks.send_welcome_email.delay', new=mock.Mock)
def test_link_to_activityfeed(self):
r = self.client.get(self.url, follow=True)
doc = pq(r.content)
assert doc('.done-next-steps a').eq(2).attr('href') == (
reverse('devhub.feed', args=[self.addon.slug]))
@mock.patch('olympia.devhub.tasks.send_welcome_email.delay', new=mock.Mock)
def test_display_non_ascii_url(self):
u = 'フォクすけといっしょ'
self.addon.update(slug=u)
r = self.client.get(reverse('devhub.submit.6', args=[u]))
assert r.status_code == 200
# The meta charset will always be utf-8.
doc = pq(r.content.decode('utf-8'))
assert doc('#submitted-addon-url').text() == (
u'%s/en-US/firefox/addon/%s/' % (
settings.SITE_URL, u.decode('utf8')))
class TestResumeStep(TestSubmitBase):
@ -1929,7 +1659,7 @@ class TestResumeStep(TestSubmitBase):
def test_step_redirects(self):
SubmitStep.objects.create(addon_id=3615, step=1)
for i in xrange(3, 6):
for i in xrange(4, 5):
SubmitStep.objects.filter(addon=self.get_addon()).update(step=i)
r = self.client.get(self.url, follow=True)
self.assert3xx(r, reverse('devhub.submit.%s' % i,
@ -1967,93 +1697,34 @@ class TestSubmitSteps(TestCase):
assert self.client.login(email='del@icio.us')
self.user = UserProfile.objects.get(email='del@icio.us')
def assert_linked(self, doc, numbers):
"""Check that the nth <li> in the steps list is a link."""
lis = doc('.submit-addon-progress li')
assert len(lis) == 6
for idx, li in enumerate(lis):
links = pq(li)('a')
if (idx + 1) in numbers:
assert len(links) == 1
else:
assert len(links) == 0
def assert_highlight(self, doc, num):
"""Check that the nth <li> is marked as .current."""
lis = doc('.submit-addon-progress li')
assert pq(lis[num - 1]).hasClass('current')
assert len(pq('.current', lis)) == 1
def test_step_1(self):
self.user.update(read_dev_agreement=None)
r = self.client.get(reverse('devhub.submit.1'))
assert r.status_code == 200
def test_on_step_5(self):
def test_on_step_4(self):
# Hitting the step we're supposed to be on is a 200.
SubmitStep.objects.create(addon_id=3615, step=5)
r = self.client.get(reverse('devhub.submit.5',
SubmitStep.objects.create(addon_id=3615, step=4)
r = self.client.get(reverse('devhub.submit.4',
args=['a3615']))
assert r.status_code == 200
def test_skip_step_5(self):
# We get bounced back to step 3.
SubmitStep.objects.create(addon_id=3615, step=3)
def test_skip_step_4(self):
# We get bounced back to step 4.
SubmitStep.objects.create(addon_id=3615, step=4)
r = self.client.get(reverse('devhub.submit.5',
args=['a3615']), follow=True)
self.assert3xx(r, reverse('devhub.submit.3', args=['a3615']))
self.assert3xx(r, reverse('devhub.submit.4', args=['a3615']))
def test_all_done(self):
# There's no SubmitStep, so we must be done.
r = self.client.get(reverse('devhub.submit.5',
r = self.client.get(reverse('devhub.submit.4',
args=['a3615']), follow=True)
self.assert3xx(r, reverse('devhub.submit.6', args=['a3615']))
self.assert3xx(r, reverse('devhub.submit.5', args=['a3615']))
def test_menu_step_1(self):
self.user.update(read_dev_agreement=None)
doc = pq(self.client.get(reverse('devhub.submit.1')).content)
self.assert_linked(doc, [1])
self.assert_highlight(doc, 1)
def test_menu_step_2(self):
self.client.post(reverse('devhub.submit.1'))
doc = pq(self.client.get(reverse('devhub.submit.2')).content)
self.assert_linked(doc, [2])
self.assert_highlight(doc, 2)
def test_menu_step_3(self):
SubmitStep.objects.create(addon_id=3615, step=3)
url = reverse('devhub.submit.3', args=['a3615'])
doc = pq(self.client.get(url).content)
self.assert_linked(doc, [3])
self.assert_highlight(doc, 3)
def test_menu_step_3_from_5(self):
SubmitStep.objects.create(addon_id=3615, step=5)
url = reverse('devhub.submit.3', args=['a3615'])
doc = pq(self.client.get(url).content)
self.assert_linked(doc, [3, 4, 5])
self.assert_highlight(doc, 3)
def test_menu_step_6(self):
url = reverse('devhub.submit.6', args=['a3615'])
doc = pq(self.client.get(url).content)
self.assert_linked(doc, [])
self.assert_highlight(doc, 6)
def test_menu_step_6_unlisted(self):
SubmitStep.objects.create(addon_id=3615, step=6)
Addon.objects.get(pk=3615).update(is_listed=False)
url = reverse('devhub.submit.6', args=['a3615'])
doc = pq(self.client.get(url).content)
self.assert_linked(doc, []) # Last step: no previous step linked.
# Skipped from step 3 to 6, as unlisted add-ons don't need listing
# information. Thus none of the steps from 4 to 5 should be there.
# For reference, the steps that are with the "listed" class (instead of
# "all") aren't displayed.
assert len(doc('.submit-addon-progress li.all')) == 4
# The step 6 is thus the 4th visible in the list.
self.assert_highlight(doc, 6) # Current step is still the 6th.
def test_submit_no_step_redirects_to_done(self):
r = self.client.get('developers/addon/a3615/submit/', follow=True)
self.assert3xx(r, reverse('devhub.submit.5', args=['a3615']))
class TestUpload(BaseUploadTest):
@ -2754,10 +2425,11 @@ class TestUploadErrors(UploadTest):
@mock.patch.object(waffle, 'flag_is_active', return_value=True)
@mock.patch('olympia.devhub.tasks.validate')
@mock.patch('olympia.devhub.tasks.run_validator')
def test_dupe_xpi(self, run_validator, validate_, flag_is_active):
def test_dupe_xpi(self, run_validator, validate_, flag_is_active, **kw):
# Submit a new addon:
self.client.post(reverse('devhub.submit.1')) # set cookie
res = self.client.get(reverse('devhub.submit.2'))
channel = kw.get('channel', 'listed')
res = self.client.get(reverse('devhub.submit.3', args=[channel]))
assert res.status_code == 200
doc = pq(res.content)
@ -2771,7 +2443,7 @@ class TestUploadErrors(UploadTest):
upload = FileUpload.objects.get(uuid=data['upload'])
# Check that `tasks.validate` has been called with the expected upload.
validate_.assert_called_with(upload, listed=True)
validate_.assert_called_with(upload, listed=(channel == 'listed'))
# Poll and check that we are still pending validation.
data = json.loads(self.client.get(poll_url).content)
@ -2791,7 +2463,7 @@ class TestUploadErrors(UploadTest):
def test_dupe_xpi_unlisted_addon(self):
"""Submitting an xpi with the same UUID as an unlisted addon."""
self.addon.update(is_listed=False)
self.test_dupe_xpi()
self.test_dupe_xpi(channel='unlisted')
class AddVersionTest(UploadTest):
@ -3141,16 +2813,24 @@ class TestVersionXSS(UploadTest):
assert '&amp;lt;script&amp;gt;alert' in r.content
class UploadAddon(object):
class TestCreateAddon(BaseUploadTest, TestCase):
fixtures = ['base/users']
def setUp(self):
super(TestCreateAddon, self).setUp()
self.upload = self.get_upload('extension.xpi')
assert self.client.login(email='regular@mozilla.com')
self.client.post(reverse('devhub.submit.1'))
def post(self, supported_platforms=None, expect_errors=False,
source=None, is_listed=True, status_code=200):
if supported_platforms is None:
supported_platforms = [amo.PLATFORM_ALL]
d = dict(upload=self.upload.uuid.hex, source=source,
supported_platforms=[p.id for p in supported_platforms],
is_unlisted=not is_listed)
r = self.client.post(self.url, d, follow=True)
supported_platforms=[p.id for p in supported_platforms])
url = reverse('devhub.submit.3',
args=['listed' if is_listed else 'unlisted'])
r = self.client.post(url, d, follow=True)
assert r.status_code == status_code
if not expect_errors:
# Show any unexpected form errors.
@ -3158,17 +2838,6 @@ class UploadAddon(object):
assert r.context['new_addon_form'].errors.as_text() == ''
return r
class TestCreateAddon(BaseUploadTest, UploadAddon, TestCase):
fixtures = ['base/users']
def setUp(self):
super(TestCreateAddon, self).setUp()
self.upload = self.get_upload('extension.xpi')
self.url = reverse('devhub.submit.2')
assert self.client.login(email='regular@mozilla.com')
self.client.post(reverse('devhub.submit.1'))
def assert_json_error(self, *args):
UploadTest().assert_json_error(self, *args)
@ -3210,7 +2879,7 @@ class TestCreateAddon(BaseUploadTest, UploadAddon, TestCase):
addon = Addon.objects.get()
assert addon.is_listed
assert addon.latest_version.channel == amo.RELEASE_CHANNEL_LISTED
self.assert3xx(r, reverse('devhub.submit.3', args=[addon.slug]))
self.assert3xx(r, reverse('devhub.submit.4', args=[addon.slug]))
log_items = ActivityLog.objects.for_addons(addon)
assert log_items.filter(action=amo.LOG.CREATE_ADDON.id), (
'New add-on creation never logged.')
@ -3256,7 +2925,8 @@ class TestCreateAddon(BaseUploadTest, UploadAddon, TestCase):
assert mock_sign_file.called
def test_missing_platforms(self):
r = self.client.post(self.url, dict(upload=self.upload.uuid.hex))
url = reverse('devhub.submit.3', args=['listed'])
r = self.client.post(url, dict(upload=self.upload.uuid.hex))
assert r.status_code == 200
assert r.context['new_addon_form'].errors.as_text() == (
'* supported_platforms\n * Need at least one platform.')
@ -3269,7 +2939,7 @@ class TestCreateAddon(BaseUploadTest, UploadAddon, TestCase):
r = self.post(supported_platforms=[amo.PLATFORM_MAC,
amo.PLATFORM_LINUX])
addon = Addon.objects.get()
self.assert3xx(r, reverse('devhub.submit.3', args=[addon.slug]))
self.assert3xx(r, reverse('devhub.submit.4', args=[addon.slug]))
all_ = sorted([f.filename for f in addon.current_version.all_files])
assert all_ == [u'xpi_name-0.1-linux.xpi', u'xpi_name-0.1-mac.xpi']
@ -3281,7 +2951,7 @@ class TestCreateAddon(BaseUploadTest, UploadAddon, TestCase):
amo.PLATFORM_LINUX],
is_listed=False)
addon = Addon.unfiltered.get()
self.assert3xx(r, reverse('devhub.submit.3', args=[addon.slug]))
self.assert3xx(r, reverse('devhub.submit.5', args=[addon.slug]))
all_ = sorted([f.filename for f in addon.current_version.all_files])
assert all_ == [u'xpi_name-0.1-linux.xpi', u'xpi_name-0.1-mac.xpi']
mock_auto_sign_file.assert_has_calls(
@ -3295,7 +2965,7 @@ class TestCreateAddon(BaseUploadTest, UploadAddon, TestCase):
assert Addon.objects.count() == 0
r = self.post(source=source)
addon = Addon.objects.get()
self.assert3xx(r, reverse('devhub.submit.3', args=[addon.slug]))
self.assert3xx(r, reverse('devhub.submit.4', args=[addon.slug]))
assert addon.current_version.source
assert Addon.objects.get(pk=addon.pk).admin_review

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

@ -14,11 +14,9 @@ PACKAGE_NAME = '(?P<package_name>[_\w]+)'
# These will all start with /addon/<addon_id>/submit/
submit_patterns = patterns(
'',
url('^$', lambda r, addon_id: redirect('devhub.submit.6', addon_id)),
url('^3$', views.submit_describe, name='devhub.submit.3'),
url('^4$', views.submit_media, name='devhub.submit.4'),
url('^5$', views.submit_license, name='devhub.submit.5'),
url('^6$', views.submit_done, name='devhub.submit.6'),
url('^$', lambda r, addon_id: redirect('devhub.submit.5', addon_id)),
url('^details$', views.submit_describe, name='devhub.submit.4'),
url('^finish$', views.submit_done, name='devhub.submit.5'),
url('^bump$', views.submit_bump, name='devhub.submit.bump'),
)
@ -145,11 +143,14 @@ urlpatterns = decorate(write, patterns(
# Add-on submission
url('^addon/submit/$',
lambda r: redirect('devhub.submit.1', permanent=True)),
url('^addon/submit/1$', views.submit, name='devhub.submit.1'),
url('^addon/submit/2$', views.submit_addon, name='devhub.submit.2'),
url('^addon/submit/agreement$', views.submit, name='devhub.submit.1'),
url('^addon/submit/distribution$', views.submit_addon_distribute,
name='devhub.submit.2'),
url('^addon/submit/upload-(?P<channel>listed|unlisted)$',
views.submit_addon_upload, name='devhub.submit.3'),
# Submission API
url('^addon/submit/agreement/$', views.api_key_agreement,
url('^addon/agreement/$', views.api_key_agreement,
name='devhub.api_key_agreement'),
url('^addon/api/key/$', views.api_key, name='devhub.api_key'),

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

@ -591,8 +591,7 @@ def compat_application_versions(request):
def validate_addon(request):
return render(request, 'devhub/validate_addon.html',
{'title': _('Validate Add-on'),
# Hack: we just need the "is_unlisted" field from this form.
'new_addon_form': forms.NewAddonForm(
'new_addon_form': forms.StandaloneValidationForm(
None, None, request=request)})
@ -602,8 +601,7 @@ def check_addon_compatibility(request):
return render(request, 'devhub/validate_addon.html',
{'appversion_form': form,
'title': _('Check Add-on Compatibility'),
# Hack: we just need the "is_unlisted" field from this form.
'new_addon_form': forms.NewAddonForm(
'new_addon_form': forms.StandaloneValidationForm(
None, None, request=request)})
@ -1395,7 +1393,7 @@ def submit_step(outer_step):
@functools.wraps(f)
def wrapper(request, *args, **kw):
step = outer_step
max_step = 6
max_step = 5
# We only bounce on pages with an addon id.
if 'addon' in kw:
addon = kw['addon']
@ -1407,7 +1405,7 @@ def submit_step(outer_step):
return redirect(_step_url(max_step), addon.slug)
elif step != max_step:
# We couldn't find a step, so we must be done.
return redirect(_step_url(6), addon.slug)
return redirect(_step_url(5), addon.slug)
kw['step'] = Step(step, max_step)
return f(request, *args, **kw)
# Tell @dev_required that this is a function in the submit flow so it
@ -1432,22 +1430,34 @@ def submit(request, step):
@login_required
@submit_step(2)
@transaction.atomic
def submit_addon(request, step):
def submit_addon_distribute(request, step):
if request.user.read_dev_agreement is None:
return redirect(_step_url(1))
form = forms.DistributionChoiceForm(request.POST)
if request.method == 'POST' and form.is_valid():
data = form.cleaned_data
return redirect(_step_url(3), data['choices'])
return render(request, 'devhub/addons/submit/distribute.html',
{'step': step, 'distribution_form': form})
@login_required
@submit_step(3)
@transaction.atomic
def submit_addon_upload(request, step, channel):
form = forms.NewAddonForm(
request.POST or None,
request.FILES or None,
request=request
)
is_listed = channel == 'listed'
if request.method == 'POST':
if form.is_valid():
data = form.cleaned_data
p = data.get('supported_platforms', [])
is_listed = not data['is_unlisted']
addon = Addon.from_upload(data['upload'], p, source=data['source'],
is_listed=is_listed)
AddonUser(addon=addon, user=request.user).save()
@ -1457,92 +1467,53 @@ def submit_addon(request, step):
addon.update(status=amo.STATUS_NOMINATED)
# Sign all the files submitted, one for each platform.
auto_sign_version(addon.versions.get())
SubmitStep.objects.create(addon=addon, step=3)
return redirect(_step_url(3), addon.slug)
return redirect('devhub.submit.5', addon.slug)
SubmitStep.objects.create(addon=addon, step=4)
return redirect(_step_url(4), addon.slug)
is_admin = acl.action_allowed(request, 'ReviewerAdminTools', 'View')
return render(request, 'devhub/addons/submit/upload.html',
{'step': step, 'new_addon_form': form, 'is_admin': is_admin})
@dev_required
@submit_step(3)
def submit_describe(request, addon_id, addon, step):
form_cls = forms.Step3Form
form = form_cls(request.POST or None, instance=addon, request=request)
cat_form = addon_forms.CategoryFormSet(request.POST or None, addon=addon,
request=request)
if request.method == 'POST' and form.is_valid() and (
not addon.is_listed or cat_form.is_valid()):
addon = form.save(addon)
submit_step = SubmitStep.objects.filter(addon=addon)
if addon.is_listed:
cat_form.save()
submit_step.update(step=4)
return redirect(_step_url(4), addon.slug)
else: # Finished for unlisted addons.
submit_step.delete()
signals.submission_done.send(sender=addon)
return redirect('devhub.submit.6', addon.slug)
return render(request, 'devhub/addons/submit/describe.html',
{'form': form, 'cat_form': cat_form, 'addon': addon,
'step': step})
{'step': step, 'new_addon_form': form, 'is_admin': is_admin,
'listed': is_listed})
@dev_required
@submit_step(4)
def submit_media(request, addon_id, addon, step):
form_icon = addon_forms.AddonFormMedia(
request.POST or None,
request.FILES or None, instance=addon, request=request)
form_previews = forms.PreviewFormSet(
request.POST or None,
prefix='files', queryset=addon.previews.all())
def submit_describe(request, addon_id, addon, step):
forms_, context = [], {}
describe_form = forms.DescribeForm(request.POST or None, instance=addon,
request=request)
cat_form = addon_forms.CategoryFormSet(
request.POST or None, addon=addon, request=request)
license_form = forms.LicenseForm(request.POST or None, addon=addon)
policy_form = forms.PolicyForm(request.POST or None, addon=addon)
reviewer_form = forms.ReviewerNotesForm(
request.POST or None, instance=addon.latest_version)
if (request.method == 'POST' and
form_icon.is_valid() and form_previews.is_valid()):
addon = form_icon.save(addon)
context.update(license_form.get_context())
context.update(form=describe_form, cat_form=cat_form,
policy_form=policy_form, reviewer_form=reviewer_form)
forms_.extend([describe_form, cat_form, context['license_form'],
policy_form, reviewer_form])
for preview in form_previews.forms:
preview.save(addon)
if request.method == 'POST' and all([form.is_valid() for form in forms_]):
addon = describe_form.save(addon)
cat_form.save()
license_form.save(log=False)
policy_form.save()
reviewer_form.save()
addon.update(status=amo.STATUS_NOMINATED)
SubmitStep.objects.filter(addon=addon).update(step=5)
SubmitStep.objects.filter(addon=addon).delete()
signals.submission_done.send(sender=addon)
return redirect(_step_url(5), addon.slug)
return render(request, 'devhub/addons/submit/media.html',
{'form': form_icon, 'addon': addon, 'step': step,
'preview_form': form_previews})
return redirect('devhub.submit.5', addon.slug)
context.update(addon=addon, step=step)
return render(request, 'devhub/addons/submit/describe.html', context)
@dev_required
@submit_step(5)
def submit_license(request, addon_id, addon, step):
fs, ctx = [], {}
# Versions.
license_form = forms.LicenseForm(request.POST or None, addon=addon)
ctx.update(license_form.get_context())
fs.append(ctx['license_form'])
# Policy.
policy_form = forms.PolicyForm(request.POST or None, addon=addon)
fs.append(policy_form)
if request.method == 'POST' and all([form.is_valid() for form in fs]):
if license_form in fs:
license_form.save(log=False)
policy_form.save()
addon.update(status=amo.STATUS_NOMINATED)
SubmitStep.objects.filter(addon=addon).delete()
signals.submission_done.send(sender=addon)
return redirect('devhub.submit.6', addon.slug)
ctx.update(addon=addon, policy_form=policy_form, step=step)
return render(request, 'devhub/addons/submit/license.html', ctx)
@dev_required
@submit_step(6)
def submit_done(request, addon_id, addon, step):
# Bounce to the versions page if they don't have any versions.
if not addon.versions.exists():

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

@ -129,6 +129,10 @@ span.remove {
cursor: help;
}
.addon-submit-distribute * label span.helptext {
font-weight: normal;
}
span.tip:hover,
a.remove:hover {
background-color: #2A4364 !important;

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

@ -70,13 +70,17 @@ ul.refinements:last-child {
width: 80%;
}
#submit-describe #trans-name {
#submit-describe #trans-name,
#submit-describe #trans-support_email,
#submit-describe #trans-support_url {
display: inline-block;
margin-right: 8px;
vertical-align: bottom;
}
#submit-describe #trans-name input {
#submit-describe #trans-name input,
#submit-describe #trans-support_email input,
#submit-describe #trans-support_url input {
width: 370px;
}

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

@ -339,7 +339,7 @@
$('.upload-status').remove();
}
$isUnlistedCheckbox.bind('change', updateListedStatus);
updateListedStatus();
if ($isUnlistedCheckbox.length) updateListedStatus();
$('#id_is_manual_review').bind('change', function() {
$('.addon-upload-dependant').prop('disabled', !($(this).is(':checked')));