add in a status api (bug 769750)
This commit is contained in:
Родитель
5e84fecb07
Коммит
19b4a1ff05
|
@ -42,6 +42,41 @@ STATUS_CHOICES = {
|
|||
STATUS_PUBLIC_WAITING: _('Approved but waiting'),
|
||||
}
|
||||
|
||||
# We need to expose nice values that aren't localisable.
|
||||
STATUS_CHOICES_API = {
|
||||
STATUS_NULL: 'incomplete',
|
||||
STATUS_UNREVIEWED: 'unreviewed',
|
||||
STATUS_PENDING: 'pending',
|
||||
STATUS_NOMINATED: 'nominated',
|
||||
STATUS_PUBLIC: 'public',
|
||||
STATUS_DISABLED: 'disabled',
|
||||
STATUS_LISTED: 'listed',
|
||||
STATUS_BETA: 'beta',
|
||||
STATUS_LITE: 'lite',
|
||||
STATUS_LITE_AND_NOMINATED: 'lite-nominated',
|
||||
STATUS_PURGATORY: 'purgatory',
|
||||
STATUS_DELETED: 'deleted',
|
||||
STATUS_REJECTED: 'rejected',
|
||||
STATUS_PUBLIC_WAITING: 'waiting',
|
||||
}
|
||||
|
||||
STATUS_CHOICES_API_LOOKUP = {
|
||||
'incomplete': STATUS_NULL,
|
||||
'unreviewed': STATUS_UNREVIEWED,
|
||||
'pending': STATUS_PENDING,
|
||||
'nominated': STATUS_NOMINATED,
|
||||
'public': STATUS_PUBLIC,
|
||||
'disabled': STATUS_DISABLED,
|
||||
'listed': STATUS_LISTED,
|
||||
'beta': STATUS_BETA,
|
||||
'lite': STATUS_LITE,
|
||||
'lite-nominated': STATUS_LITE_AND_NOMINATED,
|
||||
'purgatory': STATUS_PURGATORY,
|
||||
'deleted': STATUS_DELETED,
|
||||
'rejected': STATUS_REJECTED,
|
||||
'waiting': STATUS_PUBLIC_WAITING,
|
||||
}
|
||||
|
||||
PUBLIC_IMMEDIATELY = None
|
||||
# Our MySQL does not store microseconds.
|
||||
PUBLIC_WAIT = datetime.max.replace(microsecond=0)
|
||||
|
|
|
@ -289,10 +289,46 @@ Delete
|
|||
|
||||
Delete a screenshot of video::
|
||||
|
||||
DELETE /en-US/api/apps/previe/<preview id>/
|
||||
DELETE /en-US/api/apps/preview/<preview id>/
|
||||
|
||||
This will return a 204 if the screenshot has been deleted.
|
||||
|
||||
Enabling an App
|
||||
===============
|
||||
|
||||
Once all the data has been completed and at least one screenshot created, you
|
||||
can push the app to the review queue::
|
||||
|
||||
PATCH /en-US/api/apps/status/<app id>/
|
||||
{"status": "pending"}
|
||||
|
||||
* `status` (optional): key statuses are
|
||||
|
||||
* `incomplete`: incomplete
|
||||
* `pending`: pending
|
||||
* `public`: public
|
||||
* `waiting`: waiting to be public
|
||||
|
||||
* `disabled_by_user` (optional): `True` or `False`.
|
||||
|
||||
Valid transitions that users can initiate are:
|
||||
|
||||
* *waiting to be public* to *public*: occurs when the app has been reviewed,
|
||||
but not yet been made public.
|
||||
* *incomplete* to *pending*: call this once your app has been completed and it
|
||||
will be added to the Marketplace review queue. This can only be called if all
|
||||
the required data is there. If not, you'll get an error containing the
|
||||
reason. For example::
|
||||
|
||||
PATCH /en-US/api/apps/status/<app id>/
|
||||
{"status": "pending"}
|
||||
|
||||
Status code: 400
|
||||
{"error_message":
|
||||
{"status": ["You must provide a support email.",
|
||||
"You must provide at least one device type.",
|
||||
"You must provide at least one category.",
|
||||
"You must upload at least one screenshot or video."]}}
|
||||
|
||||
Other APIs
|
||||
----------
|
||||
|
|
|
@ -6,7 +6,7 @@ from django import forms
|
|||
|
||||
import happyforms
|
||||
|
||||
from addons.models import Category
|
||||
from addons.models import Addon, Category
|
||||
import amo
|
||||
from files.models import FileUpload
|
||||
from mkt.developers.utils import check_upload
|
||||
|
@ -71,3 +71,35 @@ class CategoryForm(happyforms.Form):
|
|||
# Hopefully this is easier.
|
||||
categories = forms.ModelMultipleChoiceField(
|
||||
queryset=Category.objects.filter(type=amo.ADDON_WEBAPP))
|
||||
|
||||
|
||||
class StatusForm(happyforms.ModelForm):
|
||||
status = forms.ChoiceField(choices=(), required=False)
|
||||
|
||||
lookup = {
|
||||
# You can push to the pending queue.
|
||||
amo.STATUS_NULL: amo.STATUS_PENDING,
|
||||
# You can push to public if you've been reviewed.
|
||||
amo.STATUS_PUBLIC_WAITING: amo.STATUS_PUBLIC,
|
||||
}
|
||||
|
||||
class Meta:
|
||||
model = Addon
|
||||
fields = ['status', 'disabled_by_user']
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
super(StatusForm, self).__init__(*args, **kw)
|
||||
choice = self.lookup.get(self.instance.status)
|
||||
choices = []
|
||||
if self.instance.status in self.lookup:
|
||||
choices.append(amo.STATUS_CHOICES_API[choice])
|
||||
choices.append(amo.STATUS_CHOICES_API[self.instance.status])
|
||||
self.fields['status'].choices = [(k, k) for k in sorted(choices)]
|
||||
|
||||
def clean_status(self):
|
||||
requested = self.cleaned_data['status']
|
||||
if requested == amo.STATUS_CHOICES_API[amo.STATUS_PENDING]:
|
||||
valid, reasons = self.instance.is_complete()
|
||||
if not valid:
|
||||
raise forms.ValidationError(reasons)
|
||||
return amo.STATUS_CHOICES_API_LOOKUP[requested]
|
||||
|
|
|
@ -18,7 +18,7 @@ from files.models import FileUpload, Platform
|
|||
from mkt.api.authentication import (AppOwnerAuthorization, OwnerAuthorization,
|
||||
MarketplaceAuthentication)
|
||||
from mkt.api.base import MarketplaceResource
|
||||
from mkt.api.forms import CategoryForm, UploadForm
|
||||
from mkt.api.forms import CategoryForm, UploadForm, StatusForm
|
||||
from mkt.developers import tasks
|
||||
from mkt.developers.forms import NewManifestForm
|
||||
from mkt.developers.forms import PreviewForm
|
||||
|
@ -185,6 +185,54 @@ class AppResource(MarketplaceResource):
|
|||
return bundle
|
||||
|
||||
|
||||
class StatusResource(MarketplaceResource):
|
||||
|
||||
class Meta:
|
||||
queryset = Addon.objects.filter(type=amo.ADDON_WEBAPP)
|
||||
fields = ['status', 'disabled_by_user']
|
||||
list_allowed_methods = []
|
||||
allowed_methods = ['patch', 'get']
|
||||
always_return_data = True
|
||||
authentication = MarketplaceAuthentication()
|
||||
authorization = AppOwnerAuthorization()
|
||||
resource_name = 'status'
|
||||
serializer = Serializer(formats=['json'])
|
||||
|
||||
@write
|
||||
@transaction.commit_on_success
|
||||
def obj_update(self, bundle, request, **kwargs):
|
||||
try:
|
||||
obj = self.get_object_list(bundle.request).get(**kwargs)
|
||||
except Addon.DoesNotExist:
|
||||
raise ImmediateHttpResponse(response=http.HttpNotFound())
|
||||
|
||||
if not AppOwnerAuthorization().is_authorized(request, object=obj):
|
||||
raise ImmediateHttpResponse(response=http.HttpForbidden())
|
||||
|
||||
form = StatusForm(bundle.data, instance=obj)
|
||||
if not form.is_valid():
|
||||
raise self.form_errors(form)
|
||||
|
||||
form.save()
|
||||
log.info('App status updated: %s' % obj.pk)
|
||||
bundle.obj = obj
|
||||
return bundle
|
||||
|
||||
def obj_get(self, request=None, **kwargs):
|
||||
obj = super(StatusResource, self).obj_get(request=request, **kwargs)
|
||||
if not AppOwnerAuthorization().is_authorized(request, object=obj):
|
||||
raise ImmediateHttpResponse(response=http.HttpForbidden())
|
||||
|
||||
log.info('App status retreived: %s' % obj.pk)
|
||||
return obj
|
||||
|
||||
def dehydrate_status(self, bundle):
|
||||
return amo.STATUS_CHOICES_API[int(bundle.data['status'])]
|
||||
|
||||
def hydrate_status(self, bundle):
|
||||
return amo.STATUS_CHOICES_API_LOOKUP[int(bundle.data['status'])]
|
||||
|
||||
|
||||
class CategoryResource(MarketplaceResource):
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -2,9 +2,10 @@ import base64
|
|||
|
||||
from nose.tools import eq_
|
||||
|
||||
from addons.models import Addon
|
||||
import amo
|
||||
import amo.tests
|
||||
from mkt.api.forms import PreviewJSONForm
|
||||
from mkt.api.forms import PreviewJSONForm, StatusForm
|
||||
|
||||
|
||||
class TestPreviewForm(amo.tests.TestCase, amo.tests.AMOPaths):
|
||||
|
@ -46,3 +47,29 @@ class TestPreviewForm(amo.tests.TestCase, amo.tests.AMOPaths):
|
|||
form = PreviewJSONForm({'position': 1})
|
||||
assert not form.is_valid()
|
||||
eq_(form.errors['file'], ['This field is required.'])
|
||||
|
||||
|
||||
class TestSubmitForm(amo.tests.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.addon = Addon()
|
||||
|
||||
def test_status_null(self):
|
||||
self.addon.status = amo.STATUS_NULL
|
||||
status = StatusForm(instance=self.addon).fields['status']
|
||||
eq_([k for k, v in status.choices],
|
||||
['incomplete', 'pending'])
|
||||
|
||||
def test_status_public(self):
|
||||
self.addon.status = amo.STATUS_PUBLIC_WAITING
|
||||
status = StatusForm(instance=self.addon).fields['status']
|
||||
eq_([k for k, v in status.choices],
|
||||
['public', 'waiting'])
|
||||
|
||||
def test_status_other(self):
|
||||
for s in amo.STATUS_CHOICES.keys():
|
||||
if s in [amo.STATUS_NULL, amo.STATUS_PUBLIC_WAITING]:
|
||||
continue
|
||||
self.addon.status = s
|
||||
status = StatusForm(instance=self.addon).fields['status']
|
||||
eq_([k for k, v in status.choices], [k])
|
||||
|
|
|
@ -327,6 +327,80 @@ class TestAppCreateHandler(CreateHandler, AMOPaths):
|
|||
eq_(res.status_code, 404)
|
||||
|
||||
|
||||
@patch.object(settings, 'SITE_URL', 'http://api/')
|
||||
class TestAppStatusHandler(CreateHandler, AMOPaths):
|
||||
|
||||
fixtures = ['base/user_2519', 'base/users',
|
||||
'base/platforms', 'base/appversion']
|
||||
|
||||
def setUp(self):
|
||||
super(TestAppStatusHandler, self).setUp()
|
||||
self.list_url = ('api_dispatch_list', {'resource_name': 'status'})
|
||||
|
||||
def create_app(self):
|
||||
obj = self.create()
|
||||
res = self.client.post(('api_dispatch_list', {'resource_name': 'app'}),
|
||||
data=json.dumps({'manifest': obj.uuid}))
|
||||
pk = json.loads(res.content)['id']
|
||||
self.get_url = ('api_dispatch_detail',
|
||||
{'resource_name': 'status', 'pk': pk})
|
||||
return Webapp.objects.get(pk=pk)
|
||||
|
||||
def test_verbs(self):
|
||||
self._allowed_verbs(self.list_url, [])
|
||||
|
||||
def test_status(self):
|
||||
self.create_app()
|
||||
res = self.client.get(self.get_url)
|
||||
eq_(res.status_code, 200)
|
||||
data = json.loads(res.content)
|
||||
eq_(data['disabled_by_user'], False)
|
||||
eq_(data['status'], 'incomplete')
|
||||
|
||||
def test_disable(self):
|
||||
self.create_app()
|
||||
res = self.client.patch(self.get_url,
|
||||
data=json.dumps({'disabled_by_user': True}))
|
||||
eq_(res.status_code, 202, res.content)
|
||||
data = json.loads(res.content)
|
||||
eq_(data['disabled_by_user'], True)
|
||||
eq_(data['status'], 'incomplete')
|
||||
|
||||
def test_change_status_fails(self):
|
||||
self.create_app()
|
||||
res = self.client.patch(self.get_url,
|
||||
data=json.dumps({'status': 'pending'}))
|
||||
eq_(res.status_code, 400)
|
||||
assert isinstance(self.get_error(res)['status'], list)
|
||||
|
||||
@patch('mkt.webapps.models.Webapp.is_complete')
|
||||
def test_change_status_passes(self, is_complete):
|
||||
is_complete.return_value = True, []
|
||||
self.create_app()
|
||||
res = self.client.patch(self.get_url,
|
||||
data=json.dumps({'status': 'pending'}))
|
||||
eq_(res.status_code, 202, res.content)
|
||||
eq_(json.loads(res.content)['status'], 'pending')
|
||||
|
||||
@patch('mkt.webapps.models.Webapp.is_complete')
|
||||
def test_cant_skip(self, is_complete):
|
||||
is_complete.return_value = True, []
|
||||
app = self.create_app()
|
||||
res = self.client.patch(self.get_url,
|
||||
data=json.dumps({'status': 'public'}))
|
||||
eq_(res.status_code, 400)
|
||||
assert 'available choices' in self.get_error(res)['status'][0]
|
||||
eq_(Addon.objects.get(pk=app.pk).status, amo.STATUS_NULL)
|
||||
|
||||
def test_public_waiting(self):
|
||||
app = self.create_app()
|
||||
app.update(status=amo.STATUS_PUBLIC_WAITING)
|
||||
res = self.client.patch(self.get_url,
|
||||
data=json.dumps({'status': 'public'}))
|
||||
eq_(res.status_code, 202)
|
||||
eq_(json.loads(res.content)['status'], 'public')
|
||||
|
||||
|
||||
class TestCategoryHandler(BaseOAuth):
|
||||
|
||||
def setUp(self):
|
||||
|
|
|
@ -25,7 +25,7 @@ import urlparse
|
|||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.test.client import Client
|
||||
from django.test.client import Client, FakePayload
|
||||
|
||||
import oauth2 as oauth
|
||||
from mock import Mock, patch
|
||||
|
@ -110,7 +110,7 @@ class OAuthClient(Client):
|
|||
'CONTENT_TYPE': 'application/json',
|
||||
'PATH_INFO': urllib.unquote(parsed[2]),
|
||||
'REQUEST_METHOD': 'PATCH',
|
||||
'wsgi.input': data,
|
||||
'wsgi.input': FakePayload(data),
|
||||
'HTTP_HOST': 'api',
|
||||
'HTTP_AUTHORIZATION': self.header('PATCH', url)
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@ from django.conf.urls.defaults import include, patterns, url
|
|||
from tastypie.api import Api
|
||||
from mkt.search.api import SearchResource
|
||||
from mkt.api.resources import (AppResource, CategoryResource,
|
||||
DeviceTypeResource,
|
||||
PreviewResource, ValidationResource)
|
||||
DeviceTypeResource, PreviewResource,
|
||||
StatusResource, ValidationResource)
|
||||
|
||||
api = Api(api_name='apps')
|
||||
api.register(ValidationResource())
|
||||
|
@ -13,6 +13,7 @@ api.register(CategoryResource())
|
|||
api.register(DeviceTypeResource())
|
||||
api.register(SearchResource())
|
||||
api.register(PreviewResource())
|
||||
api.register(StatusResource())
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^', include(api.urls)),
|
||||
|
|
Загрузка…
Ссылка в новой задаче