add in a status api (bug 769750)

This commit is contained in:
Andy McKay 2012-07-25 14:14:42 -07:00
Родитель 5e84fecb07
Коммит 19b4a1ff05
8 изменённых файлов: 261 добавлений и 8 удалений

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

@ -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)),