Replace inline new version dialog with upload flow a-la new addon (#3940)

This commit is contained in:
Andrew Williamson 2016-11-09 17:58:15 +00:00 коммит произвёл GitHub
Родитель 433c033979
Коммит 4b46b39506
21 изменённых файлов: 951 добавлений и 232 удалений

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

@ -16,7 +16,7 @@ from quieter_formset.formset import BaseModelFormSet
from olympia.access import acl
from olympia import amo, paypal
from olympia.amo.helpers import mark_safe_lazy
from olympia.addons.forms import AddonFormBasic
from olympia.addons.forms import AddonFormBase, clean_addon_name
from olympia.addons.models import (
Addon, AddonDependency, AddonUser, Charity, Preview)
from olympia.amo.fields import HttpHttpsOnlyURLField
@ -530,23 +530,6 @@ class AddonUploadForm(WithSourceMixin, happyforms.Form):
u'upload. Please try again.'))
class NewAddonForm(AddonUploadForm):
supported_platforms = forms.TypedMultipleChoiceField(
choices=amo.SUPPORTED_PLATFORMS_CHOICES,
widget=forms.CheckboxSelectMultiple(attrs={'class': 'platform'}),
initial=[amo.PLATFORM_ALL.id],
coerce=int,
error_messages={'required': 'Need at least one platform.'}
)
def clean(self):
if not self.errors:
self._clean_upload()
# parse and validate the add-on
parse_addon(self.cleaned_data['upload'])
return self.cleaned_data
class StandaloneValidationForm(AddonUploadForm):
is_unlisted = forms.BooleanField(
initial=False,
@ -557,7 +540,15 @@ class StandaloneValidationForm(AddonUploadForm):
u'your own and only need it to be signed by Mozilla.'))
class NewVersionForm(NewAddonForm):
class NewVersionForm(AddonUploadForm):
supported_platforms = forms.TypedMultipleChoiceField(
choices=amo.SUPPORTED_PLATFORMS_CHOICES,
widget=forms.CheckboxSelectMultiple(attrs={'class': 'platform'}),
initial=[amo.PLATFORM_ALL.id],
coerce=int,
error_messages={'required': 'Need at least one platform.'}
)
beta = forms.BooleanField(
required=False,
help_text=_lazy(u'A file with a version ending with '
@ -565,7 +556,7 @@ class NewVersionForm(NewAddonForm):
u'detected as beta.'))
def __init__(self, *args, **kw):
self.addon = kw.pop('addon')
self.addon = kw.pop('addon', None)
super(NewVersionForm, self).__init__(*args, **kw)
def clean(self):
@ -573,7 +564,7 @@ class NewVersionForm(NewAddonForm):
self._clean_upload()
xpi = parse_addon(self.cleaned_data['upload'], self.addon)
# Make sure we don't already have the same non-rejected version.
version_exists = Version.unfiltered.filter(
version_exists = self.addon and Version.unfiltered.filter(
addon=self.addon, version=xpi['version']).exists()
if version_exists:
msg = _(u'Version %s already exists, or was uploaded before.')
@ -685,24 +676,45 @@ FileFormSet = modelformset_factory(File, formset=BaseFileFormSet,
form=FileForm, can_delete=True, extra=0)
class DescribeForm(AddonFormBasic):
tags = None
class DescribeForm(AddonFormBase):
name = TransField(max_length=50)
slug = forms.CharField(max_length=30)
summary = TransField(widget=TransTextarea(attrs={'rows': 4}),
max_length=250)
is_experimental = forms.BooleanField(required=False)
support_url = TransField.adapt(HttpHttpsOnlyURLField)(required=False)
support_email = TransField.adapt(forms.EmailField)(required=False)
has_priv = forms.BooleanField(
required=False, label=_lazy(u"This add-on has a Privacy Policy"),
label_suffix='')
privacy_policy = TransField(
widget=TransTextarea(), required=False,
label=_lazy(u"Please specify your add-on's Privacy Policy:"))
class Meta:
model = Addon
fields = ('name', 'slug', 'summary', 'is_experimental', 'support_url',
'support_email')
'support_email', 'privacy_policy')
def __init__(self, *args, **kw):
kw['initial'] = {
'has_priv': self._has_field('privacy_policy', kw['instance'])}
super(DescribeForm, self).__init__(*args, **kw)
class ReviewerNotesForm(happyforms.ModelForm):
approvalnotes = forms.CharField(
widget=TranslationTextarea(attrs={'rows': 4}), required=False)
def clean_name(self):
return clean_addon_name(self.cleaned_data['name'], self.instance)
class Meta:
model = Version
fields = ('approvalnotes',)
def _has_field(self, name, instance=None):
# If there's a policy in any language, this addon has a policy.
n = getattr(instance or self.instance, u'%s_id' % name)
return any(map(bool, Translation.objects.filter(id=n)))
def save(self, commit=True):
obj = super(DescribeForm, self).save(commit)
if not self.cleaned_data['has_priv']:
delete_translation(self.instance, 'privacy_policy')
return obj
class PreviewForm(happyforms.ModelForm):
@ -840,7 +852,7 @@ class DistributionChoiceForm(happyforms.Form):
u'self-distribution. Updates should be handled by you via an '
u'updateURL or external application updates.</span>')
choices = forms.ChoiceField(
channel = forms.ChoiceField(
choices=(
('listed', mark_safe_lazy(LISTED_LABEL)),
('unlisted', mark_safe_lazy(UNLISTED_LABEL))),

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

@ -26,7 +26,12 @@
{{ _('Edit Information') }}</a>
</li>
<li>
<a href="{{ addon.get_dev_url('versions') }}#version-upload" class="tooltip"
{% if waffle.switch('step-version-upload') %}
{% set version_upload_url = url('devhub.submit.version', addon.slug) %}
{% else %}
{% set version_upload_url = '%s#version-upload' % addon.get_dev_url('versions') %}
{% endif %}
<a href="{{ version_upload_url }}" class="tooltip"
title="{{ _('Upload a new version of this add-on.') }}">
{{ _('New Version') }}</a>
</li>

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

@ -1,7 +1,10 @@
{% extends "devhub/base.html" %}
{% set title = _('Submit a New Add-on') %}
{% if submit_page == 'version' %}
{% set title = _('Submit a New Version') %}
{% else %}
{% set title = _('Submit a New Add-on') %}
{% endif %}
{% block title %}
{{ dev_page_title(title) }}

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

@ -5,7 +5,7 @@
{% block primary %}
<h3>{{ _('Describe Add-on') }}</h3>
<form method="post" id="submit-describe" class="item{% if not addon.is_listed %} unlisted{% endif %}">
<form method="post" id="submit-describe" class="item">
{{ csrf() }}
<div class="addon-submission-field">
<label for="id_name">{{ _("Name:") }}</label>
@ -95,20 +95,20 @@
</div>
</div>
{% endif %}
{% set values = policy_form.data if policy_form.is_bound else policy_form.initial %}
{% set values = form.data if form.is_bound else form.initial %}
<div class="optional-terms">
<div class="addon-submission-field">
{{ policy_form.has_priv }}
{{ policy_form.has_priv.label_tag() }}
{{ form.has_priv }}
{{ 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 }}
{{ form.privacy_policy.errors }}
{{ form.privacy_policy.label_tag() }}
{{ form.privacy_policy }}
</div>
</div>
</div>
@ -122,7 +122,7 @@
</div>
<div class="submission-buttons addon-submission-field">
<button type="submit">
{{ _('Submit Add-on for Review') }}
{{ _('Submit Version for Review') }}
</button>
</div>
</form>

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

@ -0,0 +1,32 @@
{% from "devhub/includes/macros.html" import some_html_tip, select_cats %}
{% extends "devhub/addons/submit/base.html" %}
{% block title %}{{ dev_page_title(_('Describe Add-on'), addon) }}{% endblock %}
{% block primary %}
<h3>{{ _('Describe Version') }}</h3>
<form method="post" id="submit-describe" class="item">
{{ csrf() }}
<div class="addon-submission-field">
<label for="{{ reviewer_form.approvalnotes.auto_id }}">
{{ _('Release Notes:') }}
</label>
<p>{{ _("Let your users know what's new and what's changed in this version.") }}</p>
{{ reviewer_form.releasenotes }}
<p>{{ _('These notes will appear on the detail page.') }}</p>
</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">
{{ _('Submit Version for Review') }}
</button>
</div>
</form>
{% endblock primary %}

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

@ -6,7 +6,7 @@
{% block primary %}
{% set version_edit_url = url('devhub.versions.edit', addon.slug, uploaded_version.id) %}
{% if addon.is_listed %}
{% if uploaded_version.channel == amo.RELEASE_CHANNEL_LISTED %}
<h3>{{ _("Version Submitted for Review") }}</h3>
<p>
{{ _("Youre done! This version has been submitted for review. You will be "

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

@ -7,7 +7,17 @@
<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>{{ _('Upload Add-on') }}</h3>
{% if submit_page == 'version' %}
<h3>{{ _('Where to Host Version') }}</h3>
<p class="addon-submit-distribute">
{{ channel_choice_text|safe }}
{% if waffle.switch('mixed-listed-unlisted') %}
{% set channel_param = 'listed' if listed else 'unlisted' %}
<a href="{{ url('devhub.submit.version.distribution', addon.slug) }}?channel={{ channel_param }}">{{ _('Change') }}</a>
{% endif %}}
</p>
{% endif %}
<h3>{{ _('Upload Version') }}</h3>
<p>
{% trans %}
Use the fields below to upload your add-on package and select any platform
@ -21,10 +31,18 @@
{{ new_addon_form.upload }}
</div>
<input type="file" id="upload-addon"
{% if listed %}
data-upload-url="{{ url('devhub.upload') }}"
{% if addon %}
{% if listed %}
data-upload-url="{{ url('devhub.upload_for_version', addon.slug, 'listed') }}"
{% else %}
data-upload-url="{{ url('devhub.upload_for_version', addon.slug, 'unlisted') }}"
{% endif %}
{% else %}
data-upload-url="{{ url('devhub.upload_unlisted') }}"
{% if listed %}
data-upload-url="{{ url('devhub.upload') }}"
{% else %}
data-upload-url="{{ url('devhub.upload_unlisted') }}"
{% endif %}
{% endif %}
>
@ -38,7 +56,7 @@
{{ new_addon_form.errors.supported_platforms }}
{{ new_addon_form.supported_platforms }}
</div>
{{ new_addon_form.upload }}
</div>
{% if is_admin %}
<div class="admin-settings">
@ -53,7 +71,11 @@
{% endif %}
<div class="submission-buttons addon-submission-field">
<button class="addon-upload-dependant" id="submit-upload-file-finish" disabled=disabled type="submit">
{{ _('Continue') }}
{% if listed %}
{{ _('Continue') }}
{% else %}
{{ _('Sign Add-on') }}
{% endif %}
</button>
</div>
</section>

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

@ -45,8 +45,7 @@
{{ status_and_tip(addon,
_("Your add-on was disabled by a site administrator and is no "
"longer shown in our gallery. If you have any questions, "
"please email amo-admins@mozilla.org."),
url=(addon.get_dev_url('versions') + '#version-upload')) }}
"please email amo-admins@mozilla.org.")) }}
{% endif %}
</strong>
</li>

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

@ -25,7 +25,12 @@
<p class="addon-upload">
{% if (addon.has_complete_metadata() or addon.status != amo.STATUS_NULL) and not addon.is_disabled %}
<strong>
<a href="{{ addon.get_dev_url('versions') }}#version-upload"
{% if waffle.switch('step-version-upload') %}
{% set version_upload_url = url('devhub.submit.version', addon.slug) %}
{% else %}
{% set version_upload_url = '%s#version-upload' % addon.get_dev_url('versions') %}
{% endif %}
<a href="{{ version_upload_url }}"
class="{{ version_upload_class }}">
{{ _('Upload New Version') }}</a>
</strong>

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

@ -116,7 +116,12 @@
{% endwith %}
{% if not item.addon.is_persona() and item.addon.has_complete_metadata() and not item.addon.is_disabled %}
<p class="upload-new-version">
<a href="{{ item.addon.get_dev_url('versions') }}#version-upload">
{% if waffle.switch('step-version-upload') %}
{% set version_upload_url = url('devhub.submit.version', item.addon.slug) %}
{% else %}
{% set version_upload_url = '%s#version-upload' % item.addon.get_dev_url('versions') %}
{% endif %}
<a href="{{ version_upload_url }}">
{{ _('Upload New Version') }}</a>
</p>
{% endif %}

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

@ -219,7 +219,12 @@
</tr>
<tr>
<td colspan="0">
<a href="#" class="button version-upload">{{ _('Upload a New Version') }}</a>
{% if waffle.switch('step-version-upload') %}
{% set version_upload_url = url('devhub.submit.version', addon.slug) %}
{% else %}
{% set version_upload_url = '#' %}
{% endif %}
<a href="{{ version_upload_url }}" class="button version-upload">{{ _('Upload a New Version') }}</a>
</td>
</tr>
{% for version in versions.object_list %}
@ -275,10 +280,12 @@
</form>
</div>
{% if not waffle.switch('step-version-upload') %}
{{ add_file_modal(_("Add a new Version"),
url('devhub.versions.add', addon.slug),
_("Add Version"),
'version')}}
{% endif %}
{% if not addon.disabled_by_user and not addon.is_disabled %}
<div id="modal-disable" class="modal">

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

@ -27,11 +27,11 @@ from olympia.users.models import UserProfile
from olympia.versions.models import ApplicationsVersions, License, Version
class TestNewAddonForm(TestCase):
class TestNewVersionForm(TestCase):
def test_only_valid_uploads(self):
upload = FileUpload.objects.create(valid=False)
form = forms.NewAddonForm(
form = forms.NewVersionForm(
{'upload': upload.uuid, 'supported_platforms': [1]},
request=mock.Mock())
assert ('There was an error with your upload. Please try again.' in
@ -39,38 +39,13 @@ class TestNewAddonForm(TestCase):
upload.validation = '{"errors": 0}'
upload.save()
form = forms.NewAddonForm(
addon = Addon.objects.create()
form = forms.NewVersionForm(
{'upload': upload.uuid, 'supported_platforms': [1]},
request=mock.Mock())
addon=addon, request=mock.Mock())
assert ('There was an error with your upload. Please try again.' not in
form.errors.get('__all__')), form.errors
# Those three patches are so files.utils.parse_addon doesn't fail on a
# non-existent file even before having a chance to call check_xpi_info.
@mock.patch('olympia.files.utils.Extractor.parse')
@mock.patch('olympia.files.utils.extract_xpi', lambda xpi, path: None)
@mock.patch('olympia.files.utils.get_file', lambda xpi: None)
# This is the one we want to test.
@mock.patch('olympia.files.utils.check_xpi_info')
def test_check_xpi_called(self, mock_check_xpi_info, mock_parse):
"""Make sure the check_xpi_info helper is called.
There's some important checks made in check_xpi_info, if we ever
refactor the form to not call it anymore, we need to make sure those
checks are run at some point.
"""
mock_parse.return_value = None
mock_check_xpi_info.return_value = {'name': 'foo', 'type': 2}
upload = FileUpload.objects.create(valid=True)
form = forms.NewAddonForm(
{'upload': upload.uuid, 'supported_platforms': [1]},
request=mock.Mock())
form.clean()
assert mock_check_xpi_info.called
class TestNewVersionForm(TestCase):
# Those three patches are so files.utils.parse_addon doesn't fail on a
# non-existent file even before having a chance to call check_xpi_info.
@mock.patch('olympia.files.utils.Extractor.parse')
@ -90,10 +65,8 @@ class TestNewVersionForm(TestCase):
upload = FileUpload.objects.create(valid=True)
addon = Addon.objects.create()
form = forms.NewVersionForm(
{'upload': upload.uuid, 'supported_platforms': [1],
'nomination_type': amo.STATUS_NOMINATED},
addon=addon,
request=mock.Mock())
{'upload': upload.uuid, 'supported_platforms': [1]},
addon=addon, request=mock.Mock())
form.clean()
assert mock_check_xpi_info.called
@ -776,7 +749,7 @@ class TestDistributionChoiceForm(TestCase):
"""
with translation.override('en-US'):
form = forms.DistributionChoiceForm()
label = form.fields['choices'].choices[0][1]
label = form.fields['channel'].choices[0][1]
expected = 'On this site.'
label = unicode(label)
@ -784,7 +757,7 @@ class TestDistributionChoiceForm(TestCase):
with translation.override('de'):
form = forms.DistributionChoiceForm()
label = form.fields['choices'].choices[0][1]
label = form.fields['channel'].choices[0][1]
expected = 'Auf dieser Website.'
label = unicode(label)

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

@ -17,6 +17,7 @@ import mock
import waffle
from jingo.helpers import datetime as datetime_filter
from pyquery import PyQuery as pq
from waffle.testutils import override_switch
from olympia import amo, paypal, files
from olympia.amo.tests import TestCase, version_factory
@ -833,7 +834,8 @@ class TestHome(TestCase):
# Regular users (non-devs) should not see this promo.
assert self.get_pq()('#devhub-sidebar #editor-promo').length == 0
def test_my_addons(self):
@override_switch('step-version-upload', active=False)
def test_my_addons_inline_version_upload(self):
statuses = [(amo.STATUS_NOMINATED, amo.STATUS_AWAITING_REVIEW),
(amo.STATUS_PUBLIC, amo.STATUS_AWAITING_REVIEW)]
@ -871,6 +873,50 @@ class TestHome(TestCase):
Addon.with_unlisted.all().delete()
assert self.get_pq()('#my-addons').length == 0
def test_my_unlisted_addons_inline_version_upload(self):
self.addon.update(is_listed=False)
# Run the test again but with an unlisted addon.
self.test_my_addons_inline_version_upload()
@override_switch('step-version-upload', active=True)
def test_my_addons(self):
statuses = [(amo.STATUS_NOMINATED, amo.STATUS_AWAITING_REVIEW),
(amo.STATUS_PUBLIC, amo.STATUS_AWAITING_REVIEW)]
for addon_status, file_status in statuses:
latest_version = self.addon.find_latest_version()
file = latest_version.files.all()[0]
file.update(status=file_status)
self.addon.update(status=addon_status)
doc = self.get_pq()
addon_item = doc('#my-addons .addon-item')
assert addon_item.length == 1
assert addon_item.find('.addon-name').attr('href') == (
self.addon.get_dev_url('edit'))
if self.addon.is_listed:
# We don't display a link to the inexistent public page for
# unlisted addons.
assert addon_item.find('p').eq(3).find('a').attr('href') == (
self.addon.current_version.get_url_path())
if self.addon.is_listed:
assert 'Queue Position: 1 of 1' == (
addon_item.find('p').eq(4).text())
assert addon_item.find('.upload-new-version a').attr('href') == (
reverse('devhub.submit.version', args=[self.addon.slug]))
self.addon.status = statuses[1][0]
self.addon.save()
doc = self.get_pq()
addon_item = doc('#my-addons .addon-item')
status_str = 'Status: ' + unicode(
self.addon.STATUS_CHOICES[self.addon.status])
assert status_str == addon_item.find('p').eq(1).text()
Addon.with_unlisted.all().delete()
assert self.get_pq()('#my-addons').length == 0
def test_my_unlisted_addons(self):
self.addon.update(is_listed=False)
self.test_my_addons() # Run the test again but with an unlisted addon.
@ -2003,6 +2049,7 @@ class TestUploadErrors(UploadTest):
self.test_dupe_xpi(channel='unlisted')
@override_switch('step-version-upload', active=False)
class AddVersionTest(UploadTest):
def post(self, supported_platforms=None,
@ -2024,6 +2071,7 @@ class AddVersionTest(UploadTest):
self.url = reverse('devhub.versions.add', args=[self.addon.slug])
@override_switch('step-version-upload', active=False)
class TestAddVersion(AddVersionTest):
def test_unique_version_num(self):
@ -2195,10 +2243,11 @@ class TestAddVersion(AddVersionTest):
assert signed_file.version.addon == self.addon
assert signed_file.version.channel == amo.RELEASE_CHANNEL_LISTED
# There is a log for that beta file signature (with passed validation).
log = ActivityLog.objects.get()
log = ActivityLog.objects.latest(field_name='id')
assert log.action == amo.LOG.EXPERIMENT_SIGNED.id
@override_switch('step-version-upload', active=False)
class TestAddBetaVersion(AddVersionTest):
fixtures = ['base/users', 'base/appversion', 'base/addon_3615']
@ -2281,6 +2330,7 @@ class TestAddBetaVersion(AddVersionTest):
assert log.action == amo.LOG.BETA_SIGNED_VALIDATION_FAILED.id
@override_switch('step-version-upload', active=False)
class TestAddVersionValidation(AddVersionTest):
def login_as_admin(self):

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

@ -6,14 +6,17 @@ from django.core.files import temp
import mock
from pyquery import PyQuery as pq
from waffle.testutils import override_switch
from olympia import amo
from olympia.addons.models import Addon, AddonCategory, Category
from olympia.amo.tests import addon_factory, formset, initial, TestCase
from olympia.amo.tests import (
addon_factory, formset, initial, TestCase, version_factory)
from olympia.amo.tests.test_helpers import get_image_path
from olympia.amo.urlresolvers import reverse
from olympia.devhub import views
from olympia.devhub.models import ActivityLog
from olympia.files.tests.test_models import UploadTest as BaseUploadTest
from olympia.files.tests.test_models import UploadTest
from olympia.users.models import UserProfile
from olympia.versions.models import License
@ -76,10 +79,10 @@ class TestSubmitBase(TestCase):
return Addon.with_unlisted.no_cache().get(pk=3615)
def get_version(self):
return self.get_addon().versions.get()
return self.get_addon().versions.latest()
class TestSubmitStepAgreement(TestSubmitBase):
class TestAddonSubmitAgreement(TestSubmitBase):
def test_step1_submit(self):
self.user.update(read_dev_agreement=None)
response = self.client.get(reverse('devhub.submit.agreement'))
@ -110,11 +113,45 @@ class TestSubmitStepAgreement(TestSubmitBase):
self.assert3xx(response, reverse('devhub.submit.distribution'))
class TestCreateAddon(BaseUploadTest, TestCase):
class TestAddonSubmitDistribution(TestCase):
fixtures = ['base/users']
def setUp(self):
super(TestCreateAddon, self).setUp()
super(TestAddonSubmitDistribution, self).setUp()
self.client.login(email='regular@mozilla.com')
self.user = UserProfile.objects.get(email='regular@mozilla.com')
def test_check_agreement_okay(self):
r = self.client.post(reverse('devhub.submit.agreement'))
self.assert3xx(r, reverse('devhub.submit.distribution'))
r = self.client.get(reverse('devhub.submit.distribution'))
assert r.status_code == 200
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.distribution'), follow=True)
self.assert3xx(r, reverse('devhub.submit.agreement'))
def test_listed_redirects_to_next_step(self):
response = self.client.post(reverse('devhub.submit.distribution'),
{'channel': 'listed'})
self.assert3xx(response,
reverse('devhub.submit.upload', args=['listed']))
def test_unlisted_redirects_to_next_step(self):
response = self.client.post(reverse('devhub.submit.distribution'),
{'channel': 'unlisted'})
self.assert3xx(response, reverse('devhub.submit.upload',
args=['unlisted']))
class TestAddonSubmitUpload(UploadTest, TestCase):
fixtures = ['base/users']
def setUp(self):
super(TestAddonSubmitUpload, self).setUp()
self.upload = self.get_upload('extension.xpi')
assert self.client.login(email='regular@mozilla.com')
self.client.post(reverse('devhub.submit.agreement'))
@ -259,7 +296,7 @@ class TestCreateAddon(BaseUploadTest, TestCase):
all_ = sorted([f.filename for f in latest_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(
[mock.call(f) for f in latest_version.all_files])
[mock.call(f, is_beta=False) for f in latest_version.all_files])
def test_with_source(self):
tdir = temp.gettempdir()
@ -274,47 +311,10 @@ class TestCreateAddon(BaseUploadTest, TestCase):
assert Addon.objects.get(pk=addon.pk).admin_review
class TestSubmitStepDistribution(TestCase):
fixtures = ['base/users']
class TestAddonSubmitDetails(TestSubmitBase):
def setUp(self):
super(TestSubmitStepDistribution, self).setUp()
self.client.login(email='regular@mozilla.com')
self.user = UserProfile.objects.get(email='regular@mozilla.com')
def test_check_agreement_okay(self):
r = self.client.post(reverse('devhub.submit.agreement'))
self.assert3xx(r, reverse('devhub.submit.distribution'))
r = self.client.get(reverse('devhub.submit.distribution'))
assert r.status_code == 200
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.distribution'), follow=True)
self.assert3xx(r, reverse('devhub.submit.agreement'))
def test_listed_redirects_to_next_step(self):
response = self.client.post(reverse('devhub.submit.distribution'),
{'choices': 'listed'})
self.assert3xx(response,
reverse('devhub.submit.upload', args=['listed']))
def test_unlisted_redirects_to_next_step(self):
response = self.client.post(reverse('devhub.submit.distribution'),
{'choices': 'unlisted'})
self.assert3xx(response, reverse('devhub.submit.upload',
args=['unlisted']))
# Tests for Upload step in TestCreateAddon
class TestSubmitStepDetails(TestSubmitBase):
def setUp(self):
super(TestSubmitStepDetails, self).setUp()
super(TestAddonSubmitDetails, self).setUp()
self.url = reverse('devhub.submit.details', args=['a3615'])
AddonCategory.objects.filter(
@ -339,7 +339,7 @@ class TestSubmitStepDetails(TestSubmitBase):
'support_email': 'black@hole.org'})
cat_initial = kw.pop('cat_initial', self.cat_initial)
cat_form = formset(cat_initial, initial_count=1)
license_form = {'builtin': 3}
license_form = {'license-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'}
@ -354,11 +354,12 @@ class TestSubmitStepDetails(TestSubmitBase):
def is_success(self, data):
assert self.get_addon().status == amo.STATUS_NULL
response = self.client.post(self.url, data)
assert all(self.get_addon().get_required_metadata())
assert response.status_code == 302
assert self.get_addon().status == amo.STATUS_NOMINATED
return response
def test_submit_success_minimal(self):
def test_submit_success_required(self):
# Set/change the required fields only
r = self.client.get(self.url)
assert r.status_code == 200
@ -380,6 +381,7 @@ class TestSubmitStepDetails(TestSubmitBase):
assert addon.slug == 'testname'
assert addon.summary == 'Hello!'
assert addon.is_experimental
assert addon.all_categories[0].id == 22
# Test add-on log activity.
log_items = ActivityLog.objects.for_addons(addon)
@ -398,6 +400,7 @@ class TestSubmitStepDetails(TestSubmitBase):
assert addon.support_url == 'http://stackoverflow.com'
assert addon.support_email == 'black@hole.org'
assert addon.privacy_policy == 'Ur data belongs to us now.'
assert addon.current_version.approvalnotes == 'approove plz'
def test_submit_name_unique(self):
# Make sure name is unique.
@ -510,7 +513,7 @@ class TestSubmitStepDetails(TestSubmitBase):
assert category_ids_new == [22]
def test_set_builtin_license_no_log(self):
self.is_success(self.get_dict(builtin=3))
self.is_success(self.get_dict(**{'license-builtin': 3}))
addon = self.get_addon()
assert addon.status == amo.STATUS_NOMINATED
assert addon.current_version.license.builtin == 3
@ -518,7 +521,8 @@ class TestSubmitStepDetails(TestSubmitBase):
assert not log_items.filter(action=amo.LOG.CHANGE_LICENSE.id)
def test_license_error(self):
response = self.client.post(self.url, self.get_dict(builtin=4))
response = self.client.post(
self.url, self.get_dict(**{'license-builtin': 4}))
assert response.status_code == 200
self.assertFormError(response, 'license_form', 'builtin',
'Select a valid choice. 4 is not one of '
@ -550,14 +554,13 @@ class TestSubmitStepDetails(TestSubmitBase):
version = self.get_addon().versions.latest()
version.update(channel=amo.RELEASE_CHANNEL_UNLISTED)
response = self.client.get(self.url)
self.assert3xx(
response, reverse('devhub.submit.finish', args=[self.addon.slug]))
self.assert3xx(response, self.next_step)
class TestSubmitStepFinish(TestSubmitBase):
class TestAddonSubmitFinish(TestSubmitBase):
def setUp(self):
super(TestSubmitStepFinish, self).setUp()
super(TestAddonSubmitFinish, self).setUp()
self.url = reverse('devhub.submit.finish', args=[self.addon.slug])
@mock.patch.object(settings, 'SITE_URL', 'http://b.ro')
@ -624,8 +627,9 @@ class TestSubmitStepFinish(TestSubmitBase):
assert not send_welcome_email_mock.called
def test_finish_submitting_listed_addon(self):
assert self.addon.current_version.supported_platforms == (
[amo.PLATFORM_ALL])
version = self.addon.find_latest_version(
channel=amo.RELEASE_CHANNEL_LISTED)
assert version.supported_platforms == ([amo.PLATFORM_ALL])
r = self.client.get(self.url)
assert r.status_code == 200
@ -639,9 +643,9 @@ class TestSubmitStepFinish(TestSubmitBase):
# 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])
args=[self.addon.slug, version.id])
assert links[1].text == (
'Edit version %s' % self.addon.current_version.version)
'Edit version %s' % version.version)
# Third back to my submissions.
assert links[2].attrib['href'] == reverse('devhub.addons')
@ -737,10 +741,10 @@ class TestSubmitStepFinish(TestSubmitBase):
self.assert3xx(r, reverse('devhub.submit.details', args=['a3615']))
class TestResumeStep(TestSubmitBase):
class TestAddonSubmitResume(TestSubmitBase):
def setUp(self):
super(TestResumeStep, self).setUp()
super(TestAddonSubmitResume, self).setUp()
self.url = reverse('devhub.submit.resume', args=['a3615'])
def test_addon_no_versions_redirects_to_versions(self):
@ -763,3 +767,442 @@ class TestResumeStep(TestSubmitBase):
r = self.client.get(reverse('devhub.addons.edit', args=['a3615']),
follow=True)
self.assert3xx(r, reverse('devhub.submit.details', args=['a3615']))
@override_switch('step-version-upload', active=True)
@override_switch('mixed-listed-unlisted', active=True)
class TestVersionSubmitDistribution(TestSubmitBase):
def setUp(self):
super(TestVersionSubmitDistribution, self).setUp()
self.url = reverse('devhub.submit.version.distribution',
args=[self.addon.slug])
def test_listed_redirects_to_next_step(self):
response = self.client.post(self.url, {'channel': 'listed'})
self.assert3xx(
response,
reverse('devhub.submit.version.upload', args=[
self.addon.slug, 'listed']))
def test_unlisted_redirects_to_next_step(self):
response = self.client.post(self.url, {'channel': 'unlisted'})
self.assert3xx(
response,
reverse('devhub.submit.version.upload', args=[
self.addon.slug, 'unlisted']))
@override_switch('step-version-upload', active=True)
@override_switch('mixed-listed-unlisted', active=True)
class TestVersionSubmitAutoChannel(TestSubmitBase):
""" Just check we chose the right upload channel. The upload tests
themselves are in other tests. """
def setUp(self):
super(TestVersionSubmitAutoChannel, self).setUp()
self.url = reverse('devhub.submit.version', args=[self.addon.slug])
@mock.patch('olympia.devhub.views._submit_upload',
side_effect=views._submit_upload)
def test_listed_last_uses_listed_upload(self, _submit_upload_mock):
version_factory(addon=self.addon, channel=amo.RELEASE_CHANNEL_LISTED)
self.client.post(self.url)
args, _ = _submit_upload_mock.call_args
assert args[1:] == (
self.addon, amo.RELEASE_CHANNEL_LISTED,
'devhub.submit.version.details', 'devhub.submit.version.finish')
@mock.patch('olympia.devhub.views._submit_upload',
side_effect=views._submit_upload)
def test_unlisted_last_uses_unlisted_upload(self, _submit_upload_mock):
version_factory(addon=self.addon, channel=amo.RELEASE_CHANNEL_UNLISTED)
self.client.post(self.url)
args, _ = _submit_upload_mock.call_args
assert args[1:] == (
self.addon, amo.RELEASE_CHANNEL_UNLISTED,
'devhub.submit.version.details', 'devhub.submit.version.finish')
def test_no_versions_redirects_to_distribution(self):
[v.delete() for v in self.addon.versions.all()]
response = self.client.post(self.url)
self.assert3xx(
response,
reverse('devhub.submit.version.distribution',
args=[self.addon.slug]))
@override_switch('step-version-upload', active=True)
@override_switch('mixed-listed-unlisted', active=False)
class TestVersionSubmitAutoChannelNoMixed(TestSubmitBase):
def setUp(self):
super(TestVersionSubmitAutoChannelNoMixed, self).setUp()
self.url = reverse('devhub.submit.version', args=[self.addon.slug])
@mock.patch('olympia.devhub.views._submit_upload',
side_effect=views._submit_upload)
def test_redirect_listed_based_on_is_listed(self, _submit_upload_mock):
self.addon.update(is_listed=True)
self.client.post(self.url)
args, _ = _submit_upload_mock.call_args
assert args[1:] == (
self.addon, amo.RELEASE_CHANNEL_LISTED,
'devhub.submit.version.details', 'devhub.submit.version.finish')
@mock.patch('olympia.devhub.views._submit_upload',
side_effect=views._submit_upload)
def test_redirect_unlisted_based_on_is_listed(self, _submit_upload_mock):
self.addon.update(is_listed=False)
self.client.post(self.url)
args, _ = _submit_upload_mock.call_args
assert args[1:] == (
self.addon, amo.RELEASE_CHANNEL_UNLISTED,
'devhub.submit.version.details', 'devhub.submit.version.finish')
class VersionSubmitUploadMixin(object):
channel = None
fixtures = ['base/users', 'base/addon_3615']
def setUp(self):
super(VersionSubmitUploadMixin, self).setUp()
self.upload = self.get_upload('extension.xpi')
self.addon = Addon.objects.get(id=3615)
self.version = self.addon.current_version
self.addon.update(guid='guid@xpi')
assert self.client.login(email='del@icio.us')
self.addon.update(
is_listed=(self.channel == amo.RELEASE_CHANNEL_LISTED))
self.addon.versions.update(channel=self.channel)
channel = ('listed' if self.channel == amo.RELEASE_CHANNEL_LISTED else
'unlisted')
self.url = reverse('devhub.submit.version.upload',
args=[self.addon.slug, channel])
assert self.addon.has_complete_metadata()
def post(self, supported_platforms=None,
override_validation=False, expected_status=302, source=None,
beta=False):
if supported_platforms is None:
supported_platforms = [amo.PLATFORM_MAC]
d = dict(upload=self.upload.uuid.hex, source=source,
supported_platforms=[p.id for p in supported_platforms],
admin_override_validation=override_validation, beta=beta)
r = self.client.post(self.url, d)
assert r.status_code == expected_status
return r
def get_next_url(self, version):
raise NotImplementedError
def test_with_source(self):
tdir = temp.gettempdir()
source = temp.NamedTemporaryFile(suffix=".zip", dir=tdir)
source.write('a' * (2 ** 21))
source.seek(0)
response = self.post(source=source)
version = self.addon.find_latest_version(channel=self.channel)
assert version.source
self.assert3xx(response, self.get_next_url(version))
assert self.addon.reload().admin_review
def test_with_bad_source_format(self):
tdir = temp.gettempdir()
source = temp.NamedTemporaryFile(suffix=".exe", dir=tdir)
source.write('a' * (2 ** 21))
source.seek(0)
response = self.post(source=source, expected_status=200)
assert response.context['new_addon_form'].errors.as_text().startswith(
'* source\n * Unsupported file type, please upload an archive ')
def test_missing_platforms(self):
r = self.client.post(self.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.')
def test_one_xpi_for_multiple_platforms(self):
response = self.post(supported_platforms=[amo.PLATFORM_MAC,
amo.PLATFORM_LINUX])
version = self.addon.find_latest_version(channel=self.channel)
self.assert3xx(response, self.get_next_url(version))
all_ = sorted([f.filename for f in version.all_files])
assert all_ == [u'delicious_bookmarks-0.1-linux.xpi',
u'delicious_bookmarks-0.1-mac.xpi']
def test_unique_version_num(self):
self.version.update(version='0.1')
r = self.post(expected_status=200)
assert pq(r.content)('ul.errorlist').text() == (
'Version 0.1 already exists, or was uploaded before.')
def test_same_version_if_previous_is_rejected(self):
# We can't re-use the same version number, even if the previous
# versions have been disabled/rejected.
self.version.update(version='0.1')
self.version.files.update(status=amo.STATUS_DISABLED)
r = self.post(expected_status=200)
assert pq(r.content)('ul.errorlist').text() == (
'Version 0.1 already exists, or was uploaded before.')
def test_same_version_if_previous_is_deleted(self):
# We can't re-use the same version number if the previous
# versions has been deleted either.
self.version.update(version='0.1')
self.version.delete()
r = self.post(expected_status=200)
assert pq(r.content)('ul.errorlist').text() == (
'Version 0.1 already exists, or was uploaded before.')
@override_switch('mixed-listed-unlisted', active=True)
def test_distribution_link(self):
response = self.client.get(self.url)
channel_text = ('listed' if self.channel == amo.RELEASE_CHANNEL_LISTED
else 'unlisted')
distribution_url = reverse('devhub.submit.version.distribution',
args=[self.addon.slug])
doc = pq(response.content)
assert doc('.addon-submit-distribute a').attr('href') == (
distribution_url + '?channel=' + channel_text)
@override_switch('mixed-listed-unlisted', active=False)
def test_distribution_link_hidden(self):
response = self.client.get(self.url)
doc = pq(response.content)
assert doc('.addon-submit-distribute a').length == 0
@override_switch('step-version-upload', active=True)
class TestVersionSubmitUploadListed(VersionSubmitUploadMixin, UploadTest):
channel = amo.RELEASE_CHANNEL_LISTED
def get_next_url(self, version):
return reverse('devhub.submit.version.details', args=[
self.addon.slug, version.pk])
def test_success(self):
response = self.post()
version = self.addon.find_latest_version(
channel=amo.RELEASE_CHANNEL_LISTED)
assert version.channel == amo.RELEASE_CHANNEL_LISTED
assert version.all_files[0].status == amo.STATUS_AWAITING_REVIEW
self.assert3xx(response, self.get_next_url(version))
log_items = ActivityLog.objects.for_addons(self.addon)
assert log_items.filter(action=amo.LOG.ADD_VERSION.id)
@mock.patch('olympia.devhub.views.sign_file')
def test_experiments_are_auto_signed(self, mock_sign_file):
"""Experiment extensions (bug 1220097) are auto-signed."""
# We're going to sign even if it has signing related errors/warnings.
self.upload = self.get_upload(
'telemetry_experiment.xpi',
validation=json.dumps({
"notices": 2, "errors": 0, "messages": [],
"metadata": {}, "warnings": 1,
"signing_summary": {"trivial": 1, "low": 0,
"medium": 0, "high": 1},
"passed_auto_validation": 0}))
self.addon.update(guid='experiment@xpi', is_listed=True,
status=amo.STATUS_PUBLIC)
self.post()
# Make sure the file created and signed is for this addon.
assert mock_sign_file.call_count == 1
mock_sign_file_call = mock_sign_file.call_args[0]
signed_file = mock_sign_file_call[0]
assert signed_file.version.addon == self.addon
assert signed_file.version.channel == amo.RELEASE_CHANNEL_LISTED
# There is a log for that file (with passed validation).
log = ActivityLog.objects.latest(field_name='id')
assert log.action == amo.LOG.EXPERIMENT_SIGNED.id
def test_force_beta(self):
self.post(beta=True)
# Need latest() rather than find_latest_version as Beta isn't returned.
version = self.addon.versions.latest()
assert version.all_files[0].status == amo.STATUS_BETA
def test_incomplete_addon_now_nominated(self):
"""Uploading a new version for an incomplete addon should set it to
nominated."""
self.addon.current_version.files.update(status=amo.STATUS_DISABLED)
self.addon.update_status()
# Deleting all the versions should make it null.
assert self.addon.status == amo.STATUS_NULL
self.post()
self.addon.reload()
assert self.addon.status == amo.STATUS_NOMINATED
@override_switch('step-version-upload', active=True)
class TestVersionSubmitUploadUnlisted(VersionSubmitUploadMixin, UploadTest):
channel = amo.RELEASE_CHANNEL_UNLISTED
def get_next_url(self, version):
return reverse('devhub.submit.version.finish', args=[
self.addon.slug, version.pk])
@mock.patch('olympia.editors.helpers.sign_file')
def test_success(self, mock_sign_file):
"""Sign automatically."""
# No validation errors or warning.
self.upload = self.get_upload(
'extension.xpi',
validation=json.dumps(dict(errors=0, warnings=0, notices=2,
metadata={}, messages=[],
signing_summary={
'trivial': 1, 'low': 0, 'medium': 0,
'high': 0},
passed_auto_validation=True
)))
response = self.post()
version = self.addon.find_latest_version(
channel=amo.RELEASE_CHANNEL_UNLISTED)
assert version.channel == amo.RELEASE_CHANNEL_UNLISTED
assert version.all_files[0].status == amo.STATUS_PUBLIC
self.assert3xx(response, self.get_next_url(version))
assert mock_sign_file.called
@mock.patch('olympia.editors.helpers.sign_file')
def test_success_fail_validation(self, mock_sign_file):
self.upload = self.get_upload(
'extension.xpi',
validation=json.dumps(dict(errors=0, warnings=0, notices=2,
metadata={}, messages=[],
signing_summary={
'trivial': 0, 'low': 1, 'medium': 0,
'high': 0},
passed_auto_validation=False
)))
response = self.post()
version = self.addon.find_latest_version(
channel=amo.RELEASE_CHANNEL_UNLISTED)
assert version.channel == amo.RELEASE_CHANNEL_UNLISTED
assert version.all_files[0].status == amo.STATUS_PUBLIC
self.assert3xx(response, self.get_next_url(version))
assert mock_sign_file.called
@mock.patch('olympia.devhub.views.auto_sign_file')
def test_one_xpi_for_multiple_platforms(self, mock_auto_sign_file):
super(TestVersionSubmitUploadUnlisted,
self).test_one_xpi_for_multiple_platforms()
version = self.addon.find_latest_version(
channel=amo.RELEASE_CHANNEL_UNLISTED)
mock_auto_sign_file.assert_has_calls(
[mock.call(f, is_beta=False) for f in version.all_files])
def test_no_force_beta_for_unlisted(self):
"""No beta version for unlisted addons."""
self.post(beta=True)
# Need latest() rather than find_latest_version as Beta isn't returned.
version = self.addon.versions.latest()
assert version.all_files[0].status != amo.STATUS_BETA
@override_switch('step-version-upload', active=True)
class TestVersionSubmitDetails(TestSubmitBase):
def setUp(self):
super(TestVersionSubmitDetails, self).setUp()
addon = self.get_addon()
self.version = version_factory(
addon=addon,
channel=amo.RELEASE_CHANNEL_LISTED,
license_id=addon.versions.latest().license_id)
self.url = reverse('devhub.submit.version.details',
args=[addon.slug, self.version.pk])
def test_submit_empty_is_okay(self):
assert all(self.get_addon().get_required_metadata())
r = self.client.get(self.url)
assert r.status_code == 200
response = self.client.post(self.url, {})
self.assert3xx(
response, reverse('devhub.submit.version.finish',
args=[self.addon.slug, self.version.pk]))
assert not self.version.approvalnotes
assert not self.version.releasenotes
def test_submit_success(self):
assert all(self.get_addon().get_required_metadata())
r = self.client.get(self.url)
assert r.status_code == 200
# Post and be redirected - trying to sneak in a field that shouldn't
# be modified when this is not the first listed version.
data = {'approvalnotes': 'approove plz',
'releasenotes': 'loadsa stuff', 'name': 'foo'}
response = self.client.post(self.url, data)
self.assert3xx(
response, reverse('devhub.submit.version.finish',
args=[self.addon.slug, self.version.pk]))
# This field should not have been modified.
assert self.get_addon().name != 'foo'
self.version.reload()
assert self.version.approvalnotes == 'approove plz'
assert self.version.releasenotes == 'loadsa stuff'
def test_submit_details_unlisted_should_redirect(self):
self.version.update(channel=amo.RELEASE_CHANNEL_UNLISTED)
assert all(self.get_addon().get_required_metadata())
response = self.client.get(self.url)
self.assert3xx(
response, reverse('devhub.submit.version.finish',
args=[self.addon.slug, self.version.pk]))
@override_switch('step-version-upload', active=True)
class TestVersionSubmitDetailsFirstListed(TestAddonSubmitDetails):
""" Testing the case of a listed version being submitted on an add-on that
previously only had unlisted versions - so is missing metadata."""
def setUp(self):
super(TestVersionSubmitDetailsFirstListed, self).setUp()
self.addon.versions.update(channel=amo.RELEASE_CHANNEL_UNLISTED)
self.version = version_factory(addon=self.addon,
channel=amo.RELEASE_CHANNEL_LISTED)
self.url = reverse('devhub.submit.version.details',
args=['a3615', self.version.pk])
self.next_step = reverse('devhub.submit.version.finish',
args=['a3615', self.version.pk])
@override_switch('step-version-upload', active=True)
class TestVersionSubmitFinish(TestAddonSubmitFinish):
def setUp(self):
super(TestVersionSubmitFinish, self).setUp()
addon = self.get_addon()
self.version = version_factory(
addon=addon,
channel=amo.RELEASE_CHANNEL_LISTED,
license_id=addon.versions.latest().license_id,
file_kw={'status': amo.STATUS_AWAITING_REVIEW})
self.url = reverse('devhub.submit.version.finish',
args=[addon.slug, self.version.pk])
@mock.patch('olympia.devhub.tasks.send_welcome_email.delay')
def test_no_welcome_email(self, send_welcome_email_mock):
"""No emails for version finish."""
self.client.get(self.url)
assert not send_welcome_email_mock.called
def test_addon_no_versions_redirects_to_versions(self):
# No versions makes getting to this step difficult!
pass
# No emails for any of these cases so ignore them.
def test_welcome_email_for_newbies(self):
pass
def test_welcome_email_first_listed_addon(self):
pass
def test_welcome_email_if_previous_addon_is_incomplete(self):
pass
def test_no_welcome_email_if_unlisted(self):
pass

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

@ -74,7 +74,8 @@ class TestVersion(TestCase):
reverse('devhub.file_validation',
args=[self.addon.slug, self.version.all_files[0].id]))
def test_upload_link_label_in_edit_nav(self):
@override_switch('step-version-upload', active=False)
def test_upload_link_label_in_edit_nav_inline_upload(self):
url = reverse('devhub.versions.edit',
args=(self.addon.slug, self.version.pk))
r = self.client.get(url)
@ -83,6 +84,16 @@ class TestVersion(TestCase):
assert link.attr('href') == '%s#version-upload' % (
reverse('devhub.addons.versions', args=[self.addon.slug]))
@override_switch('step-version-upload', active=True)
def test_upload_link_label_in_edit_nav(self):
url = reverse('devhub.versions.edit',
args=(self.addon.slug, self.version.pk))
r = self.client.get(url)
link = pq(r.content)('.addon-status>.addon-upload>strong>a')
assert link.text() == 'Upload New Version'
assert link.attr('href') == (
reverse('devhub.submit.version', args=[self.addon.slug]))
def test_delete_message(self):
"""Make sure we warn our users of the pain they will feel."""
r = self.client.get(self.url)

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

@ -11,15 +11,6 @@ from . import views
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.finish', addon_id)),
url('^details$', views.submit_details, name='devhub.submit.details'),
url('^finish$', views.submit_finish, name='devhub.submit.finish'),
)
# These will all start with /theme/<slug>/
theme_detail_patterns = patterns(
'',
@ -82,6 +73,23 @@ detail_patterns = patterns(
name='devhub.versions.add_file'),
url('^versions/(?P<version>[^/]+)$', views.version_bounce),
# New version submission
url('^versions/submit/$',
views.submit_version,
name='devhub.submit.version'),
url('^versions/submit/distribution$',
views.submit_version_distribution,
name='devhub.submit.version.distribution'),
url('^versions/submit/upload-(?P<channel>listed|unlisted)$',
views.submit_version_upload,
name='devhub.submit.version.upload'),
url('^versions/submit/(?P<version_id>\d+)/details$',
views.submit_version_details,
name='devhub.submit.version.details'),
url('^versions/submit/(?P<version_id>\d+)/finish$',
views.submit_version_finish,
name='devhub.submit.version.finish'),
url('^file/(?P<file_id>[^/]+)/validation$', views.file_validation,
name='devhub.file_validation'),
url('^file/(?P<file_id>[^/]+)/validation\.json$',
@ -99,8 +107,14 @@ detail_patterns = patterns(
views.json_bulk_compat_result,
name='devhub.json_bulk_compat_result'),
url('^submit/', include(submit_patterns)),
url('^submit/$',
lambda r, addon_id: redirect('devhub.submit.finish', addon_id)),
url('^submit/details$',
views.submit_addon_details, name='devhub.submit.details'),
url('^submit/finish$', views.submit_addon_finish,
name='devhub.submit.finish'),
url('^submit/resume$', views.submit_resume, name='devhub.submit.resume'),
url('^request-review$',
views.request_review, name='devhub.request-review'),
url('^rmlocale$', views.remove_locale, name='devhub.addons.remove-locale'),
@ -143,7 +157,7 @@ urlpatterns = decorate(write, patterns(
# Add-on submission
url('^addon/submit/(?:1)?$',
lambda r: redirect('devhub.submit.agreement', permanent=True)),
url('^addon/submit/agreement$', views.submit,
url('^addon/submit/agreement$', views.submit_addon,
name='devhub.submit.agreement'),
url('^addon/submit/distribution$', views.submit_addon_distribution,
name='devhub.submit.distribution'),

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

@ -18,8 +18,10 @@ from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_exempt
import commonware.log
import waffle
from django_statsd.clients import statsd
from PIL import Image
from waffle.decorators import waffle_switch
from olympia import amo
from olympia.amo import utils as amo_utils
@ -1262,6 +1264,7 @@ def auto_sign_version(version, **kwargs):
@json_view
@dev_required
@post_required
@waffle_switch('!step-version-upload')
def version_add(request, addon_id, addon):
form = forms.NewVersionForm(
request.POST,
@ -1387,109 +1390,208 @@ def version_stats(request, addon_id, addon):
@login_required
def submit(request):
def submit_addon(request):
return render_agreement(request, 'devhub/addons/submit/start.html',
'devhub.submit.distribution')
@login_required
@transaction.atomic
def submit_addon_distribution(request):
def _submit_distribution(request, addon, next_view):
if request.user.read_dev_agreement is None:
return redirect('devhub.submit.agreement')
form = forms.DistributionChoiceForm(request.POST)
# Accept GET for the first load so we can preselect the channel.
form = forms.DistributionChoiceForm(
request.POST if request.method == 'POST' else request.GET)
if request.method == 'POST' and form.is_valid():
data = form.cleaned_data
return redirect('devhub.submit.upload', data['choices'])
args = [addon.slug] if addon else []
args.append(data['channel'])
return redirect(next_view, *args)
return render(request, 'devhub/addons/submit/distribute.html',
{'distribution_form': form})
{'distribution_form': form,
'submit_page': 'version' if addon else 'addon'})
@login_required
def submit_addon_distribution(request):
return _submit_distribution(request, None, 'devhub.submit.upload')
@dev_required
@waffle_switch('step-version-upload')
@waffle_switch('mixed-listed-unlisted')
def submit_version_distribution(request, addon_id, addon):
return _submit_distribution(request, addon, 'devhub.submit.version.upload')
@transaction.atomic
def submit_addon_upload(request, channel):
form = forms.NewAddonForm(
def _submit_upload(request, addon, channel, next_listed, next_unlisted):
form = forms.NewVersionForm(
request.POST or None,
request.FILES or None,
addon=addon,
request=request
)
is_listed = channel == 'listed'
if request.method == 'POST':
if form.is_valid():
data = form.cleaned_data
if request.method == 'POST' and form.is_valid():
data = form.cleaned_data
is_beta = (data['beta'] and addon and
channel == amo.RELEASE_CHANNEL_LISTED)
p = data.get('supported_platforms', [])
addon = Addon.from_upload(data['upload'], p, source=data['source'],
is_listed=is_listed)
if addon:
version = Version.from_upload(
upload=form.cleaned_data['upload'],
addon=addon,
platforms=data.get('supported_platforms', []),
channel=channel,
source=data['source'],
is_beta=is_beta)
url_args = [addon.slug, version.id]
else:
addon = Addon.from_upload(
upload=data['upload'],
platforms=data.get('supported_platforms', []),
source=data['source'],
is_listed=channel == amo.RELEASE_CHANNEL_LISTED)
version = addon.find_latest_version(channel=channel)
AddonUser(addon=addon, user=request.user).save()
check_validation_override(request, form, addon,
addon.current_version)
if not addon.is_listed: # Not listed? Automatically choose queue.
addon.update(status=amo.STATUS_NOMINATED)
# Sign all the files submitted, one for each platform.
auto_sign_version(addon.versions.get())
return redirect('devhub.submit.finish', addon.slug)
return redirect('devhub.submit.details', addon.slug)
url_args = [addon.slug]
check_validation_override(request, form, addon, version)
addon_update = {}
if data['source']:
addon_update['admin_review'] = True
if addon.status == amo.STATUS_NULL and addon.has_complete_metadata():
addon_update['status'] = amo.STATUS_NOMINATED
if addon_update:
addon.update(**addon_update)
# auto-sign versions (the method checks eligibility)
auto_sign_version(version, is_beta=is_beta)
next_url = (next_listed if channel == amo.RELEASE_CHANNEL_LISTED
else next_unlisted)
return redirect(next_url, *url_args)
is_admin = acl.action_allowed(request, 'ReviewerAdminTools', 'View')
if addon:
channel_choice_text = (forms.DistributionChoiceForm().LISTED_LABEL
if channel == amo.RELEASE_CHANNEL_LISTED else
forms.DistributionChoiceForm().UNLISTED_LABEL)
else:
channel_choice_text = '' # We only need this for Version upload.
return render(request, 'devhub/addons/submit/upload.html',
{'new_addon_form': form, 'is_admin': is_admin,
'listed': is_listed})
{'new_addon_form': form,
'is_admin': is_admin,
'addon': addon,
'submit_page': 'version' if addon else 'addon',
'listed': channel == amo.RELEASE_CHANNEL_LISTED,
'channel_choice_text': channel_choice_text})
@dev_required(submitting=True)
def submit_details(request, addon_id, addon):
forms_, context = [], {}
@login_required
def submit_addon_upload(request, channel):
channel_id = amo.CHANNEL_CHOICES_LOOKUP[channel]
return _submit_upload(request, None, channel_id,
'devhub.submit.details', 'devhub.submit.finish')
@dev_required
@waffle_switch('step-version-upload')
def submit_version_upload(request, addon_id, addon, channel):
if waffle.switch_is_active('mixed-listed-unlisted'):
channel_id = amo.CHANNEL_CHOICES_LOOKUP[channel]
else:
# Don't allow channel choice until rest of AMO supports it.
channel_id = (amo.RELEASE_CHANNEL_LISTED if addon.is_listed else
amo.RELEASE_CHANNEL_UNLISTED)
return _submit_upload(request, addon, channel_id,
'devhub.submit.version.details',
'devhub.submit.version.finish')
@dev_required
@waffle_switch('step-version-upload')
def submit_version(request, addon_id, addon):
if waffle.switch_is_active('mixed-listed-unlisted'):
# choose the channel we need from the last upload
last_version = addon.find_latest_version_including_rejected()
if not last_version:
return redirect('devhub.submit.version.distribution', addon.slug)
channel = last_version.channel
else:
# Don't allow channel choice until rest of AMO supports it.
channel = (amo.RELEASE_CHANNEL_LISTED if addon.is_listed else
amo.RELEASE_CHANNEL_UNLISTED)
return _submit_upload(request, addon, channel,
'devhub.submit.version.details',
'devhub.submit.version.finish')
def _submit_details(request, addon, version):
forms_list, context = [], {}
if version and version.channel == amo.RELEASE_CHANNEL_UNLISTED:
# Not a listed version ? Then nothing to do here.
return redirect('devhub.submit.version.finish', addon.slug, version.pk)
# Figure out the latest version early in order to pass the same instance to
# each form that needs it (otherwise they might overwrite each other).
latest_version = addon.find_latest_version(
latest_version = version or addon.find_latest_version(
channel=amo.RELEASE_CHANNEL_LISTED)
if not latest_version:
# No listed version ? Then nothing to do in the listed submission flow.
return redirect('devhub.submit.finish', addon.slug)
post_data = request.POST if request.method == 'POST' else None
show_all_fields = not version or not addon.has_complete_metadata()
describe_form = forms.DescribeForm(
post_data, instance=addon, request=request)
cat_form = addon_forms.CategoryFormSet(
request.POST or None, addon=addon, request=request)
license_form = forms.LicenseForm(post_data, version=latest_version)
policy_form = forms.PolicyForm(post_data, addon=addon)
reviewer_form = forms.ReviewerNotesForm(
if show_all_fields:
describe_form = forms.DescribeForm(
post_data, instance=addon, request=request)
cat_form = addon_forms.CategoryFormSet(
post_data, addon=addon, request=request)
license_form = forms.LicenseForm(
post_data, version=latest_version, prefix='license')
context.update(license_form.get_context())
context.update(form=describe_form, cat_form=cat_form)
forms_list.extend([describe_form, cat_form, context['license_form']])
reviewer_form = forms.VersionForm(
post_data, instance=latest_version)
context.update(reviewer_form=reviewer_form)
forms_list.append(reviewer_form)
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])
if request.method == 'POST' and all(
form.is_valid() for form in forms_list):
if show_all_fields:
addon = describe_form.save()
cat_form.save()
license_form.save(log=False)
reviewer_form.save()
addon.update(status=amo.STATUS_NOMINATED)
signals.submission_done.send(sender=addon)
else:
reviewer_form.save()
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)
signals.submission_done.send(sender=addon)
return redirect('devhub.submit.finish', addon.slug)
context.update(addon=addon)
return render(request, 'devhub/addons/submit/describe.html', context)
if not version:
return redirect('devhub.submit.finish', addon.slug)
else:
return redirect('devhub.submit.version.finish',
addon.slug, version.id)
context.update(addon=addon, submit_page='version' if version else 'addon')
template = 'devhub/addons/submit/%s' % (
'describe.html' if show_all_fields else 'describe_minimal.html')
return render(request, template, context)
@dev_required(submitting=True)
def submit_finish(request, addon_id, addon):
# Bounce to the details step if incomplete
if not addon.has_complete_metadata():
return redirect('devhub.submit.details', addon.slug)
# Bounce to the versions page if they don't have any versions.
if not addon.versions.exists():
return redirect(addon.get_dev_url('versions'))
uploaded_version = addon.versions.latest()
def submit_addon_details(request, addon_id, addon):
return _submit_details(request, addon, None)
@dev_required(submitting=True)
@waffle_switch('step-version-upload')
def submit_version_details(request, addon_id, addon, version_id):
version = get_object_or_404(Version, id=version_id)
return _submit_details(request, addon, version)
def _submit_finish(request, addon, version):
uploaded_version = version or addon.versions.latest()
supported_platforms = uploaded_version.supported_platforms
is_platform_specific = supported_platforms != [amo.PLATFORM_ALL]
@ -1499,7 +1601,8 @@ def submit_finish(request, addon_id, addon):
# This should never happen.
author = None
if (author and uploaded_version.channel == amo.RELEASE_CHANNEL_LISTED and
if (not version and author and
uploaded_version.channel == amo.RELEASE_CHANNEL_LISTED and
not Version.objects.exclude(pk=uploaded_version.pk)
.filter(addon__authors=author,
channel=amo.RELEASE_CHANNEL_LISTED)
@ -1519,10 +1622,30 @@ def submit_finish(request, addon_id, addon):
tasks.send_welcome_email.delay(addon.id, [author.email], context)
return render(request, 'devhub/addons/submit/done.html',
{'addon': addon, 'uploaded_version': uploaded_version,
{'addon': addon,
'uploaded_version': uploaded_version,
'submit_page': 'version' if version else 'addon',
'is_platform_specific': is_platform_specific})
@dev_required(submitting=True)
def submit_addon_finish(request, addon_id, addon):
# Bounce to the details step if incomplete
if not addon.has_complete_metadata():
return redirect('devhub.submit.details', addon.slug)
# Bounce to the versions page if they don't have any versions.
if not addon.versions.exists():
return redirect(addon.get_dev_url('versions'))
return _submit_finish(request, addon, None)
@dev_required
@waffle_switch('step-version-upload')
def submit_version_finish(request, addon_id, addon, version_id):
version = get_object_or_404(Version, id=version_id)
return _submit_finish(request, addon, version)
@dev_required(submitting=True)
def submit_resume(request, addon_id, addon):
# Redirect to end and @submit_step will send us back if incomplete.

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

@ -0,0 +1,3 @@
INSERT INTO waffle_switch (name, active, note, created, modified)
VALUES ('mixed-listed-unlisted', 0, 'Allow mixed listed/unlisted versions on a single add-on.', NOW(), NOW());

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

@ -0,0 +1,3 @@
INSERT INTO waffle_switch (name, active, note, created, modified)
VALUES ('step-version-upload', 0, 'Use step by step (listed/unlisted) version upload instead of inline.', NOW(), NOW());

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

@ -145,7 +145,7 @@ class Version(OnChangeMixin, ModelBase):
)
log.info(
'New version: %r (%s) from %r' % (version, version.id, upload))
amo.log(amo.LOG.ADD_VERSION, version, addon)
# Update the add-on e10s compatibility since we're creating a new
# version that may change that.
e10s_compatibility = data.get('e10s_compatibility')

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

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