add in api for searching (bug 755801)
This commit is contained in:
Родитель
7c3d9685ac
Коммит
cf72feb0a8
|
@ -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)),
|
||||
|
|
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче