diff --git a/mkt/api/serializers.py b/mkt/api/serializers.py index 6fc054610a0..940a12283a5 100644 --- a/mkt/api/serializers.py +++ b/mkt/api/serializers.py @@ -27,3 +27,21 @@ def deserialize(self, content, format='application/json'): return super(Serializer, self).deserialize(content, format) except JSONDecodeError, exc: raise DeserializationError(original=exc) + + +class SuggestionsSerializer(Serializer): + formats = ['suggestions+json', 'json'] + content_types = { + 'suggestions+json': 'application/x-suggestions+json', + 'json': 'application/json', + } + + def serialize(self, bundle, format='application/json', options=None): + if options is None: + options = {} + if format == 'application/x-suggestions+json': + # Format application/x-suggestions+json just like regular json. + format = 'application/json' + return super(SuggestionsSerializer, self).serialize(bundle, + format=format, + options=options) diff --git a/mkt/api/urls.py b/mkt/api/urls.py index 89bde50d692..df920416c6e 100644 --- a/mkt/api/urls.py +++ b/mkt/api/urls.py @@ -11,7 +11,7 @@ from mkt.features.views import AppFeaturesList from mkt.stats.api import GlobalStatsResource from mkt.ratings.resources import RatingResource -from mkt.search.api import SearchResource +from mkt.search.api import SearchResource, SuggestionsResource api = Api(api_name='apps') @@ -19,6 +19,7 @@ api.register(AppResource()) api.register(PreviewResource()) api.register(SearchResource()) +api.register(SuggestionsResource()) api.register(StatusResource()) api.register(RatingResource()) diff --git a/mkt/search/api.py b/mkt/search/api.py index e6058275cb1..e7de9b9582c 100644 --- a/mkt/search/api.py +++ b/mkt/search/api.py @@ -9,6 +9,7 @@ OptionalOAuthAuthentication) from mkt.api.base import CORSResource, MarketplaceResource from mkt.api.resources import AppResource +from mkt.api.serializers import SuggestionsSerializer from mkt.constants.features import FeatureProfile from mkt.search.views import _filter_search, _get_query from mkt.search.forms import ApiSearchForm @@ -149,3 +150,52 @@ def alter_list_data_to_serialize(self, request, data): # Alter the _view_name so that statsd logs seperately from search. request._view_name = 'featured' return data + + +class SuggestionsResource(SearchResource): + + class Meta(SearchResource.Meta): + authorization = ReadOnlyAuthorization() + fields = ['name', 'manifest_url'] + resource_name = 'suggest' + limit = 10 + serializer = SuggestionsSerializer(['suggestions+json']) + + def determine_format(self, request): + return 'application/x-suggestions+json' + + def get_search_data(self, request): + data = super(SuggestionsResource, self).get_search_data(request) + self.query = data.get('q', '') + return data + + def alter_list_data_to_serialize(self, request, data): + return data + + def paginate_results(self, request, qs): + return self.rehydrate_results(request, qs[:self._meta.limit]) + + def rehydrate_results(self, request, qs): + names = [] + descriptions = [] + urls = [] + icons = [] + for obj in qs: + # Tastypie expects obj.pk to be present, so set it manually. + obj.pk = obj.id + data = self.full_dehydrate(self.build_bundle(obj=obj, + request=request)) + names.append(data['name']) + descriptions.append(data['description']) + urls.append(data['absolute_url']) + icons.append(data['icon']) + return [self.query, names, descriptions, urls, icons] + + def dehydrate(self, bundle): + data = super(SuggestionsResource, self).dehydrate(bundle).data + return { + 'description': data['description'], + 'name': data['name'], + 'absolute_url': data['absolute_url'], + 'icon': data['icons'][64], + } diff --git a/mkt/search/tests/test_api.py b/mkt/search/tests/test_api.py index 63ad1632e58..7c0b9aee7cf 100644 --- a/mkt/search/tests/test_api.py +++ b/mkt/search/tests/test_api.py @@ -1,5 +1,8 @@ +# -*- coding: utf-8 -*- import json +from django.conf import settings + from mock import Mock, patch from nose.tools import eq_, ok_ @@ -7,6 +10,7 @@ import mkt.regions from addons.models import (AddonCategory, AddonDeviceType, AddonUpsell, Category) +from amo.helpers import absolutify from amo.tests import app_factory, ESTestCase from stats.models import ClientData from users.models import UserProfile @@ -540,3 +544,34 @@ def test_non_matching_profile_desktop_with_category(self): eq_(res.status_code, 200) eq_(len(res.json['featured']), 1) eq_(int(res.json['featured'][0]['id']), self.app.pk) + + +@patch.object(settings, 'SITE_URL', 'http://testserver') +class TestSuggestionsApi(ESTestCase): + fixtures = fixture('webapp_337141') + + def setUp(self): + self.url = list_url('suggest') + self.refresh('webapp') + self.client = OAuthClient(None) + + def test_suggestions(self): + app1 = Webapp.objects.get(pk=337141) + app1.save() + app2 = app_factory(name=u"Second âpp", description=u"Second dèsc", + created=self.days_ago(3)) + self.refresh('webapp') + + response = self.client.get(self.url) + parsed = json.loads(response.content) + eq_(parsed[0], '') + eq_(parsed[1], [unicode(app1.name), unicode(app2.name)]) + eq_(parsed[2], [unicode(app1.description), unicode(app2.description)]) + eq_(parsed[3], [absolutify(app1.get_detail_url()), + absolutify(app2.get_detail_url())]) + eq_(parsed[4], [app1.get_icon_url(64), app2.get_icon_url(64)]) + + # Cleanup to remove these from the index. + unindex_webapps([app1.id, app2.id]) + app1.delete() + app2.delete()