add endpoint for categories with creatured list (bug 855887)

This commit is contained in:
Allen Short 2013-04-04 11:33:33 -07:00
Родитель e48bb8bd1f
Коммит d94958b2b2
5 изменённых файлов: 119 добавлений и 11 удалений

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

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