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.
|
This API allows search for apps by various properties.
|
||||||
|
|
||||||
|
.. _search-api:
|
||||||
|
|
||||||
Search
|
Search
|
||||||
======
|
======
|
||||||
|
|
||||||
|
@ -17,8 +19,8 @@ The API accepts various query string parameters to filter or sort by
|
||||||
described below:
|
described below:
|
||||||
|
|
||||||
* `q` (optional): The query string to search for.
|
* `q` (optional): The query string to search for.
|
||||||
* `cat` (optional): The category ID to filter by. Use the category API to
|
* `cat` (optional): The category slug or ID to filter by. Use the
|
||||||
find the ids of the categories.
|
category API to find the ids of the categories.
|
||||||
* `device` (optional): Filters by supported device. One of 'desktop',
|
* `device` (optional): Filters by supported device. One of 'desktop',
|
||||||
'mobile', 'tablet', or 'gaia'.
|
'mobile', 'tablet', or 'gaia'.
|
||||||
* `premium_types` (optional): Filters by whether the app is free or
|
* `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",
|
"premium_type": "free",
|
||||||
"resource_uri": null,
|
"resource_uri": null,
|
||||||
"slug": "marble-run"
|
"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,
|
from mkt.api.resources import (AppResource, CategoryResource, PreviewResource,
|
||||||
StatusResource, ValidationResource)
|
StatusResource, ValidationResource)
|
||||||
from mkt.ratings.resources import RatingResource
|
from mkt.ratings.resources import RatingResource
|
||||||
from mkt.search.api import SearchResource
|
from mkt.search.api import SearchResource, WithCreaturedResource
|
||||||
|
|
||||||
|
|
||||||
api = Api(api_name='apps')
|
api = Api(api_name='apps')
|
||||||
|
@ -13,6 +13,7 @@ api.register(ValidationResource())
|
||||||
api.register(AppResource())
|
api.register(AppResource())
|
||||||
api.register(CategoryResource())
|
api.register(CategoryResource())
|
||||||
api.register(PreviewResource())
|
api.register(PreviewResource())
|
||||||
|
api.register(WithCreaturedResource())
|
||||||
api.register(SearchResource())
|
api.register(SearchResource())
|
||||||
api.register(StatusResource())
|
api.register(StatusResource())
|
||||||
api.register(RatingResource())
|
api.register(RatingResource())
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from django.conf.urls import url
|
||||||
|
|
||||||
from tastypie import http
|
from tastypie import http
|
||||||
from tastypie.authorization import ReadOnlyAuthorization
|
from tastypie.authorization import ReadOnlyAuthorization
|
||||||
|
from tastypie.utils import trailing_slash
|
||||||
from tower import ugettext as _
|
from tower import ugettext as _
|
||||||
|
|
||||||
import amo
|
import amo
|
||||||
from access import acl
|
from access import acl
|
||||||
|
from addons.models import Category
|
||||||
from amo.helpers import absolutify
|
from amo.helpers import absolutify
|
||||||
|
|
||||||
import mkt
|
import mkt
|
||||||
|
@ -13,6 +17,7 @@ from mkt.api.authentication import OptionalOAuthAuthentication
|
||||||
from mkt.api.resources import AppResource
|
from mkt.api.resources import AppResource
|
||||||
from mkt.search.views import _get_query, _filter_search
|
from mkt.search.views import _get_query, _filter_search
|
||||||
from mkt.search.forms import ApiSearchForm
|
from mkt.search.forms import ApiSearchForm
|
||||||
|
from mkt.webapps.models import Webapp
|
||||||
|
|
||||||
|
|
||||||
class SearchResource(AppResource):
|
class SearchResource(AppResource):
|
||||||
|
@ -30,18 +35,21 @@ class SearchResource(AppResource):
|
||||||
# At this time we don't have an API to the Webapp details.
|
# At this time we don't have an API to the Webapp details.
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_list(self, request=None, **kwargs):
|
def search_form(self, request):
|
||||||
form = ApiSearchForm(request.GET if request else None)
|
form = ApiSearchForm(request.GET if request else None)
|
||||||
if not form.is_valid():
|
if not form.is_valid():
|
||||||
raise self.form_errors(form)
|
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_admin = acl.action_allowed(request, 'Admin', '%')
|
||||||
is_reviewer = acl.action_allowed(request, 'Apps', 'Review')
|
is_reviewer = acl.action_allowed(request, 'Apps', 'Review')
|
||||||
|
|
||||||
# Pluck out status and addon type first since it forms part of the base
|
# Pluck out status and addon type first since it forms part of the base
|
||||||
# query, but only for privileged users.
|
# query, but only for privileged users.
|
||||||
status = form.cleaned_data['status']
|
status = form_data['status']
|
||||||
addon_type = form.cleaned_data['type']
|
addon_type = form_data['type']
|
||||||
|
|
||||||
base_filters = {
|
base_filters = {
|
||||||
'type': addon_type,
|
'type': addon_type,
|
||||||
|
@ -59,7 +67,7 @@ class SearchResource(AppResource):
|
||||||
region = getattr(request, 'REGION', mkt.regions.WORLDWIDE)
|
region = getattr(request, 'REGION', mkt.regions.WORLDWIDE)
|
||||||
qs = _get_query(region, gaia=request.GAIA, mobile=request.MOBILE,
|
qs = _get_query(region, gaia=request.GAIA, mobile=request.MOBILE,
|
||||||
tablet=request.TABLET, filters=base_filters)
|
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,
|
paginator = self._meta.paginator_class(request.GET, qs,
|
||||||
resource_uri=self.get_resource_list_uri(),
|
resource_uri=self.get_resource_list_uri(),
|
||||||
limit=self._meta.limit)
|
limit=self._meta.limit)
|
||||||
|
@ -71,7 +79,8 @@ class SearchResource(AppResource):
|
||||||
page['objects'] = [self.full_dehydrate(bundle) for bundle in objs]
|
page['objects'] = [self.full_dehydrate(bundle) for bundle in objs]
|
||||||
# This isn't as quite a full as a full TastyPie meta object,
|
# 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.
|
# 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):
|
def dehydrate_slug(self, bundle):
|
||||||
return bundle.obj.app_slug
|
return bundle.obj.app_slug
|
||||||
|
@ -88,3 +97,38 @@ class SearchResource(AppResource):
|
||||||
if bundle.obj.is_packaged else None)
|
if bundle.obj.is_packaged else None)
|
||||||
|
|
||||||
return bundle
|
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
|
from addons.models import Category
|
||||||
import amo
|
import amo
|
||||||
|
|
||||||
|
from mkt.api.forms import SluggableModelChoiceField
|
||||||
|
|
||||||
|
|
||||||
ADDON_CHOICES = [(v, v) for k, v in amo.MKT_ADDON_TYPES_API.items()]
|
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'))
|
label=_lazy(u'Add-on type'))
|
||||||
status = forms.ChoiceField(required=False, choices=STATUS_CHOICES,
|
status = forms.ChoiceField(required=False, choices=STATUS_CHOICES,
|
||||||
label=_lazy(u'Status'))
|
label=_lazy(u'Status'))
|
||||||
cat = forms.TypedChoiceField(required=False, coerce=int, empty_value=None,
|
cat = SluggableModelChoiceField(queryset=Category.objects.all(),
|
||||||
choices=[], label=_lazy(u'Category'))
|
sluggable_to_field_name='slug',
|
||||||
|
required=False)
|
||||||
device = forms.ChoiceField(
|
device = forms.ChoiceField(
|
||||||
required=False, choices=DEVICE_CHOICES, label=_lazy(u'Device type'))
|
required=False, choices=DEVICE_CHOICES, label=_lazy(u'Device type'))
|
||||||
premium_types = forms.MultipleChoiceField(
|
premium_types = forms.MultipleChoiceField(
|
||||||
|
@ -169,6 +172,10 @@ class ApiSearchForm(forms.Form):
|
||||||
'limit': 200,
|
'limit': 200,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def clean_cat(self):
|
||||||
|
if self.cleaned_data['cat']:
|
||||||
|
return self.cleaned_data['cat'].pk
|
||||||
|
|
||||||
def clean_type(self):
|
def clean_type(self):
|
||||||
return amo.MKT_ADDON_TYPES_API_LOOKUP.get(self.cleaned_data['type'],
|
return amo.MKT_ADDON_TYPES_API_LOOKUP.get(self.cleaned_data['type'],
|
||||||
amo.ADDON_WEBAPP)
|
amo.ADDON_WEBAPP)
|
||||||
|
|
|
@ -8,6 +8,8 @@ from nose.tools import eq_
|
||||||
import amo
|
import amo
|
||||||
from addons.models import AddonCategory, AddonDeviceType, Category
|
from addons.models import AddonCategory, AddonDeviceType, Category
|
||||||
from amo.tests import ESTestCase
|
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.models import Access, generate
|
||||||
from mkt.api.tests.test_oauth import BaseOAuth, OAuthClient
|
from mkt.api.tests.test_oauth import BaseOAuth, OAuthClient
|
||||||
from mkt.search.forms import DEVICE_CHOICES_IDS
|
from mkt.search.forms import DEVICE_CHOICES_IDS
|
||||||
|
@ -250,3 +252,38 @@ class TestApiReviewer(BaseOAuth, ESTestCase):
|
||||||
eq_(res.status_code, 400)
|
eq_(res.status_code, 400)
|
||||||
error = json.loads(res.content)['error_message']
|
error = json.loads(res.content)['error_message']
|
||||||
eq_(error.keys(), ['type'])
|
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)
|
||||||
|
|
Загрузка…
Ссылка в новой задаче