add endpoint for categories with creatured list (bug 855887)
This commit is contained in:
Родитель
e48bb8bd1f
Коммит
d94958b2b2
|
@ -6,6 +6,8 @@ Search API
|
|||
|
||||
This API allows search for apps by various properties.
|
||||
|
||||
.. _search-api:
|
||||
|
||||
Search
|
||||
======
|
||||
|
||||
|
@ -17,8 +19,8 @@ The API accepts various query string parameters to filter or sort by
|
|||
described below:
|
||||
|
||||
* `q` (optional): The query string to search for.
|
||||
* `cat` (optional): The category ID to filter by. Use the category API to
|
||||
find the ids of the categories.
|
||||
* `cat` (optional): The category slug or ID to filter by. Use the
|
||||
category API to find the ids of the categories.
|
||||
* `device` (optional): Filters by supported device. One of 'desktop',
|
||||
'mobile', 'tablet', or 'gaia'.
|
||||
* `premium_types` (optional): Filters by whether the app is free or
|
||||
|
@ -52,5 +54,22 @@ The API returns a list of the apps sorted by relevance (default) or
|
|||
"premium_type": "free",
|
||||
"resource_uri": null,
|
||||
"slug": "marble-run"
|
||||
}, ...
|
||||
}, ...]
|
||||
}
|
||||
|
||||
|
||||
Category Listing With Featured Apps
|
||||
===================================
|
||||
|
||||
.. http:get:: /api/v1/apps/search/creatured/
|
||||
|
||||
**Request**
|
||||
Accepts the same parameters and returns the same objects as the normal
|
||||
search interface: :ref:`search-api`. Includes 'creatured' list of
|
||||
apps, listing featured apps for the requested category, if any.
|
||||
|
||||
**Response**:
|
||||
:param meta: :ref:`meta-response-label`.
|
||||
:param objects: A :ref:`listing <objects-response-label>` of :ref:`apps <app-response-label>` satisfying the search parameters.
|
||||
:param creatured: A list of :ref:`apps <app-response-label>` featured for the requested category, if any
|
||||
:status 200: successfully completed..
|
||||
|
|
|
@ -5,7 +5,7 @@ from tastypie.api import Api
|
|||
from mkt.api.resources import (AppResource, CategoryResource, PreviewResource,
|
||||
StatusResource, ValidationResource)
|
||||
from mkt.ratings.resources import RatingResource
|
||||
from mkt.search.api import SearchResource
|
||||
from mkt.search.api import SearchResource, WithCreaturedResource
|
||||
|
||||
|
||||
api = Api(api_name='apps')
|
||||
|
@ -13,6 +13,7 @@ api.register(ValidationResource())
|
|||
api.register(AppResource())
|
||||
api.register(CategoryResource())
|
||||
api.register(PreviewResource())
|
||||
api.register(WithCreaturedResource())
|
||||
api.register(SearchResource())
|
||||
api.register(StatusResource())
|
||||
api.register(RatingResource())
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import json
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from tastypie import http
|
||||
from tastypie.authorization import ReadOnlyAuthorization
|
||||
from tastypie.utils import trailing_slash
|
||||
from tower import ugettext as _
|
||||
|
||||
import amo
|
||||
from access import acl
|
||||
from addons.models import Category
|
||||
from amo.helpers import absolutify
|
||||
|
||||
import mkt
|
||||
|
@ -13,6 +17,7 @@ from mkt.api.authentication import OptionalOAuthAuthentication
|
|||
from mkt.api.resources import AppResource
|
||||
from mkt.search.views import _get_query, _filter_search
|
||||
from mkt.search.forms import ApiSearchForm
|
||||
from mkt.webapps.models import Webapp
|
||||
|
||||
|
||||
class SearchResource(AppResource):
|
||||
|
@ -30,18 +35,21 @@ class SearchResource(AppResource):
|
|||
# At this time we don't have an API to the Webapp details.
|
||||
return None
|
||||
|
||||
def get_list(self, request=None, **kwargs):
|
||||
def search_form(self, request):
|
||||
form = ApiSearchForm(request.GET if request else None)
|
||||
if not form.is_valid():
|
||||
raise self.form_errors(form)
|
||||
return form.cleaned_data
|
||||
|
||||
def get_list(self, request=None, **kwargs):
|
||||
form_data = self.search_form(request)
|
||||
is_admin = acl.action_allowed(request, 'Admin', '%')
|
||||
is_reviewer = acl.action_allowed(request, 'Apps', 'Review')
|
||||
|
||||
# Pluck out status and addon type first since it forms part of the base
|
||||
# query, but only for privileged users.
|
||||
status = form.cleaned_data['status']
|
||||
addon_type = form.cleaned_data['type']
|
||||
status = form_data['status']
|
||||
addon_type = form_data['type']
|
||||
|
||||
base_filters = {
|
||||
'type': addon_type,
|
||||
|
@ -59,7 +67,7 @@ class SearchResource(AppResource):
|
|||
region = getattr(request, 'REGION', mkt.regions.WORLDWIDE)
|
||||
qs = _get_query(region, gaia=request.GAIA, mobile=request.MOBILE,
|
||||
tablet=request.TABLET, filters=base_filters)
|
||||
qs = _filter_search(request, qs, form.cleaned_data, region=region)
|
||||
qs = _filter_search(request, qs, form_data, region=region)
|
||||
paginator = self._meta.paginator_class(request.GET, qs,
|
||||
resource_uri=self.get_resource_list_uri(),
|
||||
limit=self._meta.limit)
|
||||
|
@ -71,7 +79,8 @@ class SearchResource(AppResource):
|
|||
page['objects'] = [self.full_dehydrate(bundle) for bundle in objs]
|
||||
# This isn't as quite a full as a full TastyPie meta object,
|
||||
# but at least it's namespaced that way and ready to expand.
|
||||
return self.create_response(request, page)
|
||||
to_be_serialized = self.alter_list_data_to_serialize(request, page)
|
||||
return self.create_response(request, to_be_serialized)
|
||||
|
||||
def dehydrate_slug(self, bundle):
|
||||
return bundle.obj.app_slug
|
||||
|
@ -88,3 +97,38 @@ class SearchResource(AppResource):
|
|||
if bundle.obj.is_packaged else None)
|
||||
|
||||
return bundle
|
||||
|
||||
def override_urls(self):
|
||||
return [
|
||||
url(r'^(?P<resource_name>%s)/with_creatured%s$' %
|
||||
(self._meta.resource_name, trailing_slash()),
|
||||
self.wrap_view('with_creatured'), name='api_with_creatured')
|
||||
]
|
||||
|
||||
def with_creatured(self, request, **kwargs):
|
||||
return WithCreaturedResource().dispatch('list', request, **kwargs)
|
||||
|
||||
|
||||
class WithCreaturedResource(SearchResource):
|
||||
|
||||
class Meta(SearchResource.Meta):
|
||||
authorization = ReadOnlyAuthorization()
|
||||
authentication = OptionalOAuthAuthentication()
|
||||
detail_allowed_methods = []
|
||||
fields = SearchResource.Meta.fields + ['cat']
|
||||
list_allowed_methods = ['get']
|
||||
resource_name = 'search/with_creatured'
|
||||
slug_lookup = None
|
||||
|
||||
def alter_list_data_to_serialize(self, request, data):
|
||||
form_data = self.search_form(request)
|
||||
region = getattr(request, 'REGION', mkt.regions.WORLDWIDE)
|
||||
if not form_data['cat']:
|
||||
data['creatured'] = []
|
||||
return data
|
||||
category = Category.objects.get(pk=form_data['cat'])
|
||||
bundles = [self.build_bundle(obj=obj, request=request)
|
||||
for obj in Webapp.featured(cat=category,
|
||||
region=region)]
|
||||
data['creatured'] = [self.full_dehydrate(bundle) for bundle in bundles]
|
||||
return data
|
||||
|
|
|
@ -6,6 +6,8 @@ from tower import ugettext_lazy as _lazy
|
|||
from addons.models import Category
|
||||
import amo
|
||||
|
||||
from mkt.api.forms import SluggableModelChoiceField
|
||||
|
||||
|
||||
ADDON_CHOICES = [(v, v) for k, v in amo.MKT_ADDON_TYPES_API.items()]
|
||||
|
||||
|
@ -143,8 +145,9 @@ class ApiSearchForm(forms.Form):
|
|||
label=_lazy(u'Add-on type'))
|
||||
status = forms.ChoiceField(required=False, choices=STATUS_CHOICES,
|
||||
label=_lazy(u'Status'))
|
||||
cat = forms.TypedChoiceField(required=False, coerce=int, empty_value=None,
|
||||
choices=[], label=_lazy(u'Category'))
|
||||
cat = SluggableModelChoiceField(queryset=Category.objects.all(),
|
||||
sluggable_to_field_name='slug',
|
||||
required=False)
|
||||
device = forms.ChoiceField(
|
||||
required=False, choices=DEVICE_CHOICES, label=_lazy(u'Device type'))
|
||||
premium_types = forms.MultipleChoiceField(
|
||||
|
@ -169,6 +172,10 @@ class ApiSearchForm(forms.Form):
|
|||
'limit': 200,
|
||||
})
|
||||
|
||||
def clean_cat(self):
|
||||
if self.cleaned_data['cat']:
|
||||
return self.cleaned_data['cat'].pk
|
||||
|
||||
def clean_type(self):
|
||||
return amo.MKT_ADDON_TYPES_API_LOOKUP.get(self.cleaned_data['type'],
|
||||
amo.ADDON_WEBAPP)
|
||||
|
|
|
@ -8,6 +8,8 @@ from nose.tools import eq_
|
|||
import amo
|
||||
from addons.models import AddonCategory, AddonDeviceType, Category
|
||||
from amo.tests import ESTestCase
|
||||
import mkt.regions
|
||||
from mkt.api.base import list_url
|
||||
from mkt.api.models import Access, generate
|
||||
from mkt.api.tests.test_oauth import BaseOAuth, OAuthClient
|
||||
from mkt.search.forms import DEVICE_CHOICES_IDS
|
||||
|
@ -250,3 +252,38 @@ class TestApiReviewer(BaseOAuth, ESTestCase):
|
|||
eq_(res.status_code, 400)
|
||||
error = json.loads(res.content)['error_message']
|
||||
eq_(error.keys(), ['type'])
|
||||
|
||||
|
||||
class TestCategoriesWithCreatured(BaseOAuth, ESTestCase):
|
||||
fixtures = fixture('webapp_337141', 'user_2519')
|
||||
list_url = list_url('search/with_creatured')
|
||||
|
||||
def test_creatured_plus_category(self):
|
||||
cat = Category.objects.create(type=amo.ADDON_WEBAPP, slug='shiny')
|
||||
app2 = amo.tests.app_factory()
|
||||
AddonCategory.objects.get_or_create(addon=app2, category=cat)
|
||||
AddonCategory.objects.get_or_create(addon_id=337141, category=cat)
|
||||
self.make_featured(app=app2, category=cat,
|
||||
region=mkt.regions.US)
|
||||
|
||||
self.refresh()
|
||||
res = self.client.get(self.list_url + ({'cat': 'shiny'},))
|
||||
eq_(res.status_code, 200)
|
||||
data = json.loads(res.content)
|
||||
eq_(len(data['objects']), 2)
|
||||
eq_(len(data['creatured']), 1)
|
||||
eq_(int(data['creatured'][0]['id']), app2.pk)
|
||||
|
||||
def test_no_category(self):
|
||||
cat = Category.objects.create(type=amo.ADDON_WEBAPP, slug='shiny')
|
||||
app = amo.tests.app_factory()
|
||||
AddonCategory.objects.get_or_create(addon=app, category=cat)
|
||||
self.make_featured(app=app, category=cat,
|
||||
region=mkt.regions.US)
|
||||
|
||||
self.refresh()
|
||||
res = self.client.get(self.list_url)
|
||||
eq_(res.status_code, 200)
|
||||
data = json.loads(res.content)
|
||||
eq_(len(data['objects']), 2)
|
||||
eq_(len(data['creatured']), 0)
|
||||
|
|
Загрузка…
Ссылка в новой задаче