add in choice of when to go public (bug 740967)

This commit is contained in:
Andy McKay 2012-04-17 11:26:30 -07:00
Родитель 0ac647a972
Коммит 213eace875
17 изменённых файлов: 161 добавлений и 16 удалений

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

@ -283,6 +283,7 @@ class Addon(amo.models.OnChangeMixin, amo.models.ModelBase):
objects = AddonManager()
with_deleted = AddonManager(include_deleted=True)
make_public = models.DateTimeField(null=True)
class Meta:
db_table = 'addons'

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

@ -379,6 +379,16 @@ class MANIFEST_UPDATED(_LOG):
format = _(u'{addon} manifest updated.')
class APPROVE_VERSION_WAITING(_LOG):
id = 53
action_class = 'approve'
format = _(u'{addon} {version} approved but waiting to be made public.')
short = _(u'Approved but waiting')
keep = True
review_email_user = True
review_queue = True
class CUSTOM_TEXT(_LOG):
id = 98
format = '{0}'

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

@ -1,3 +1,4 @@
from datetime import datetime
import re
from django.conf import settings
@ -19,6 +20,7 @@ STATUS_LITE_AND_NOMINATED = 9
STATUS_PURGATORY = 10 # A temporary home; bug 614686
STATUS_DELETED = 11
STATUS_REJECTED = 12 # This applies only to apps (for now)
STATUS_PUBLIC_WAITING = 13 # bug 740967
STATUS_CHOICES = {
STATUS_NULL: _(u'Incomplete'),
@ -34,18 +36,26 @@ STATUS_CHOICES = {
_(u'Preliminarily Reviewed and Awaiting Full Review'),
STATUS_PURGATORY: _(u'Pending a review choice'),
STATUS_DELETED: _(u'Deleted'),
STATUS_REJECTED: _(u'Rejected')
STATUS_REJECTED: _(u'Rejected'),
# Approved, but the developer would like to put it public when they want.
# The need to go to the marketplace and actualy make it public.
STATUS_PUBLIC_WAITING: _('Approved but waiting'),
}
PUBLIC_IMMEDIATELY = None
# Our MySQL does not store microseconds.
PUBLIC_WAIT = datetime.max.replace(microsecond=0)
REVIEWED_STATUSES = (STATUS_LITE, STATUS_LITE_AND_NOMINATED, STATUS_PUBLIC)
UNREVIEWED_STATUSES = (STATUS_UNREVIEWED, STATUS_PENDING, STATUS_NOMINATED,
STATUS_PURGATORY)
VALID_STATUSES = (STATUS_UNREVIEWED, STATUS_PENDING, STATUS_NOMINATED,
STATUS_PUBLIC, STATUS_LISTED, STATUS_BETA, STATUS_LITE,
STATUS_LITE_AND_NOMINATED, STATUS_PURGATORY)
STATUS_LITE_AND_NOMINATED, STATUS_PURGATORY,
STATUS_PUBLIC_WAITING)
# We don't show addons/versions with UNREVIEWED_STATUS in public.
LISTED_STATUSES = tuple(st for st in VALID_STATUSES
if st not in (STATUS_PENDING,))
if st not in (STATUS_PENDING, STATUS_PUBLIC_WAITING))
# An add-on in one of these statuses is awaiting a review.
STATUS_UNDER_REVIEW = (STATUS_UNREVIEWED, STATUS_NOMINATED,

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

@ -55,7 +55,8 @@
.status-incomplete b,
.status-disabled b,
.status-admin-disabled b,
.status-purgatory b {
.status-purgatory b,
.status-waiting b {
color: #851006;
}

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

@ -0,0 +1 @@
ALTER TABLE addons ADD COLUMN `make_public` datetime;

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

@ -183,6 +183,7 @@ def status_class(addon):
amo.STATUS_LITE: 'lite',
amo.STATUS_LITE_AND_NOMINATED: 'lite-nom',
amo.STATUS_PURGATORY: 'purgatory',
amo.STATUS_PUBLIC_WAITING: 'waiting',
}
if addon.disabled_by_user and addon.status != amo.STATUS_DISABLED:
cls = 'disabled'

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

@ -36,6 +36,9 @@
questions, please email app-reviews@mozilla.org.") }}
{% elif addon.status == amo.STATUS_REJECTED %}
{{ status(_('This app has been <b>rejected</b> by a Mozilla Marketplace reviewer.')|safe) }}
{% elif addon.status == amo.STATUS_PUBLIC_WAITING %}
{{ status(_('Your app has been <b>approved but is not public</b>.')|safe) }}
{{ _('It is waiting your approval to make public.') }}
{% endif %}
{% if not (addon.is_disabled or addon.is_incomplete()) %}
<a href="https://developer.mozilla.org/en/Apps/Marketplace_Review"
@ -68,6 +71,11 @@
{{ form_field(form.release_notes, opt=True) }}
<p><button type="submit">{{ _('Resubmit App') }}</button></p>
</form>
{% elif addon.status == amo.STATUS_PUBLIC_WAITING %}
<form method="post" action="{{ addon.get_dev_url('publicise') }}">
{{ csrf() }}
<p><button type="submit">{{ _('Make App public') }}</button></p>
</form>
{% endif %}
</p>
<p class="version-status-actions listing-footer">

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

@ -6,7 +6,6 @@ from decimal import Decimal
from django.conf import settings
from django.core import mail
from django.utils.http import urlencode
import mock
from nose.plugins.attrib import attr
@ -589,7 +588,7 @@ class TestIssueRefund(amo.tests.TestCase):
@mock.patch('stats.models.Contribution.enqueue_refund')
@mock.patch('paypal.refund')
def test_apps_issue(self, refund, enqueue_refund):
def test_apps_issue_error(self, refund, enqueue_refund):
refund.side_effect = PaypalError
c = self.make_purchase()
r = self.client.post(self.url, {'transaction_id': c.transaction_id,
@ -829,6 +828,39 @@ class TestRefunds(amo.tests.TestCase):
babel_datetime(refund.requested).strip())
class TestPublicise(amo.tests.TestCase):
fixtures = ['webapps/337141-steamcube']
def setUp(self):
self.webapp = self.get_webapp()
self.webapp.update(status=amo.STATUS_PUBLIC_WAITING)
self.publicise_url = self.webapp.get_dev_url('publicise')
self.status_url = self.webapp.get_dev_url('versions')
assert self.client.login(username='steamcube@mozilla.com',
password='password')
def get_webapp(self):
return Addon.objects.no_cache().get(id=337141)
def test_logout(self):
self.client.logout()
res = self.client.post(self.publicise_url)
eq_(res.status_code, 302)
eq_(self.get_webapp().status, amo.STATUS_PUBLIC_WAITING)
def test_publicise(self):
res = self.client.post(self.publicise_url)
eq_(res.status_code, 302)
eq_(self.get_webapp().status, amo.STATUS_PUBLIC)
def test_status(self):
res = self.client.get(self.status_url)
eq_(res.status_code, 200)
doc = pq(res.content)
eq_(doc('#version-status form').attr('action'), self.publicise_url)
eq_(len(doc('strong.status-waiting')), 1)
class TestDelete(amo.tests.TestCase):
fixtures = ['webapps/337141-steamcube']

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

@ -35,6 +35,7 @@ app_detail_patterns = patterns('',
url('^enable$', views.enable, name='mkt.developers.apps.enable'),
url('^delete$', views.delete, name='mkt.developers.apps.delete'),
url('^disable$', views.disable, name='mkt.developers.apps.disable'),
url('^publicise$', views.publicise, name='mkt.developers.apps.publicise'),
url('^status$', views.status, name='mkt.developers.apps.versions'),
url('^payments$', views.payments, name='mkt.developers.apps.payments'),

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

@ -185,6 +185,15 @@ def disable(request, addon_id, addon):
return redirect(addon.get_dev_url('versions'))
@dev_required
@post_required
def publicise(request, addon_id, addon):
if addon.status == amo.STATUS_PUBLIC_WAITING:
addon.update(status=amo.STATUS_PUBLIC)
amo.log(amo.LOG.CHANGE_STATUS, addon.get_status_display(), addon)
return redirect(addon.get_dev_url('versions'))
@dev_required(webapp=True)
def status(request, addon_id, addon, webapp=False):
form = forms.AppAppealForm(request.POST, product=addon)

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

@ -123,7 +123,7 @@ class TestReviewApp(AppReviewerTest, EditorTest):
def _check_log(self, action):
assert AppLog.objects.filter(addon=self.app,
activity_log__action=action.id).exists(), (
activity_log__action=action.id).exists(), (
"Didn't find `%s` action in logs." % action.short)
def test_push_public(self):
@ -143,6 +143,24 @@ class TestReviewApp(AppReviewerTest, EditorTest):
self._check_email(msg, 'App Approved')
self._check_email_body(msg)
def test_push_public_waiting(self):
files = list(self.version.files.values_list('id', flat=True))
self.get_app().update(make_public=amo.PUBLIC_WAIT)
self.post({
'action': 'public',
'operating_systems': '',
'applications': '',
'comments': 'something',
'addon_files': files,
})
eq_(self.get_app().status, amo.STATUS_PUBLIC_WAITING)
self._check_log(amo.LOG.APPROVE_VERSION_WAITING)
eq_(len(mail.outbox), 1)
msg = mail.outbox[0]
self._check_email(msg, 'App Approved but waiting')
self._check_email_body(msg)
def test_comment(self):
self.post({'action': 'comment', 'comments': 'mmm, nice app'})
eq_(len(mail.outbox), 0)
@ -204,7 +222,8 @@ class TestCannedResponses(EditorTest):
# choices is grouped by the sort_group, where choices[0] is the
# default "Choose a response..." option.
# Within that, it's paired by [group, [[response, name],...]].
# So above, choices[1][1] gets the first real group's list of responses.
# So above, choices[1][1] gets the first real group's list of
# responses.
eq_(len(choices), 1)
assert self.cr_app.response in choices[0]
assert self.cr_addon.response not in choices[0]

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

@ -56,7 +56,6 @@ class WebappQueueTable(tables.ModelTable, ItemStateTable):
return ''.join(o)
def render_created(self, row):
return timesince(row.created)
@ -179,11 +178,28 @@ class ReviewApp(ReviewBase):
self.files = self.version.files.all()
def process_public(self):
if self.addon.make_public == amo.PUBLIC_IMMEDIATELY:
return self.process_public_immediately()
return self.process_public_waiting()
def process_public_waiting(self):
"""Make an app pending."""
self.set_files(amo.STATUS_PUBLIC_WAITING, self.version.files.all())
self.set_addon(highest_status=amo.STATUS_PUBLIC_WAITING,
status=amo.STATUS_PUBLIC_WAITING)
self.log_action(amo.LOG.APPROVE_VERSION_WAITING)
self.notify_email('%s_to_public_waiting' % self.review_type,
u'App Approved but waiting: %s')
log.info(u'Making %s public but pending' % self.addon)
log.info(u'Sending email for %s' % self.addon)
def process_public_immediately(self):
"""Approve an app."""
# Save files first, because set_addon checks to make sure there
# is at least one public file or it won't make the addon public.
self.set_files(amo.STATUS_PUBLIC, self.version.files.all(),
copy_to_mirror=True)
self.set_files(amo.STATUS_PUBLIC, self.version.files.all())
self.set_addon(highest_status=amo.STATUS_PUBLIC,
status=amo.STATUS_PUBLIC)

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

@ -39,6 +39,7 @@ INSTALLED_APPS.remove('search')
INSTALLED_APPS = tuple(INSTALLED_APPS)
INSTALLED_APPS += (
'devhub', # Put here so helpers.py doesn't get loaded first.
'mkt.site',
'mkt.account',
'mkt.browse',
@ -53,7 +54,6 @@ INSTALLED_APPS += (
'mkt.submit',
'mkt.support',
'mkt.webapps',
'devhub', # Put here so helpers.py doesn't get loaded first.
)
SUPPORTED_NONLOCALES += (

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

@ -1,5 +1,6 @@
from django import forms
import amo
from tower import ugettext as _
@ -9,7 +10,12 @@ APP_UPSELL_CHOICES = (
)
APP_PUBLIC_CHOICES = (
(0, _('As soon as it is approved.')),
(1, _('Not until I manually make it public.')),
)
class AddonChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
return obj.name

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

@ -18,7 +18,8 @@ from translations.fields import TransField
from mkt.developers.forms import (PaypalSetupForm as OriginalPaypalSetupForm,
verify_app_domain)
from mkt.site.forms import AddonChoiceField, APP_UPSELL_CHOICES
from mkt.site.forms import (AddonChoiceField, APP_UPSELL_CHOICES,
APP_PUBLIC_CHOICES)
class DevAgreementForm(happyforms.Form):
@ -74,7 +75,12 @@ class UpsellForm(happyforms.Form):
label=_('App Price'),
empty_label=None,
required=True)
make_public = forms.TypedChoiceField(choices=APP_PUBLIC_CHOICES,
widget=forms.RadioSelect(),
label=_('When should your app be '
'made available for sale?'),
coerce=int,
required=False)
do_upsell = forms.TypedChoiceField(coerce=lambda x: bool(int(x)),
choices=APP_UPSELL_CHOICES,
widget=forms.RadioSelect(),
@ -98,6 +104,7 @@ class UpsellForm(happyforms.Form):
if 'initial' not in kw:
kw['initial'] = {}
kw['initial']['make_public'] = amo.PUBLIC_IMMEDIATELY
if self.addon.premium:
kw['initial']['price'] = self.addon.premium.price
@ -120,6 +127,10 @@ class UpsellForm(happyforms.Form):
raise_required()
return self.cleaned_data['free']
def clean_make_public(self):
return (amo.PUBLIC_WAIT if self.cleaned_data.get('make_public')
else None)
def save(self):
if 'price' in self.cleaned_data:
premium = self.addon.premium
@ -145,6 +156,8 @@ class UpsellForm(happyforms.Form):
elif not self.cleaned_data['do_upsell'] and upsell:
upsell.delete()
self.addon.update(make_public=self.cleaned_data['make_public'])
class AppDetailsBasicForm(AddonFormBasic):
"""Form for "Details" submission step."""

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

@ -20,6 +20,7 @@
<a href="https://developer.mozilla.org/en/Apps/Marketplace_Payments#Price_tiers">
Learn more about price tiers</a></p>
{{ form_field(form.price) }}
{{ form_field(form.make_public) }}
{% if form.fields['free'].queryset.count() %}
<p>{{ loc('Linking this app with its free counterpart allows you
to promote your premium app next to the free version.

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

@ -818,6 +818,22 @@ class TestPayments(TestSubmit):
AddonUser.objects.create(addon=free, user=self.user)
return free
def test_immediate(self):
self.webapp.update(premium_type=amo.ADDON_PREMIUM)
res = self.client.post(self.get_url('payments.upsell'),
{'price': self.price.pk,
'make_public': 0})
eq_(res.status_code, 302)
eq_(self.get_webapp().make_public, amo.PUBLIC_IMMEDIATELY)
def test_wait(self):
self.webapp.update(premium_type=amo.ADDON_PREMIUM)
res = self.client.post(self.get_url('payments.upsell'),
{'price': self.price.pk,
'make_public': 1})
eq_(res.status_code, 302)
eq_(self.get_webapp().make_public, amo.PUBLIC_WAIT)
def test_upsell_states(self):
free = self._make_upsell()
free.update(status=amo.STATUS_NULL)
@ -850,7 +866,7 @@ class TestPayments(TestSubmit):
res = self.client.get(self.get_url('payments.upsell'),
{'price': self.price.pk})
eq_(res.status_code, 200)
eq_(len(pq(res.content)('div.brform')), 2)
eq_(len(pq(res.content)('div.brform')), 3)
def test_upsell_missing(self):
free = Addon.objects.create(type=amo.ADDON_WEBAPP)