Skip to content
This repository was archived by the owner on Mar 15, 2018. It is now read-only.

Commit 61870ed

Browse files
committed
calculate regional popularity and hook it up to the adolescent flag (bug 763698)
1 parent 8376ffd commit 61870ed

File tree

8 files changed

+142
-15
lines changed

8 files changed

+142
-15
lines changed

apps/addons/search.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
import logging
22
from operator import attrgetter
33

4+
from django.db.models import Count
5+
46
import elasticutils.contrib.django as elasticutils
57
import pyes.exceptions as pyes
68

79
import amo
810
from .models import Addon
911
from bandwagon.models import Collection
1012
from compat.models import AppCompat
13+
from stats.models import ClientData
1114
from users.models import UserProfile
1215
from versions.compare import version_int
1316

17+
import mkt
18+
from mkt.webapps.models import Installed
19+
1420

1521
log = logging.getLogger('z.es')
1622

@@ -54,6 +60,24 @@ def extract(addon):
5460
d['weekly_downloads'] = addon.persona.popularity
5561
# Boost on popularity.
5662
d['_boost'] = addon.persona.popularity ** .2
63+
elif addon.type == amo.ADDON_WEBAPP:
64+
installed_ids = list(Installed.objects.filter(addon=addon)
65+
.values_list('id', flat=True))
66+
d['popularity'] = d['_boost'] = len(installed_ids)
67+
68+
# Calculate regional popularity for "mature regions"
69+
# (installs + reviews/installs from that region).
70+
installs = dict(ClientData.objects.filter(installed__in=installed_ids)
71+
.annotate(region_counts=Count('region'))
72+
.values_list('region', 'region_counts').distinct())
73+
for region in mkt.regions.ALL_REGION_IDS:
74+
cnt = installs.get(region, 0)
75+
if cnt:
76+
# Magic number (like all other scores up in this piece).
77+
d['popularity_%s' % region] = d['popularity'] + cnt * 10
78+
else:
79+
d['popularity_%s' % region] = len(installed_ids)
80+
d['_boost'] += cnt * 10
5781
else:
5882
# Boost by the number of users on a logarithmic scale. The maximum
5983
# boost (11,000,000 users for adblock) is about 5x.

mkt/browse/templates/browse/landing.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ <h2 class="sticky">{{ title if category else _('Featured') }}</h2>
3333
<div>
3434
<h2 class="see-all c">
3535
<a href="{{ request.path|urlparams(category=category.slug or None,
36-
sort='downloads') }}">
36+
sort='popularity') }}">
3737
<span>{{ _('By popularity') }}</span> <em>{{ _('See all') }}</em></a>
3838
</h2>
3939
</div>

mkt/constants/regions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,5 @@ class BR(REGION):
136136
REGIONS_DICT = dict(REGIONS_CHOICES)
137137
REGIONS_CHOICES_ID_DICT = dict(REGIONS_CHOICES_ID)
138138

139+
ALL_REGION_IDS = sorted(REGIONS_CHOICES_ID_DICT.keys())
139140
REGION_IDS = sorted(REGIONS_CHOICES_ID_DICT.keys()[1:])

mkt/search/api.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import amo
44
from amo.helpers import absolutify
5+
6+
import mkt
57
from mkt.api.base import MarketplaceResource
68
from mkt.search.views import _get_query, _filter_search
79
from mkt.search.forms import ApiSearchForm
@@ -29,8 +31,9 @@ def get_list(self, request=None, **kwargs):
2931
raise self.form_errors(form)
3032

3133
# Search specific processing of the results.
32-
qs = _get_query(request)
33-
qs = _filter_search(qs, form.cleaned_data)
34+
region = getattr(request, 'REGION', mkt.regions.WORLDWIDE)
35+
qs = _get_query(region)
36+
qs = _filter_search(qs, form.cleaned_data, region=region)
3437
res = amo.utils.paginate(request, qs)
3538

3639
# Rehydrate the results as per tastypie.

mkt/search/forms.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
SORT_CHOICES = [
1111
(None, _lazy(u'Relevance')),
12+
('popularity', _lazy(u'Popularity')),
1213
('downloads', _lazy(u'Weekly Downloads')),
1314
('rating', _lazy(u'Top Rated')),
1415
('price', _lazy(u'Price')),

mkt/search/tests/test_views.py

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from django.conf import settings
44

5+
import mock
56
from nose import SkipTest
67
from nose.tools import eq_, nottest
78
from pyquery import PyQuery as pq
@@ -13,11 +14,13 @@
1314
from amo.urlresolvers import reverse
1415
from amo.utils import urlparams
1516
from search.tests.test_views import TestAjaxSearch
17+
from stats.models import ClientData
18+
from users.models import UserProfile
1619

1720
import mkt
1821
from mkt.search.forms import DEVICE_CHOICES_IDS
1922
from mkt.webapps.tests.test_views import PaidAppMixin
20-
from mkt.webapps.models import AddonExcludedRegion as AER, Webapp
23+
from mkt.webapps.models import AddonExcludedRegion as AER, Installed, Webapp
2124

2225

2326
class SearchBase(amo.tests.ESTestCase):
@@ -50,6 +53,7 @@ def check_results(self, params, expected):
5053
got = self.get_results(r)
5154
eq_(got, expected,
5255
'Got: %s. Expected: %s. Parameters: %s' % (got, expected, params))
56+
return r
5357

5458

5559
class TestWebappSearch(PaidAppMixin, SearchBase):
@@ -239,7 +243,7 @@ def test_device_tablet(self):
239243

240244
def test_results_sort_default(self):
241245
self._generate(3)
242-
self.check_sort_links(None, 'Relevance', 'weekly_downloads')
246+
self.check_sort_links(None, 'Relevance', 'popularity')
243247

244248
def test_results_sort_unknown(self):
245249
self._generate(3)
@@ -267,7 +271,7 @@ def test_price_sort_visible_for_paid_search(self):
267271
def test_price_sort_visible_for_paid_browse(self):
268272
# 'Sort by Price' option should be removed if filtering by free apps.
269273
r = self.client.get(reverse('browse.apps'),
270-
{'price': 'free', 'sort': 'downloads'})
274+
{'price': 'free', 'sort': 'popularity'})
271275
eq_(r.status_code, 200)
272276
assert 'price' not in dict(r.context['sort_opts']), (
273277
'Unexpected price sort')
@@ -289,7 +293,7 @@ def test_redirect_free_price_sort(self):
289293
# `sort=price` should be changed to `sort=downloads` if
290294
# `price=free` is in querystring.
291295
r = self.client.get(url, {'price': 'free', 'sort': 'price'})
292-
self.assert3xx(r, urlparams(url, price='free', sort='downloads'),
296+
self.assert3xx(r, urlparams(url, price='free', sort='popularity'),
293297
302)
294298

295299
def test_region_exclusions(self):
@@ -300,6 +304,81 @@ def test_region_exclusions(self):
300304
self.check_results({'q': 'Steam', 'region': region},
301305
[] if region == 'br' else [self.webapp.id])
302306

307+
@mock.patch.object(mkt.regions.BR, 'adolescent', True)
308+
def test_adolescent_popularity(self):
309+
self.skip_if_disabled(settings.REGION_STORES)
310+
311+
# Adolescent regions use global popularity.
312+
313+
# Webapp: Global: 0, Regional: 0
314+
# Unknown1: Global: 1, Regional: 1 + 10 * 1 = 11
315+
# Unknown2: Global: 2, Regional: 0
316+
317+
user = UserProfile.objects.all()[0]
318+
cd = ClientData.objects.create(region=mkt.regions.BR.id)
319+
320+
unknown1 = amo.tests.app_factory()
321+
Installed.objects.create(addon=unknown1, user=user, client_data=cd)
322+
323+
unknown2 = amo.tests.app_factory()
324+
Installed.objects.create(addon=unknown2, user=user)
325+
Installed.objects.create(addon=unknown2, user=user)
326+
327+
self.reindex(Webapp)
328+
329+
r = self.check_results({'sort': 'popularity',
330+
'region': mkt.regions.BR.slug},
331+
[unknown2.id, unknown1.id, self.webapp.id])
332+
333+
# Check the actual popularity scores.
334+
by_popularity = list(r.context['pager'].object_list
335+
.values_dict('popularity'))
336+
eq_(by_popularity,
337+
[{'id': unknown2.id, 'popularity': 2},
338+
{'id': unknown1.id, 'popularity': 1},
339+
{'id': self.webapp.id, 'popularity': 0}])
340+
341+
@mock.patch.object(mkt.regions.BR, 'adolescent', False)
342+
def test_mature_popularity(self):
343+
self.skip_if_disabled(settings.REGION_STORES)
344+
345+
# Mature regions use regional popularity.
346+
347+
# Webapp: Global: 1, Regional: 1 * 2 + 10 * 1 = 12
348+
# Unknown1: Global: 1, Regional: 1 * 2 + 10 * 2 = 22
349+
# Unknown2: Global: 2, Regional: 2
350+
351+
region = mkt.regions.BR
352+
353+
user = UserProfile.objects.all()[0]
354+
cd = ClientData.objects.create(region=region.id)
355+
356+
Installed.objects.get_or_create(addon=self.webapp, user=user)
357+
Installed.objects.create(addon=self.webapp, user=user, client_data=cd)
358+
359+
unknown1 = amo.tests.app_factory()
360+
Installed.objects.create(addon=unknown1, user=user, client_data=cd)
361+
Installed.objects.create(addon=unknown1,
362+
user=UserProfile.objects.create(), client_data=cd)
363+
364+
unknown2 = amo.tests.app_factory()
365+
Installed.objects.create(addon=unknown2, user=user)
366+
Installed.objects.create(addon=unknown2, user=user)
367+
368+
self.reindex(Webapp)
369+
370+
r = self.check_results({'sort': 'popularity',
371+
'region': region.slug},
372+
[unknown1.id, self.webapp.id, unknown2.id])
373+
374+
# Check the actual popularity scores.
375+
by_popularity = list(r.context['pager'].object_list
376+
.values_dict('popularity_%s' % region.id))
377+
eq_(by_popularity,
378+
[{'id': unknown1.id, 'popularity_7': 22},
379+
{'id': self.webapp.id, 'popularity_7': 12},
380+
{'id': unknown2.id, 'popularity_7': 2}])
381+
303382

304383
class TestSuggestions(TestAjaxSearch):
305384

mkt/search/views.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ def __init__(self, text, urlparams, selected=False, children=None):
2929

3030
DEFAULT_FILTERS = ['cat', 'price', 'device', 'sort']
3131
DEFAULT_SORTING = {
32+
'popularity': '-popularity',
33+
# TODO: Should popularity replace downloads?
3234
'downloads': '-weekly_downloads',
3335
'rating': '-bayesian_rating',
3436
'created': '-created',
@@ -39,7 +41,7 @@ def __init__(self, text, urlparams, selected=False, children=None):
3941

4042

4143
def _filter_search(qs, query, filters=None, sorting=None,
42-
sorting_default='-weekly_downloads'):
44+
sorting_default='-popularity', region=None):
4345
"""Filter an ES queryset based on a list of filters."""
4446
# Intersection of the form fields present and the filters we want to apply.
4547
filters = filters or DEFAULT_FILTERS
@@ -58,8 +60,24 @@ def _filter_search(qs, query, filters=None, sorting=None,
5860
if 'device' in show:
5961
qs = qs.filter(device=forms.DEVICE_CHOICES_IDS[query['device']])
6062
if 'sort' in show:
61-
qs = qs.order_by(sorting[query['sort']])
63+
sort_by = sorting[query['sort']]
64+
65+
# For "Adolescent" regions popularity is global installs + reviews.
66+
67+
if query['sort'] == 'popularity' and region and not region.adolescent:
68+
# For "Mature" regions popularity becomes installs + reviews
69+
# from only that region.
70+
sort_by = '-popularity_%s' % region.id
71+
72+
qs = qs.order_by(sort_by)
6273
elif not query.get('q'):
74+
75+
if (sorting_default == 'popularity' and region and
76+
not region.adolescent):
77+
# For "Mature" regions popularity becomes installs + reviews
78+
# from only that region.
79+
sorting_default = '-popularity_%s' % region.id
80+
6381
# Sort by a default if there was no query so results are predictable.
6482
qs = qs.order_by(sorting_default)
6583

@@ -103,8 +121,7 @@ def sort_sidebar(query, form):
103121
for key, text in form.fields['sort'].choices]
104122

105123

106-
def _get_query(request):
107-
region = getattr(request, 'REGION', mkt.regions.WORLDWIDE)
124+
def _get_query(region):
108125
return Webapp.from_search(region=region).facet('category')
109126

110127

@@ -116,16 +133,18 @@ def _app_search(request, category=None, browse=None):
116133
# Remove `sort=price` if `price=free`.
117134
if query.get('price') == 'free' and query.get('sort') == 'price':
118135
return {'redirect': amo.utils.urlparams(request.get_full_path(),
119-
sort='downloads',
136+
sort='popularity',
120137
price='free')}
121138

122-
qs = _get_query(request)
139+
region = getattr(request, 'REGION', mkt.regions.WORLDWIDE)
140+
141+
qs = _get_query(region)
123142
# On mobile, always only show mobile apps. Bug 767620
124143
if request.MOBILE:
125144
qs = qs.filter(uses_flash=False)
126145
query['device'] = 'mobile'
127146

128-
qs = _filter_search(qs, query)
147+
qs = _filter_search(qs, query, region=region)
129148

130149
# If we're mobile, leave no witnesses. (i.e.: hide "Applied Filters:
131150
# Mobile")

mkt/webapps/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ def from_search(cls, cat=None, region=None):
428428
@classmethod
429429
def popular(cls, cat=None, region=None):
430430
"""Elastically grab the most popular apps."""
431-
return cls.from_search(cat, region).order_by('-weekly_downloads')
431+
return cls.from_search(cat, region).order_by('-popularity')
432432

433433
@classmethod
434434
def latest(cls, cat=None, region=None):

0 commit comments

Comments
 (0)