def recommendations(request, version, platform, limit=9): """ Figure out recommended add-ons for an anonymous user based on POSTed guids. POST body looks like {"guids": [...]} with an optional "token" key if they've been here before. """ try: POST = json.loads(request.raw_post_data) guids = POST['guids'] except (ValueError, TypeError, KeyError): # Errors: invalid json, didn't get a dict, didn't find "guids". return http.HttpResponseBadRequest() addon_ids = get_addon_ids(guids) index = Collection.make_index(addon_ids) ids, recs = Collection.get_recs_from_ids(addon_ids, request.APP, version) recs = _recommendations(request, version, platform, limit, index, ids, recs) # We're only storing a percentage of the collections we see because the db # can't keep up with 100%. if not waffle.sample_is_active('disco-pane-store-collections'): return recs # Users have a token2 if they've been here before. The token matches # addon_index in their SyncedCollection. if 'token2' in POST: token = POST['token2'] if token == index: # We've seen them before and their add-ons have not changed. return recs elif token != index: # We've seen them before and their add-ons changed. Remove the # reference to their old synced collection. (SyncedCollection.objects.filter(addon_index=index) .update(count=F('count') - 1)) # Try to create the SyncedCollection. There's a unique constraint on # addon_index so it will fail if this addon_index already exists. If we # checked for existence first and then created a collection there would # be a race condition between multiple users with the same addon_index. try: c = SyncedCollection.objects.create(addon_index=index, count=1) c.set_addons(addon_ids) except IntegrityError: try: (SyncedCollection.objects.filter(addon_index=index) .update(count=F('count') + 1)) except Exception, e: log.error(u'Could not count++ "%s" (%s).' % (index, e))
def test_addon_index(self): c = Collection.objects.get(pk=512) c.author = self.user eq_(c.addon_index, None) ids = c.addons.values_list('id', flat=True) c.save() eq_(c.addon_index, Collection.make_index(ids))
def test_addon_index(self): c = Collection.objects.get(pk=512) c.author = self.user eq_(c.addon_index, None) ids = c.addons.values_list("id", flat=True) c.save() eq_(c.addon_index, Collection.make_index(ids))
def test_remove_other_collection(self): "403 when you try to add to a collection that isn't yours." c = Collection(author=self.other) c.save() r = self.client.post(reverse('collections.ajax_remove'), {'addon_id': 3615, 'id': c.id}, follow=True) eq_(r.status_code, 403)
def test_form_uneditable_slug(self): """ Editing a mobile or favorite collection should have an uneditable slug. """ u = UserProfile.objects.get(username='******') Collection(author=u, slug='mobile', type=amo.COLLECTION_MOBILE).save() url = reverse('collections.edit', args=['admin', 'mobile']) r = self.client.get(url, follow=True) doc = pq(r.content) eq_(len(doc('#id_slug')), 0)
def test_user_collection_list(self): c1 = Collection(uuid='eb4e3cd8-5cf1-4832-86fb-a90fc6d3765c') c2 = Collection(uuid='61780943-e159-4206-8acd-0ae9f63f294c', nickname='my_collection') heading = 'My Heading' response = unicode(user_collection_list([c1, c2], heading)) # heading eq_(pq(response)('h3').text(), heading) # both items # TODO reverse URLs assert c1.get_url_path() in response, 'Collection UUID link missing.' assert c2.get_url_path() in response, ( 'Collection nickname link missing.') # empty collection, empty response response = unicode(user_collection_list([], heading)) self.assertFalse(response, 'empty collection should not create a list')
def test_listed(self): """Make sure the manager's listed() filter works.""" listed_count = Collection.objects.listed().count() # make a private collection private = Collection( name="Hello", uuid="4e2a1acc-39ae-47ec-956f-46e080ac7f69", listed=False, author=self.user) private.save() listed = Collection.objects.listed() eq_(len(listed), listed_count)
def _collections(request): """Handle the request for collections.""" # Sorting by relevance isn't an option. Instead the default is `weekly`. initial = dict(sort="weekly") # Update with GET variables. initial.update(request.GET.items()) # Ignore appver/platform and set default number of collections per page. initial.update(appver=None, platform=None, pp=DEFAULT_NUM_COLLECTIONS) form = SecondarySearchForm(initial) form.is_valid() qs = Collection.search().filter(listed=True, app=request.APP.id) filters = ["sort"] mapping = { "weekly": "-weekly_subscribers", "monthly": "-monthly_subscribers", "all": "-subscribers", "rating": "-rating", "created": "-created", "name": "name_sort", "updated": "-modified", } results = _filter_search( request, qs, form.cleaned_data, filters, sorting=mapping, sorting_default="-weekly_subscribers", types=amo.COLLECTION_SEARCH_CHOICES, ) query = form.cleaned_data.get("q", "") search_opts = {} search_opts["limit"] = form.cleaned_data.get("pp", DEFAULT_NUM_COLLECTIONS) page = form.cleaned_data.get("page") or 1 search_opts["offset"] = (page - 1) * search_opts["limit"] search_opts["sort"] = form.cleaned_data.get("sort") pager = amo.utils.paginate(request, results, per_page=search_opts["limit"]) c = dict( pager=pager, form=form, query=query, opts=search_opts, filter=bandwagon.views.get_filter(request), search_placeholder="collections", ) return jingo.render(request, "search/collections.html", c)
def test_form_uneditable_slug_submit(self): """ Ignore the slug request change, if some jackass thinks he can change it. """ u = UserProfile.objects.get(username='******') Collection(author=u, slug='mobile', type=amo.COLLECTION_MOBILE).save() url = reverse('collections.edit', args=['admin', 'mobile']) self.client.post(url, {'name': 'HALP', 'slug': 'halp', 'listed': True}, follow=True) assert not Collection.objects.filter(slug='halp', author=u) assert Collection.objects.filter(slug='mobile', author=u)
def _collections(request): """Handle the request for collections.""" # Sorting by relevance isn't an option. Instead the default is `weekly`. initial = dict(sort='weekly') # Update with GET variables. initial.update(request.GET.items()) # Ignore appver/platform and set default number of collections per page. initial.update(appver=None, platform=None, pp=DEFAULT_NUM_COLLECTIONS) form = SecondarySearchForm(initial) form.is_valid() qs = Collection.search().filter(listed=True, app=request.APP.id) filters = ['sort'] mapping = { 'weekly': '-weekly_subscribers', 'monthly': '-monthly_subscribers', 'all': '-subscribers', 'rating': '-rating', 'created': '-created', 'name': 'name_sort', 'updated': '-modified' } results = _filter_search( request, qs, form.cleaned_data, filters, sorting=mapping, sorting_default='-weekly_subscribers', types=amo.COLLECTION_SEARCH_CHOICES) form_data = form.cleaned_data.get('q', '') search_opts = {} search_opts['limit'] = form.cleaned_data.get('pp', DEFAULT_NUM_COLLECTIONS) page = form.cleaned_data.get('page') or 1 search_opts['offset'] = (page - 1) * search_opts['limit'] search_opts['sort'] = form.cleaned_data.get('sort') pager = amo.utils.paginate(request, results, per_page=search_opts['limit']) c = dict( pager=pager, form=form, query=form_data, opts=search_opts, filter=bandwagon.views.get_filter(request), search_placeholder='collections') return render(request, 'search/collections.html', c)
def _collections(request): """Handle the request for collections.""" # Sorting by relevance isn't an option. Instead the default is `weekly`. initial = dict(sort='weekly') # Update with GET variables. initial.update(request.GET.items()) # Ignore appver/platform and set default number of collections per page. initial.update(appver=None, platform=None, pp=DEFAULT_NUM_COLLECTIONS) form = SecondarySearchForm(initial) form.is_valid() if waffle.switch_is_active('replace-sphinx'): qs = Collection.search().filter(listed=True, app=request.APP.id) filters = ['sort'] mapping = {'weekly': '-weekly_subscribers', 'monthly': '-monthly_subscribers', 'all': '-subscribers', 'rating': '-rating', 'created': '-created', 'name': 'name_sort', 'updated': '-modified'} results = _filter_search(request, qs, form.cleaned_data, filters, sorting=mapping, sorting_default='-weekly_subscribers', types=amo.COLLECTION_SEARCH_CHOICES) query = form.cleaned_data.get('q', '') search_opts = {} search_opts['limit'] = form.cleaned_data.get('pp', DEFAULT_NUM_COLLECTIONS) page = form.cleaned_data.get('page') or 1 search_opts['offset'] = (page - 1) * search_opts['limit'] search_opts['sort'] = form.cleaned_data.get('sort') if not waffle.switch_is_active('replace-sphinx'): # The new hotness calls this `created`. Sphinx still calls it `newest`. if search_opts['sort'] == 'created': search_opts['sort'] = 'newest' try: results = CollectionsClient().query(query, **search_opts) except SearchError: return jingo.render(request, 'search/down.html', {}, status=503) pager = amo.utils.paginate(request, results, per_page=search_opts['limit']) c = dict(pager=pager, form=form, query=query, opts=search_opts, filter=bandwagon.views.get_filter(request), search_placeholder='collections') return jingo.render(request, 'search/collections.html', c)
def get_synced_collection(addon_ids, token): """ Get a synced collection for these addons. May reuse an existing collection. The token is associated with the collection. """ index = Collection.make_index(addon_ids) try: c = (SyncedCollection.objects.no_cache().filter(addon_index=index))[0] except IndexError: c = SyncedCollection.objects.create(listed=False) c.set_addons(addon_ids) c.token_set.create(token=token) return c
def _collections(request): """Handle the request for collections.""" # Sorting by relevance isn't an option. Instead the default is `weekly`. initial = dict(sort='weekly') # Update with GET variables. initial.update(request.GET.items()) # Ignore appver/platform and set default number of collections per page. initial.update(appver=None, platform=None, pp=DEFAULT_NUM_COLLECTIONS) form = SecondarySearchForm(initial) form.is_valid() qs = Collection.search().filter(listed=True, app=request.APP.id) filters = ['sort'] mapping = { 'weekly': '-weekly_subscribers', 'monthly': '-monthly_subscribers', 'all': '-subscribers', 'rating': '-rating', 'created': '-created', 'name': 'name_sort', 'updated': '-modified' } results = _filter_search(request, qs, form.cleaned_data, filters, sorting=mapping, sorting_default='-weekly_subscribers', types=amo.COLLECTION_SEARCH_CHOICES) query = form.cleaned_data.get('q', '') search_opts = {} search_opts['limit'] = form.cleaned_data.get('pp', DEFAULT_NUM_COLLECTIONS) page = form.cleaned_data.get('page') or 1 search_opts['offset'] = (page - 1) * search_opts['limit'] search_opts['sort'] = form.cleaned_data.get('sort') pager = amo.utils.paginate(request, results, per_page=search_opts['limit']) c = dict(pager=pager, form=form, query=query, opts=search_opts, filter=bandwagon.views.get_filter(request), search_placeholder='collections') return jingo.render(request, 'search/collections.html', c)
def es_collections_json(request): app = request.GET.get("app", "") q = request.GET.get("q", "") qs = Collection.search() try: qs = qs.query(id__startswith=int(q)) except ValueError: qs = qs.query(name__text=q) try: qs = qs.filter(app=int(app)) except ValueError: pass data = [] for c in qs[:7]: data.append({"id": c.id, "name": unicode(c.name), "all_personas": c.all_personas, "url": c.get_url_path()}) return data
def test_icon_url(self): # Has no icon. c = Collection(pk=512, modified=datetime.datetime.now()) assert c.icon_url.endswith('img/icons/collection.png') c.icontype = 'image/png' url = c.icon_url.split('?')[0] assert url.endswith('0/512.png') c.id = 12341234 url = c.icon_url.split('?')[0] assert url.endswith('12341/12341234.png') c.icontype = None c.type = amo.COLLECTION_FAVORITES assert c.icon_url.endswith('img/icons/heart.png')
def es_collections_json(request): app = request.GET.get('app', '') q = request.GET.get('q', '') qs = Collection.search() try: qs = qs.query(id__startswith=int(q)) except ValueError: qs = qs.query(name__text=q) try: qs = qs.filter(app=int(app)) except ValueError: pass data = [] for c in qs[:7]: data.append({'id': c.id, 'name': unicode(c.name), 'all_personas': c.all_personas, 'url': c.get_url_path()}) return data
def collection_factory(**kw): data = { 'type': amo.COLLECTION_NORMAL, 'application': amo.FIREFOX.id, 'name': 'Collection %s' % abs(hash(datetime.now())), 'addon_count': random.randint(200, 2000), 'subscribers': random.randint(1000, 5000), 'monthly_subscribers': random.randint(100, 500), 'weekly_subscribers': random.randint(10, 50), 'upvotes': random.randint(100, 500), 'downvotes': random.randint(100, 500), 'listed': True, } data.update(kw) c = Collection(**data) c.slug = data['name'].replace(' ', '-').lower() c.rating = (c.upvotes - c.downvotes) * math.log(c.upvotes + c.downvotes) c.created = c.modified = datetime(2011, 11, 11, random.randint(0, 23), random.randint(0, 59)) c.save() return c
def create_collection(application): """Create a Collection for the given `application`.""" data = { 'type': amo.COLLECTION_NORMAL, 'application': application, 'name': 'Collection %s' % abs(hash(datetime.now())), 'addon_count': random.randint(200, 2000), 'subscribers': random.randint(1000, 5000), 'monthly_subscribers': random.randint(100, 500), 'weekly_subscribers': random.randint(10, 50), 'upvotes': random.randint(100, 500), 'downvotes': random.randint(100, 500), 'listed': True, } c = Collection(**data) c.slug = slugify(data['name']) c.rating = (c.upvotes - c.downvotes) * math.log(c.upvotes + c.downvotes) c.created = c.modified = datetime(2014, 10, 27, random.randint(0, 23), random.randint(0, 59)) c.save() return c
def recommendations(request, limit=5): """ Figure out recommended add-ons for an anonymous user based on POSTed guids. POST body looks like {"guids": [...]} with an optional "token" key if they've been here before. """ if request.method != 'POST': return http.HttpResponseNotAllowed(['POST']) try: POST = json.loads(request.raw_post_data) guids = POST['guids'] except (ValueError, TypeError, KeyError): # Errors: invalid json, didn't get a dict, didn't find "guids". return http.HttpResponseBadRequest() addon_ids = get_addon_ids(guids) token = POST['token'] if 'token' in POST else get_random_token() if 'token' in POST: q = SyncedCollection.objects.filter(token_set__token=token) if q: # We've seen this user before. synced = q[0] if synced.addon_index == Collection.make_index(addon_ids): # Their add-ons didn't change, get out quick. recs = synced.get_recommendations() return _recommendations(request, limit, token, recs) else: # Remove the link to the current sync, make a new one below. synced.token_set.get(token=token).delete() synced = get_synced_collection(addon_ids, token) recs = synced.get_recommendations() return _recommendations(request, limit, token, recs)
def test_barometer(self): self.client.get('/') jingo.load_helpers() collection = Collection(upvotes=1, slug='mccrackin', author=UserProfile(username='******')) # Mock logged out. c = { 'request': Mock(path='yermom', GET=Mock(urlencode=lambda: '')), 'user': Mock(), 'settings': Mock() } c['request'].user.is_authenticated.return_value = False doc = pq(barometer(c, collection)) eq_(doc('form')[0].action, '/en-US/firefox/users/login?to=yermom') # Mock logged in. c['request'].amo_user.votes.filter.return_value = [Mock(vote=1)] c['request'].user.is_authenticated.return_value = True barometer(c, collection) doc = pq(barometer(c, collection)) eq_( doc('form')[0].action, reverse('collections.vote', args=['clouserw', 'mccrackin', 'up']))
def test_addon_index(self): c = Collection.objects.get(pk=5) eq_(c.addon_index, None) ids = c.addons.values_list('id', flat=True) c.save() eq_(c.addon_index, Collection.make_index(ids))
def test_publishable_by(self): c = Collection(pk=512, author=self.other) CollectionUser(collection=c, user=self.user).save() eq_(c.publishable_by(self.user), True)
POST body looks like {"guids": [...]} with an optional "token" key if they've been here before. """ if not compat_mode: compat_mode = get_compat_mode(version) try: POST = json.loads(request.raw_post_data) guids = POST['guids'] except (ValueError, TypeError, KeyError), e: # Errors: invalid json, didn't get a dict, didn't find "guids". log.debug('Recommendations return 405 because: %s' % e) return http.HttpResponseBadRequest() addon_ids = get_addon_ids(guids) index = Collection.make_index(addon_ids) ids, recs = Collection.get_recs_from_ids(addon_ids, request.APP, version, compat_mode) recs = _recommendations(request, version, platform, limit, index, ids, recs, compat_mode) # We're only storing a percentage of the collections we see because the db # can't keep up with 100%. if not waffle.sample_is_active('disco-pane-store-collections'): return recs # Users have a token2 if they've been here before. The token matches # addon_index in their SyncedCollection. if 'token2' in POST: token = POST['token2']
def test_is_subscribed(self): c = Collection(pk=512) c.following.create(user=self.user) assert c.is_subscribed(self.user)