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 This API requires authentication and a successfully validated manifest. To
create an app with your validated manifest:: 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 Body data should contain the manifest id from the validate call and other data
in JSON:: in JSON::
@ -174,7 +174,7 @@ Update
This API requires authentication and a successfully created app:: 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. 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:: 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:: Returns the status of the app::
@ -231,7 +231,7 @@ This API requires authentication and a successfully created app.
Deletes an 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 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 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:: 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:: Returns the list of categories::
@ -310,6 +310,37 @@ Returns the list of categories::
Use the `id` of the category in your app updating. 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 .. _`MDN`: https://developer.mozilla.org
.. _`marketplace team`: marketplace-team@mozilla.org .. _`marketplace team`: marketplace-team@mozilla.org
.. _`django-tastypie`: https://github.com/toastdriven/django-tastypie .. _`django-tastypie`: https://github.com/toastdriven/django-tastypie

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

@ -33,6 +33,7 @@ from nose.tools import eq_
from piston.models import Consumer from piston.models import Consumer
from amo.tests import TestCase from amo.tests import TestCase
from amo.helpers import urlparams
from amo.urlresolvers import reverse from amo.urlresolvers import reverse
from files.models import FileUpload from files.models import FileUpload
@ -51,7 +52,10 @@ def get_absolute_url(url):
# TODO (andym): make this more standard. # TODO (andym): make this more standard.
url[1]['api_name'] = 'apps' url[1]['api_name'] = 'apps'
rev = reverse(url[0], kwargs=url[1]) 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): def data_keys(d):
@ -148,7 +152,8 @@ class BaseOAuth(TestCase):
if verb in allowed: if verb in allowed:
continue continue
res = getattr(self.client, verb)(url) 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/') @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 tastypie.api import Api
from mkt.api.resources import AppResource, CategoryResource, ValidationResource from mkt.api.resources import AppResource, CategoryResource, ValidationResource
from mkt.search.api import SearchResource
api = Api(api_name='apps') api = Api(api_name='apps')
api.register(ValidationResource()) api.register(ValidationResource())
api.register(AppResource()) api.register(AppResource())
api.register(CategoryResource()) api.register(CategoryResource())
api.register(SearchResource())
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^', include(api.urls)), 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 tower import ugettext_lazy as _lazy
from addons.models import Category
import amo
SORT_CHOICES = [ SORT_CHOICES = [
(None, _lazy(u'Relevance')), (None, _lazy(u'Relevance')),
@ -89,3 +92,17 @@ class AppSearchForm(forms.Form):
class AppListForm(AppSearchForm): class AppListForm(AppSearchForm):
sort = forms.ChoiceField(required=False, choices=LISTING_SORT_CHOICES) 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 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'): sorting_default='-weekly_downloads'):
"""Filter an ES queryset based on a list of filters.""" """Filter an ES queryset based on a list of filters."""
# Intersection of the form fields present and the filters we want to apply. # 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)] show = [f for f in filters if query.get(f)]
if query.get('q'): if query.get('q'):
@ -87,6 +100,13 @@ def sort_sidebar(query, form):
for key, text in form.fields['sort'].choices] 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): def _app_search(request, category=None, browse=None):
form = forms.AppSearchForm(request.GET) form = forms.AppSearchForm(request.GET)
form.is_valid() # Let the form try to clean data. 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(), return {'redirect': amo.utils.urlparams(request.get_full_path(),
sort=None)} sort=None)}
qs = (Webapp.search() qs = _get_query(request, form, query)
.filter(type=amo.ADDON_WEBAPP, status=amo.STATUS_PUBLIC, qs = _filter_search(qs, query)
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)
pager = amo.utils.paginate(request, qs) pager = amo.utils.paginate(request, qs)
facets = pager.object_list.facets facets = pager.object_list.facets