add in api for searching (bug 755801)

This commit is contained in:
Andy McKay 2012-05-24 15:32:53 +01:00
Родитель 7c3d9685ac
Коммит cf72feb0a8
7 изменённых файлов: 215 добавлений и 22 удалений

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

@ -137,7 +137,7 @@ Create
This API requires authentication and a successfully validated manifest. To
create an app with your validated manifest::
POST /en-US/api/apps/
POST /en-US/api/apps/app/
Body data should contain the manifest id from the validate call and other data
in JSON::
@ -174,7 +174,7 @@ Update
This API requires authentication and a successfully created app::
PUT /en-US/apps/app/<app id>/
PUT /en-US/api/apps/app/<app id>/
The body contains JSON for the data to be posted.
@ -216,7 +216,7 @@ This API requires authentication and a successfully created app.
To view details of an app, including its review status::
GET /api/apps/<slug>
GET /en-US/api/apps/app/<app id>/
Returns the status of the app::
@ -231,7 +231,7 @@ This API requires authentication and a successfully created app.
Deletes an app::
DELETE /api/apps/<slug>
DELETE /en-US/api/apps/app/<app id>/
The app will only be hard deleted if it is incomplete. Otherwise it will be
soft deleted. A soft deleted app will not appear publicly in any listings
@ -296,7 +296,7 @@ No authentication required.
To find a list of categories available on the marketplace::
GET /en-US/api/categories/
GET /en-US/api/apps/categories/
Returns the list of categories::
@ -310,6 +310,37 @@ Returns the list of categories::
Use the `id` of the category in your app updating.
Search
======
No authentication required.
To find a list of apps in a category on the marketplace::
GET /en-US/api/apps/search/
Returns a list of the apps sorted by relevance::
{"meta": {},
"objects":
[{"absolute_url": "http://../en-US/app/marble-run-1/",
"premium_type": 3, "slug": "marble-run-1", id="26",
"icon_url": "http://../addon_icons/0/26-32.png",
"resource_uri": null
}
...
Arguments:
* `cat` (optional): use the category API to find the ids of the categories
* `sort` (optional): one of 'downloads', 'rating', 'price', 'created'
Example, to specify a category sorted by rating::
GET /en-US/api/apps/search/?cat=1&sort=rating
Sorting options:
.. _`MDN`: https://developer.mozilla.org
.. _`marketplace team`: marketplace-team@mozilla.org
.. _`django-tastypie`: https://github.com/toastdriven/django-tastypie

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

@ -33,6 +33,7 @@ from nose.tools import eq_
from piston.models import Consumer
from amo.tests import TestCase
from amo.helpers import urlparams
from amo.urlresolvers import reverse
from files.models import FileUpload
@ -51,7 +52,10 @@ def get_absolute_url(url):
# TODO (andym): make this more standard.
url[1]['api_name'] = 'apps'
rev = reverse(url[0], kwargs=url[1])
return 'http://%s%s' % ('api', rev)
res = 'http://%s%s' % ('api', rev)
if len(url) > 2:
res = urlparams(res, **url[2])
return res
def data_keys(d):
@ -148,7 +152,8 @@ class BaseOAuth(TestCase):
if verb in allowed:
continue
res = getattr(self.client, verb)(url)
eq_(res.status_code, 405)
assert (res.status_code in (401, 405),
'%s: %s not 405' % (verb.upper(), res.status_code))
@patch.object(settings, 'SITE_URL', 'http://api/')

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

@ -2,12 +2,13 @@ from django.conf.urls.defaults import include, patterns, url
from tastypie.api import Api
from mkt.api.resources import AppResource, CategoryResource, ValidationResource
from mkt.search.api import SearchResource
api = Api(api_name='apps')
api.register(ValidationResource())
api.register(AppResource())
api.register(CategoryResource())
api.register(SearchResource())
urlpatterns = patterns('',
url(r'^', include(api.urls)),

52
mkt/search/api.py Normal file
Просмотреть файл

@ -0,0 +1,52 @@
from tastypie.serializers import Serializer
from django.forms import ValidationError
import amo
from amo.helpers import absolutify
from mkt.api.base import MarketplaceResource
from mkt.search.views import _get_query, _filter_search
from mkt.search.forms import ApiSearchForm
from mkt.webapps.models import Webapp
class SearchResource(MarketplaceResource):
class Meta:
available_methods = []
list_available_methods = ['get', 'post']
fields = ['id', 'name', 'description', 'premium_type', 'slug',
'summary']
object_class = Webapp
resource_name = 'search'
serializer = Serializer(formats=['json'])
def get_resource_uri(self, bundle):
# At this time we don't have an API to the Webapp details.
return None
def get_list(self, request=None, **kwargs):
form = ApiSearchForm(request.GET)
if not form.is_valid():
raise ValidationError(self.form_errors(form))
# Search specific processing of the results.
qs = _get_query(request, form, form.cleaned_data)
qs = _filter_search(qs, form.cleaned_data)
res = amo.utils.paginate(request, qs)
# Rehydrate the results as per tastypie.
bundles = [self.build_bundle(obj=obj, request=request)
for obj in res.object_list]
objs = [self.full_dehydrate(bundle) for bundle in bundles]
# 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, {'objects': objs, 'meta': {}})
def dehydrate_slug(self, bundle):
return bundle.obj.app_slug
def dehydrate(self, bundle):
bundle.data['icon_url'] = bundle.obj.get_icon_url(32)
bundle.data['absolute_url'] = absolutify(bundle.obj.get_detail_url())
return bundle

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

@ -3,6 +3,9 @@ from django.forms.util import ErrorDict
from tower import ugettext_lazy as _lazy
from addons.models import Category
import amo
SORT_CHOICES = [
(None, _lazy(u'Relevance')),
@ -89,3 +92,17 @@ class AppSearchForm(forms.Form):
class AppListForm(AppSearchForm):
sort = forms.ChoiceField(required=False, choices=LISTING_SORT_CHOICES)
class ApiSearchForm(forms.Form):
# Like App search form, but just filtering on categories for now
# and bit more strict about the filtering.
sort = forms.ChoiceField(required=False, choices=LISTING_SORT_CHOICES)
cat = forms.TypedChoiceField(required=False, coerce=int, empty_value=None,
choices=[])
def __init__(self, *args, **kw):
super(ApiSearchForm, self).__init__(*args, **kw)
CATS = (Category.objects.filter(type=amo.ADDON_WEBAPP)
.values_list('id', flat=True))
self.fields['cat'].choices = [(pk, pk) for pk in CATS]

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

@ -0,0 +1,78 @@
import json
import mock
from nose.tools import eq_
from addons.models import AddonCategory, Category
import amo
from amo.tests import ESTestCase
from mkt.api.tests.test_oauth import BaseOAuth, OAuthClient
from mkt.webapps.models import Webapp
class TestApi(BaseOAuth, ESTestCase):
fixtures = fixtures = ['webapps/337141-steamcube']
def setUp(self):
self.client = OAuthClient(None)
self.list_url = ('api_dispatch_list', {'resource_name': 'search'})
self.webapp = Webapp.objects.get(pk=337141)
self.webapp.update(status=amo.STATUS_PUBLIC)
self.webapp.save()
self.category = Category.objects.create(name='test',
type=amo.ADDON_WEBAPP)
self.webapp.save()
self.refresh()
def test_verbs(self):
self._allowed_verbs(self.list_url, ['get'])
def test_meta(self):
res = self.client.get(self.list_url)
eq_(res.status_code, 200)
eq_(set(json.loads(res.content).keys()), set(['objects', 'meta']))
def test_wrong_category(self):
res = self.client.get(self.list_url + ({'cat': self.category.pk + 1},))
eq_(res.status_code, 400)
def test_wrong_sort(self):
res = self.client.get(self.list_url + ({'sort': 'awesomeness'},))
eq_(res.status_code, 400)
def test_right_category(self):
res = self.client.get(self.list_url + ({'cat': self.category.pk},))
eq_(res.status_code, 200)
eq_(json.loads(res.content)['objects'], [])
def create(self):
AddonCategory.objects.create(addon=self.webapp, category=self.category)
self.webapp.save()
self.refresh()
def test_right_category_present(self):
self.create()
res = self.client.get(self.list_url + ({'cat': self.category.pk},))
eq_(res.status_code, 200)
objs = json.loads(res.content)['objects']
eq_(len(objs), 1)
def test_dehdryate(self):
self.create()
res = self.client.get(self.list_url + ({'cat': self.category.pk},))
eq_(res.status_code, 200)
obj = json.loads(res.content)['objects'][0]
eq_(obj['slug'], self.webapp.app_slug)
eq_(obj['icon_url'], self.webapp.get_icon_url(32))
eq_(obj['absolute_url'], self.webapp.get_absolute_url())
eq_(obj['resource_uri'], None)
@mock.patch('mkt.search.api._filter_search')
def test_others_ignored(self, _filter_search):
_filter_search.return_value = []
res = self.client.get(self.list_url +
({'q': 'foo', 'sort': 'rating'},))
eq_(res.status_code, 200)
args = _filter_search.call_args[0][1]
assert 'sort' in args
assert 'q' not in args

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

@ -24,10 +24,23 @@ class FacetLink(object):
self.null_urlparams['page'] = None
def _filter_search(qs, query, filters, sorting,
DEFAULT_FILTERS = ['cat', 'price', 'device', 'sort']
DEFAULT_SORTING = {
'downloads': '-weekly_downloads',
'rating': '-bayesian_rating',
'created': '-created',
'name': 'name_sort',
'hotness': '-hotness',
'price': 'price'
}
def _filter_search(qs, query, filters=None, sorting=None,
sorting_default='-weekly_downloads'):
"""Filter an ES queryset based on a list of filters."""
# Intersection of the form fields present and the filters we want to apply.
filters = filters or DEFAULT_FILTERS
sorting = sorting or DEFAULT_SORTING
show = [f for f in filters if query.get(f)]
if query.get('q'):
@ -87,6 +100,13 @@ def sort_sidebar(query, form):
for key, text in form.fields['sort'].choices]
def _get_query(request, form, query):
return (Webapp.search()
.filter(type=amo.ADDON_WEBAPP, status=amo.STATUS_PUBLIC,
is_disabled=False)
.facet(categories={'terms': {'field': 'category', 'size': 200}}))
def _app_search(request, category=None, browse=None):
form = forms.AppSearchForm(request.GET)
form.is_valid() # Let the form try to clean data.
@ -97,19 +117,8 @@ def _app_search(request, category=None, browse=None):
return {'redirect': amo.utils.urlparams(request.get_full_path(),
sort=None)}
qs = (Webapp.search()
.filter(type=amo.ADDON_WEBAPP, status=amo.STATUS_PUBLIC,
is_disabled=False)
.facet(categories={'terms': {'field': 'category', 'size': 200}}))
filters = ['cat', 'price', 'device', 'sort']
sorting = {'downloads': '-weekly_downloads',
'rating': '-bayesian_rating',
'created': '-created',
'name': 'name_sort',
'hotness': '-hotness',
'price': 'price'}
qs = _filter_search(qs, query, filters, sorting)
qs = _get_query(request, form, query)
qs = _filter_search(qs, query)
pager = amo.utils.paginate(request, qs)
facets = pager.object_list.facets