Merge lp:~michael.nelson/ubuntu-webcatalog/978000-recommended-apps into lp:ubuntu-webcatalog
- 978000-recommended-apps
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Anthony Lenton | ||||
Approved revision: | 109 | ||||
Merged at revision: | 110 | ||||
Proposed branch: | lp:~michael.nelson/ubuntu-webcatalog/978000-recommended-apps | ||||
Merge into: | lp:ubuntu-webcatalog | ||||
Diff against target: |
619 lines (+290/-99) 14 files modified
.bzrignore (+1/-0) fabtasks/bootstrap.py (+5/-0) src/webcatalog/schema.py (+4/-1) src/webcatalog/static/css/carousel.css (+7/-14) src/webcatalog/templates/webcatalog/app_carousel_widget.html (+43/-0) src/webcatalog/templates/webcatalog/featured_apps_widget.html (+7/-39) src/webcatalog/templates/webcatalog/recommended_apps.html (+27/-0) src/webcatalog/templates/webcatalog/recommended_apps_widget.html (+20/-0) src/webcatalog/templates/webcatalog/top_rated_apps_widget.html (+7/-43) src/webcatalog/tests/test_utilities.py (+49/-0) src/webcatalog/tests/test_views.py (+72/-2) src/webcatalog/urls.py (+2/-0) src/webcatalog/utilities.py (+16/-0) src/webcatalog/views.py (+30/-0) |
||||
To merge this branch: | bzr merge lp:~michael.nelson/ubuntu-webcatalog/978000-recommended-apps | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Anthony Lenton (community) | Approve | ||
Review via email: mp+102530@code.launchpad.net |
Commit message
Add utility for accessing cached recommendations via the recommendation api.
Description of the change
Overview
========
This branch does most of the groundwork for bug 978000 - adding a view which renders the app recommendations which handles both ajax and non-ajax requests, and sets up the api with caching for recommendations. It also DRYs up the application carousels a bit.
Note: One thing I noticed while testing this, is that we've currently got a few views which render slightly different templates depending on HTTP_X_
The following branch will add a little JS to put the widget on the page and add config options for the number of recommendations to display (0 being don't use the widget at all).
- 108. By Michael Nelson
-
GREEN: separated ajax/nonajax view responses so that they are cached separately.
- 109. By Michael Nelson
-
REFACTOR: switched to using vary_headers.
Preview Diff
1 | === modified file '.bzrignore' |
2 | --- .bzrignore 2012-03-05 17:41:06 +0000 |
3 | +++ .bzrignore 2012-04-19 21:58:17 +0000 |
4 | @@ -14,3 +14,4 @@ |
5 | django_project/rnrclient.py |
6 | django_project/adminaudit |
7 | django_project/local.cfg |
8 | +django_project/sreclient.py |
9 | |
10 | === modified file 'fabtasks/bootstrap.py' |
11 | --- fabtasks/bootstrap.py 2012-03-05 17:41:06 +0000 |
12 | +++ fabtasks/bootstrap.py 2012-04-19 21:58:17 +0000 |
13 | @@ -127,6 +127,11 @@ |
14 | "django_project/django_openid_auth") |
15 | _get_or_pull_bzr_branch("lp:convoy", "convoy", revision=20) |
16 | _symlink("branches/convoy/convoy", "django_project/convoy") |
17 | + _get_or_pull_bzr_branch( |
18 | + 'lp:~canonical-ca-hackers/ubuntu-recommender/' |
19 | + 'ubuntu-recommender-client', 'ubuntu-recommender-client') |
20 | + _symlink("branches/ubuntu-recommender-client/sreclient.py", |
21 | + "django_project/sreclient.py") |
22 | |
23 | def bootstrap(): |
24 | virtualenv_create() |
25 | |
26 | === modified file 'src/webcatalog/schema.py' |
27 | --- src/webcatalog/schema.py 2012-04-11 23:13:34 +0000 |
28 | +++ src/webcatalog/schema.py 2012-04-19 21:58:17 +0000 |
29 | @@ -69,7 +69,6 @@ |
30 | class logging(schema.Section): |
31 | webapp_logging_config = schema.StringOption() |
32 | |
33 | - # preflight |
34 | class preflight(schema.Section): |
35 | preflight_base_template = schema.StringOption( |
36 | default="webcatalog/base.html") |
37 | @@ -89,3 +88,7 @@ |
38 | |
39 | class email(schema.Section): |
40 | noreply_from_address = schema.StringOption() |
41 | + |
42 | + class recommender(schema.Section): |
43 | + rec_service_root = schema.StringOption( |
44 | + default="http://rec.ubuntu.com/api/1.0") |
45 | |
46 | === modified file 'src/webcatalog/static/css/carousel.css' |
47 | --- src/webcatalog/static/css/carousel.css 2012-04-18 21:27:46 +0000 |
48 | +++ src/webcatalog/static/css/carousel.css 2012-04-19 21:58:17 +0000 |
49 | @@ -151,40 +151,33 @@ |
50 | background-color: #333; |
51 | } |
52 | |
53 | -#featured-controls, |
54 | -#top-rated-controls { |
55 | +.carousel-controls { |
56 | width: 70px; |
57 | height: 32px; |
58 | float: right; |
59 | } |
60 | |
61 | -#featured-controls .next, #featured-controls .prev, |
62 | -#top-rated-controls .next, #top-rated-controls .prev { |
63 | +.carousel-controls .next, .carousel-controls .prev { |
64 | background: url(/assets/images/arrow-sliders.png) no-repeat 0 0; |
65 | float: left; |
66 | z-index: 20; |
67 | width: 32px; |
68 | height: 29px; |
69 | } |
70 | -#featured-controls .next span, #featured-controls .prev span, |
71 | -#top-rated-controls .next span, #top-rated-controls .prev span { |
72 | +.carousel-controls .next span, .carousel-controls .prev span { |
73 | position:absolute; |
74 | left: -9999em; |
75 | height: 0; |
76 | width: 0; |
77 | } |
78 | -#featured-controls .prev, |
79 | -#top-rated-controls .prev { |
80 | +.carousel-controls .prev { |
81 | background-position:0 0; |
82 | } |
83 | -#featured-controls .next, |
84 | -#top-rated-controls .next { |
85 | +.carousel-controls .next { |
86 | background-position: -32px 0; |
87 | } |
88 | -#featured-controls .prev:hover, #featured-controls .prev:focus, |
89 | -#featured-controls .next:hover, #featured-controls .next:focus, |
90 | -#top-rated-controls .prev:hover, #top-rated-controls .prev:focus, |
91 | -#top-rated-controls .next:hover, #top-rated-controls .next:focus { |
92 | +.carousel-controls .prev:hover, .carousel-controls .prev:focus, |
93 | +.carousel-controls .next:hover, .carousel-controls .next:focus { |
94 | outline: none; |
95 | } |
96 | |
97 | |
98 | === added file 'src/webcatalog/templates/webcatalog/app_carousel_widget.html' |
99 | --- src/webcatalog/templates/webcatalog/app_carousel_widget.html 1970-01-01 00:00:00 +0000 |
100 | +++ src/webcatalog/templates/webcatalog/app_carousel_widget.html 2012-04-19 21:58:17 +0000 |
101 | @@ -0,0 +1,43 @@ |
102 | +{% load webcatalog %} |
103 | +<div class="carousel-container"> |
104 | + <div class="carousel-wrapper"> |
105 | + <div class="carousel"> |
106 | + <ol class="carouselol"> |
107 | + {% autoescape off %} |
108 | + {% comment %} |
109 | +All slides except the first one start off with a "disabled" class, which will |
110 | +make them "display: none". This will avoid loading pointless remote resources |
111 | +(banners) if Javascript is disabled. The "disabled" class is then removed via |
112 | +Javascript. |
113 | + {% endcomment %} |
114 | + <li class="slide{% if forloop.counter > 1%} disabled{% endif %}"> |
115 | + <table class="apps"><tr> |
116 | + {% for app in apps %} |
117 | + <td{% if forloop.counter|divisibleby:3 %} class="lastcol"{% endif %}> |
118 | + <a href="{% url wc-package-detail package_name=app.package_name %}"><img class="icon64" src="{{ app.icon_url_or_default }}"/></a> |
119 | + <h4><a href="{% url wc-package-detail package_name=app.package_name %}">{{ app.name }}</a></h4> |
120 | + <p>{{ app.departments.all.0.name }} | <b>{% if app.price %}${{ app.price }}{% else %}FREE{% endif %}</b></p> |
121 | + <p>{{ app.comment }}</p> |
122 | + {% if ratings %} |
123 | + <div class="top-rated-stars"> |
124 | + {% rating_summary app.ratings_average 'small' app.ratings_total %} |
125 | + </div> |
126 | + {% endif %} |
127 | + </td> |
128 | + {% if forloop.counter|divisibleby:3 and not forloop.counter|divisibleby:6 %} |
129 | + </tr><tr class="lastrow"> |
130 | + {% endif %} |
131 | + {% if forloop.counter|divisibleby:6 and not forloop.last %} |
132 | + </tr></table> |
133 | + </li> |
134 | + <li class="slide{% if forloop.counter > 1%} disabled{% endif %}"> |
135 | + <table class="apps"><tr> |
136 | + {% endif %} |
137 | + {% endfor %} |
138 | + </tr></table> |
139 | + </li> |
140 | + {% endautoescape %} |
141 | + </ol> |
142 | + </div> |
143 | + </div> |
144 | +</div> |
145 | |
146 | === modified file 'src/webcatalog/templates/webcatalog/featured_apps_widget.html' |
147 | --- src/webcatalog/templates/webcatalog/featured_apps_widget.html 2012-04-05 01:09:37 +0000 |
148 | +++ src/webcatalog/templates/webcatalog/featured_apps_widget.html 2012-04-19 21:58:17 +0000 |
149 | @@ -1,47 +1,15 @@ |
150 | -<div id="featured-controls"></div> |
151 | +<div id="featured-carousel"> |
152 | +<div class="carousel-controls"></div> |
153 | <h3>Featured apps on the Ubuntu Software Centre</h3> |
154 | -<div class="carousel-container"> |
155 | - <div class="carousel-wrapper"> |
156 | - <div id="featured-carousel" class="carousel"> |
157 | - <ol class="carouselol"> |
158 | - {% autoescape off %} |
159 | - {% comment %} |
160 | -All slides except the first one start off with a "disabled" class, which will |
161 | -make them "display: none". This will avoid loading pointless remote resources |
162 | -(banners) if Javascript is disabled. The "disabled" class is then removed via |
163 | -Javascript. |
164 | - {% endcomment %} |
165 | - <li class="slide{% if forloop.counter > 1%} disabled{% endif %}"> |
166 | - <table class="apps"><tr> |
167 | - {% for app in featured_apps %} |
168 | - <td{% if forloop.counter|divisibleby:3 %} class="lastcol"{% endif %}> |
169 | - <a href="{% url wc-package-detail package_name=app.package_name %}"><img class="icon64" src="{{ app.icon_url_or_default }}"/></a> |
170 | - <h4><a href="{% url wc-package-detail package_name=app.package_name %}">{{ app.name }}</a></h4> |
171 | - <p>{{ app.departments.all.0.name }} | <b>{% if app.price %}${{ app.price }}{% else %}FREE{% endif %}</b></p> |
172 | - <p>{{ app.comment }}</p> |
173 | - </td> |
174 | - {% if forloop.counter|divisibleby:3 and not forloop.counter|divisibleby:6 %} |
175 | - </tr><tr class="lastrow"> |
176 | - {% endif %} |
177 | - {% if forloop.counter|divisibleby:6 and not forloop.last %} |
178 | - </tr></table> |
179 | - </li> |
180 | - <li class="slide{% if forloop.counter > 1%} disabled{% endif %}"> |
181 | - <table class="apps"><tr> |
182 | - {% endif %} |
183 | - {% endfor %} |
184 | - </tr></table> |
185 | - </li> |
186 | - {% endautoescape %} |
187 | - </ol> |
188 | - </div> |
189 | - </div> |
190 | +{% with ratings=0 apps=featured_apps %} |
191 | +{% include "webcatalog/app_carousel_widget.html" %} |
192 | +{% endwith %} |
193 | </div> |
194 | <script type="text/javascript"> |
195 | YUI({combine: true, comboBase: '{% url wc-combo %}?', root: 'yui/3.4.0/build/'}).use('uwc-carousel', function(Y) { |
196 | var caro = new Y.uwc.Carousel({ |
197 | - nodeContainer: "#featured-carousel", |
198 | - controlsContainer: "#featured-controls", |
199 | + nodeContainer: "#featured-carousel .carousel", |
200 | + controlsContainer: "#featured-carousel .carousel-controls", |
201 | containerHeight: 200, |
202 | containerWidth: 912, |
203 | autoPlay: false |
204 | |
205 | === added file 'src/webcatalog/templates/webcatalog/recommended_apps.html' |
206 | --- src/webcatalog/templates/webcatalog/recommended_apps.html 1970-01-01 00:00:00 +0000 |
207 | +++ src/webcatalog/templates/webcatalog/recommended_apps.html 2012-04-19 21:58:17 +0000 |
208 | @@ -0,0 +1,27 @@ |
209 | +{% extends "webcatalog/base.html" %} |
210 | +{% load i18n %} |
211 | +{% load webcatalog %} |
212 | + |
213 | +{% block head_extra %} |
214 | + <link rel="stylesheet" type="text/css" href="{% url wc-combo %}?light/css/reset.css&light/css/styles.css&css/webcatalog.css&light/css/forms.css&css/carousel.css"/> |
215 | + <script src="{% url wc-combo %}?yui/3.4.0/build/yui/yui-min.js&js/carousel.js"></script> |
216 | +{% endblock %} |
217 | + |
218 | +{% block title %}Recommended apps for {{ application.name }}{% endblock %} |
219 | +{% block header %}Recommended apps for {{ application.name }}{% endblock %} |
220 | + |
221 | +{% block content %} |
222 | + {% include "webcatalog/breadcrumbs_snippet.html" %} |
223 | + <div id="sc-mockup"> |
224 | + <div class="header"> |
225 | + {% rating_summary application.ratings_average 'large' application.ratings_total %} |
226 | + <img class="icon64" src="{{ application.icon_url_or_default }}"/> |
227 | + <h2>{{ application.name }}</h2> |
228 | + <p>{{ application.comment }}</p> |
229 | + </div> |
230 | + </div> |
231 | +<div class="featured-widget"> |
232 | + {% include "webcatalog/recommended_apps_widget.html" %} |
233 | +</div> |
234 | +{% endblock %} |
235 | + |
236 | |
237 | === added file 'src/webcatalog/templates/webcatalog/recommended_apps_widget.html' |
238 | --- src/webcatalog/templates/webcatalog/recommended_apps_widget.html 1970-01-01 00:00:00 +0000 |
239 | +++ src/webcatalog/templates/webcatalog/recommended_apps_widget.html 2012-04-19 21:58:17 +0000 |
240 | @@ -0,0 +1,20 @@ |
241 | +<div id="recommended-carousel"> |
242 | +<div class="carousel-controls"></div> |
243 | + <h3>Other people also downloaded...</h3> |
244 | +{% with ratings=1 apps=recommended_apps %} |
245 | +{% include "webcatalog/app_carousel_widget.html" %} |
246 | +{% endwith %} |
247 | +</div> |
248 | +<script type="text/javascript"> |
249 | +YUI({combine: true, comboBase: '{% url wc-combo %}?', root: 'yui/3.4.0/build/'}).use('uwc-carousel', function(Y) { |
250 | + var caro = new Y.uwc.Carousel({ |
251 | + nodeContainer: "#recommended-carousel .carousel", |
252 | + controlsContainer: "#recommended-carousel .carousel-controls", |
253 | + containerHeight: 200, |
254 | + containerWidth: 912, |
255 | + autoPlay: false |
256 | + }); |
257 | + Y.all('.slide').removeClass('disabled'); |
258 | +}); |
259 | +</script> |
260 | + |
261 | |
262 | === modified file 'src/webcatalog/templates/webcatalog/top_rated_apps_widget.html' |
263 | --- src/webcatalog/templates/webcatalog/top_rated_apps_widget.html 2012-04-05 01:09:37 +0000 |
264 | +++ src/webcatalog/templates/webcatalog/top_rated_apps_widget.html 2012-04-19 21:58:17 +0000 |
265 | @@ -1,51 +1,15 @@ |
266 | -{% load webcatalog %} |
267 | -<div id="top-rated-controls"></div> |
268 | +<div id="top-rated-carousel"> |
269 | +<div class="carousel-controls"></div> |
270 | <h3>Top rated apps on the Ubuntu Software Centre</h3> |
271 | -<div class="carousel-container"> |
272 | - <div class="carousel-wrapper"> |
273 | - <div id="top-rated-carousel" class="carousel"> |
274 | - <ol class="carouselol"> |
275 | - {% autoescape off %} |
276 | - {% comment %} |
277 | -All slides except the first one start off with a "disabled" class, which will |
278 | -make them "display: none". This will avoid loading pointless remote resources |
279 | -(banners) if Javascript is disabled. The "disabled" class is then removed via |
280 | -Javascript. |
281 | - {% endcomment %} |
282 | - <li class="slide{% if forloop.counter > 1%} disabled{% endif %}"> |
283 | - <table class="apps"><tr> |
284 | - {% for app in top_rated_apps %} |
285 | - <td{% if forloop.counter|divisibleby:3 %} class="lastcol"{% endif %}> |
286 | - <a href="{% url wc-package-detail package_name=app.package_name %}"><img class="icon64" src="{{ app.icon_url_or_default }}"/></a> |
287 | - <h4><a href="{% url wc-package-detail package_name=app.package_name %}">{{ app.name }}</a></h4> |
288 | - <p>{{ app.departments.all.0.name }} | <b>{% if app.price %}${{ app.price }}{% else %}FREE{% endif %}</b></p> |
289 | - <p>{{ app.comment }}</p> |
290 | - <div class='top-rated-stars'> |
291 | - {% rating_summary app.ratings_average 'small' app.ratings_total %} |
292 | - </div> |
293 | - </td> |
294 | - {% if forloop.counter|divisibleby:3 and not forloop.counter|divisibleby:6 %} |
295 | - </tr><tr class="lastrow"> |
296 | - {% endif %} |
297 | - {% if forloop.counter|divisibleby:6 and not forloop.last %} |
298 | - </tr></table> |
299 | - </li> |
300 | - <li class="slide{% if forloop.counter > 1%} disabled{% endif %}"> |
301 | - <table class="apps"><tr> |
302 | - {% endif %} |
303 | - {% endfor %} |
304 | - </tr></table> |
305 | - </li> |
306 | - {% endautoescape %} |
307 | - </ol> |
308 | - </div> |
309 | - </div> |
310 | +{% with ratings=1 apps=top_rated_apps %} |
311 | +{% include "webcatalog/app_carousel_widget.html" %} |
312 | +{% endwith %} |
313 | </div> |
314 | <script type="text/javascript"> |
315 | YUI({combine: true, comboBase: '{% url wc-combo %}?', root: 'yui/3.4.0/build/'}).use('uwc-carousel', function(Y) { |
316 | var caro = new Y.uwc.Carousel({ |
317 | - nodeContainer: "#top-rated-carousel", |
318 | - controlsContainer: "#top-rated-controls", |
319 | + nodeContainer: "#top-rated-carousel .carousel", |
320 | + controlsContainer: "#top-rated-carousel .carousel-controls", |
321 | containerHeight: 200, |
322 | containerWidth: 912, |
323 | autoPlay: false |
324 | |
325 | === modified file 'src/webcatalog/tests/test_utilities.py' |
326 | --- src/webcatalog/tests/test_utilities.py 2012-04-18 22:29:29 +0000 |
327 | +++ src/webcatalog/tests/test_utilities.py 2012-04-19 21:58:17 +0000 |
328 | @@ -21,6 +21,7 @@ |
329 | 'CreatePNGFromFileTestCase', |
330 | 'IdentityProviderTestCase', |
331 | 'ScreenshotGetterTestCase', |
332 | + 'WebServicesRecommenderTestCase', |
333 | ] |
334 | |
335 | import json |
336 | @@ -33,6 +34,7 @@ |
337 | ) |
338 | |
339 | from django.conf import settings |
340 | +from django.core.cache import cache |
341 | from django.test import TestCase |
342 | from httplib2 import ServerNotFoundError |
343 | from mock import patch |
344 | @@ -128,6 +130,53 @@ |
345 | u'consumer_key', u'token_secret']), set(data)) |
346 | |
347 | |
348 | +class WebServicesRecommenderTestCase(TestCaseWithFactory): |
349 | + |
350 | + def setUp(self): |
351 | + super(WebServicesRecommenderTestCase, self).setUp() |
352 | + recommend_app_fn = ( |
353 | + 'sreclient.SoftwareCenterRecommenderAPI.recommend_app') |
354 | + self.recommend_app_patcher = patch(recommend_app_fn) |
355 | + self.mock_recommend_app = self.recommend_app_patcher.start() |
356 | + self.addCleanup(self.recommend_app_patcher.stop) |
357 | + self.eg_recommends = { |
358 | + u'rid': u'defe066c7ad7c43f71bac58c3f23cc62', |
359 | + u'data': [ |
360 | + {u'rating': 4.0, u'package_name': u'nautilus-gksu'}, |
361 | + {u'rating': 4.0, u'package_name': u'tribaltrouble2'}, |
362 | + {u'rating': 4.0, u'package_name': u'acm'}, |
363 | + {u'rating': 4.0, u'package_name': u'zgv'}, |
364 | + {u'rating': 3.0, u'package_name': u'nautilus-wallpaper'} |
365 | + ], |
366 | + u'app': u'firefox', |
367 | + } |
368 | + self.mock_recommend_app.return_value = self.eg_recommends |
369 | + cache.clear() |
370 | + |
371 | + def test_get_recommends_for_package_uncached(self): |
372 | + result = WebServices().get_recommends_for_package('firefox') |
373 | + |
374 | + self.assertEqual(result, self.eg_recommends) |
375 | + self.assertEqual(1, self.mock_recommend_app.call_count) |
376 | + |
377 | + def test_get_recommends_caches_result(self): |
378 | + self.assertIs(None, cache.get('get_recommends_for_package-firefox')) |
379 | + |
380 | + result = WebServices().get_recommends_for_package('firefox') |
381 | + |
382 | + self.assertEqual( |
383 | + self.eg_recommends, |
384 | + cache.get('get_recommends_for_package-firefox')) |
385 | + |
386 | + def test_get_recommends_for_package_cached(self): |
387 | + cache.set('get_recommends_for_package-firefox', {'foo': 'bar'}) |
388 | + |
389 | + result = WebServices().get_recommends_for_package('firefox') |
390 | + |
391 | + self.assertEqual({'foo': 'bar'}, result) |
392 | + self.assertEqual(0, self.mock_recommend_app.call_count) |
393 | + |
394 | + |
395 | class ScreenshotGetterTestCase(TestCase): |
396 | @patch('webcatalog.utilities.urllib.urlopen') |
397 | def test_valid_response_returns_list_of_urls(self, mock_urlopen): |
398 | |
399 | === modified file 'src/webcatalog/tests/test_views.py' |
400 | --- src/webcatalog/tests/test_views.py 2012-04-19 02:59:08 +0000 |
401 | +++ src/webcatalog/tests/test_views.py 2012-04-19 21:58:17 +0000 |
402 | @@ -44,6 +44,7 @@ |
403 | __all__ = [ |
404 | 'ApplicationDetailNoSeriesTestCase', |
405 | 'ApplicationDetailTestCase', |
406 | + 'ApplicationRecommendsTestCase', |
407 | 'ApplicationReviewsTestCase', |
408 | 'ApplicationScreenshotsTestCase', |
409 | 'IndexTestCase', |
410 | @@ -611,7 +612,7 @@ |
411 | with patch_settings(FEATURED_APPS=[app.package_name for app in apps]): |
412 | response = self.client.get(reverse('wc-index')) |
413 | |
414 | - self.assertContains(response, 'id="featured-controls"', 1) |
415 | + self.assertContains(response, 'class="carousel-controls"', 1) |
416 | |
417 | def test_link_to_dev_site(self): |
418 | response = self.client.get(reverse('wc-index')) |
419 | @@ -660,7 +661,7 @@ |
420 | with patch_settings(NUMBER_TOP_RATED_APPS=2): |
421 | response = self.client.get(reverse('wc-index')) |
422 | |
423 | - self.assertContains(response, 'id="top-rated-controls"', 1) |
424 | + self.assertContains(response, 'class="carousel-controls"', 1) |
425 | self.assertContains(response, high.package_name, 2) |
426 | self.assertContains(response, mid.package_name, 2) |
427 | self.assertContains(response, low.package_name, 0) |
428 | @@ -1053,6 +1054,75 @@ |
429 | self.assertEqual(200, response.status_code) |
430 | |
431 | |
432 | +class ApplicationRecommendsTestCase(TestCaseWithFactory): |
433 | + |
434 | + def setUp(self): |
435 | + super(ApplicationRecommendsTestCase, self).setUp() |
436 | + get_recommends_fn = ( |
437 | + 'webcatalog.utilities.WebServices.get_recommends_for_package') |
438 | + self.get_recommends_patcher = patch(get_recommends_fn) |
439 | + self.mock_get_recommends = self.get_recommends_patcher.start() |
440 | + self.addCleanup(self.get_recommends_patcher.stop) |
441 | + self.eg_recommends = { |
442 | + u'rid': u'defe066c7ad7c43f71bac58c3f23cc62', |
443 | + u'data': [ |
444 | + {u'rating': 4.0, u'package_name': u'nautilus-gksu'}, |
445 | + {u'rating': 4.0, u'package_name': u'tribaltrouble2'}, |
446 | + {u'rating': 4.0, u'package_name': u'acm'}, |
447 | + {u'rating': 4.0, u'package_name': u'zgv'}, |
448 | + {u'rating': 3.0, u'package_name': u'nautilus-wallpaper'} |
449 | + ], |
450 | + u'app': u'firefox', |
451 | + } |
452 | + self.mock_get_recommends.return_value = self.eg_recommends |
453 | + cache.clear() |
454 | + |
455 | + def test_only_valid_apps(self): |
456 | + response = self.client.get(reverse('wc-package-recommends', |
457 | + args=['doesntexist'])) |
458 | + |
459 | + self.assertEqual(404, response.status_code) |
460 | + |
461 | + def test_renders_recommendations(self): |
462 | + app = self.factory.make_application(package_name='firefox') |
463 | + pkgnames = ['nautilus-gksu', 'tribaltrouble2', 'acm', 'zgv', |
464 | + 'nautilus-wallpaper'] |
465 | + for pkgname in pkgnames: |
466 | + self.factory.make_application(package_name=pkgname) |
467 | + |
468 | + response = self.client.get( |
469 | + reverse('wc-package-recommends', args=['firefox'])) |
470 | + |
471 | + self.assertEqual(200, response.status_code) |
472 | + self.assertTemplateUsed( |
473 | + response, 'webcatalog/recommended_apps.html') |
474 | + self.assertContains(response, '<div class="top-rated-stars">', 5) |
475 | + |
476 | + def test_excludes_non_existing_recommendations(self): |
477 | + app = self.factory.make_application(package_name='firefox') |
478 | + pkgnames = ['nautilus-gksu', 'tribaltrouble2', 'acm', 'zgv'] |
479 | + for pkgname in pkgnames: |
480 | + self.factory.make_application(package_name=pkgname) |
481 | + |
482 | + response = self.client.get( |
483 | + reverse('wc-package-recommends', args=['firefox'])) |
484 | + |
485 | + self.assertEqual(200, response.status_code) |
486 | + self.assertContains(response, '<div class="top-rated-stars">', 4) |
487 | + |
488 | + def test_renders_widget_only_for_json_requests(self): |
489 | + app = self.factory.make_application(package_name='firefox') |
490 | + |
491 | + response = self.client.get( |
492 | + reverse('wc-package-recommends', args=['firefox']), |
493 | + HTTP_X_REQUESTED_WITH='XMLHttpRequest') |
494 | + |
495 | + self.assertTemplateNotUsed( |
496 | + response, 'webcatalog/recommended_apps.html') |
497 | + self.assertTemplateUsed( |
498 | + response, 'webcatalog/recommended_apps_widget.html') |
499 | + |
500 | + |
501 | class ComboViewTestCase(TestCase): |
502 | """Tests for ComboView.""" |
503 | |
504 | |
505 | === modified file 'src/webcatalog/urls.py' |
506 | --- src/webcatalog/urls.py 2012-04-18 21:27:46 +0000 |
507 | +++ src/webcatalog/urls.py 2012-04-19 21:58:17 +0000 |
508 | @@ -46,6 +46,8 @@ |
509 | url(r'^applications/(?P<distro>[-.+\w]+)/(?P<package_name>[-.+:\w]+)/' |
510 | r'reviews/$', |
511 | 'application_reviews', name="wc-package-reviews"), |
512 | + url(r'^recommends/(?P<package_name>[-.+:\w]+)/$', |
513 | + 'application_recommends', name="wc-package-recommends"), |
514 | url(r'^search/$', 'search', name="wc-search"), |
515 | url(r'^search/(?P<distro>[-.+\w]+)/$', 'search', name="wc-search"), |
516 | url(r'^tos/$', name="wc-tos", |
517 | |
518 | === modified file 'src/webcatalog/utilities.py' |
519 | --- src/webcatalog/utilities.py 2012-04-19 02:59:08 +0000 |
520 | +++ src/webcatalog/utilities.py 2012-04-19 21:58:17 +0000 |
521 | @@ -44,6 +44,7 @@ |
522 | ) |
523 | from piston_mini_client.validators import ValidationException |
524 | from rnrclient import RatingsAndReviewsAPI |
525 | +from sreclient import SoftwareCenterRecommenderAPI |
526 | from ssoclient import SingleSignOnAPI |
527 | |
528 | |
529 | @@ -82,6 +83,17 @@ |
530 | cache.set(key, fresh_reviews, settings.REVIEWS_CACHE_TIMEOUT) |
531 | return fresh_reviews |
532 | |
533 | + def get_recommends_for_package(self, package_name): |
534 | + key = 'get_recommends_for_package-{0}'.format(package_name) |
535 | + cached_recommends = cache.get(key) |
536 | + if cached_recommends is not None: |
537 | + return cached_recommends |
538 | + |
539 | + fresh_recommends = self.recommender_api.recommend_app( |
540 | + pkgname=package_name) |
541 | + cache.set(key, fresh_recommends) |
542 | + return fresh_recommends |
543 | + |
544 | def get_screenshots_for_package(self, package_name): |
545 | key = 'get_screenshots_for_package-{0}'.format( |
546 | package_name) |
547 | @@ -152,6 +164,10 @@ |
548 | result.update(data) |
549 | return result |
550 | |
551 | + @property |
552 | + def recommender_api(self): |
553 | + return SoftwareCenterRecommenderAPI(service_root=settings.REC_SERVICE_ROOT) |
554 | + |
555 | |
556 | def full_claimed_id(consumer_key): |
557 | return '%s/+id/%s' % (settings.OPENID_SSO_SERVER_URL.strip('/'), |
558 | |
559 | === modified file 'src/webcatalog/views.py' |
560 | --- src/webcatalog/views.py 2012-04-19 02:10:55 +0000 |
561 | +++ src/webcatalog/views.py 2012-04-19 21:58:17 +0000 |
562 | @@ -42,6 +42,7 @@ |
563 | ) |
564 | from django.template import RequestContext |
565 | from django.utils.translation import ugettext as _ |
566 | +from django.views.decorators.vary import vary_on_headers |
567 | |
568 | from webcatalog.forms import EmailDownloadLinkForm |
569 | from webcatalog.models import ( |
570 | @@ -56,6 +57,7 @@ |
571 | __metaclass__ = type |
572 | __all__ = [ |
573 | 'application_detail', |
574 | + 'application_recommends', |
575 | 'application_reviews', |
576 | 'index', |
577 | 'department_overview', |
578 | @@ -192,6 +194,7 @@ |
579 | request, atts)) |
580 | |
581 | |
582 | +@vary_on_headers('X_REQUESTED_WITH') |
583 | def application_reviews(request, package_name, distro, page=1): |
584 | app = get_object_or_404(Application, package_name=package_name, |
585 | distroseries__code_name=distro) |
586 | @@ -210,6 +213,33 @@ |
587 | return render_to_response(template, RequestContext(request, context)) |
588 | |
589 | |
590 | +@vary_on_headers('X_REQUESTED_WITH') |
591 | +def application_recommends(request, package_name): |
592 | + app = get_object_or_404(Application, package_name=package_name) |
593 | + |
594 | + recommends = WebServices().get_recommends_for_package(package_name) |
595 | + |
596 | + num_recommends = 0 |
597 | + recommended_apps = [] |
598 | + for result in recommends['data']: |
599 | + recommended_app = Application.objects.find_best( |
600 | + package_name=result['package_name']) |
601 | + if recommended_app: |
602 | + recommended_apps.append(recommended_app) |
603 | + num_recommends += 1 |
604 | + if num_recommends > 5: |
605 | + break |
606 | + |
607 | + context = dict(application=app, recommended_apps=recommended_apps) |
608 | + if request.is_ajax(): |
609 | + template = "webcatalog/recommended_apps_widget.html" |
610 | + else: |
611 | + template = "webcatalog/recommended_apps.html" |
612 | + context['breadcrumbs'] = app.crumbs() |
613 | + return render_to_response(template, RequestContext(request, context)) |
614 | + |
615 | + |
616 | +@vary_on_headers('X_REQUESTED_WITH') |
617 | def application_screenshots(request, package_name): |
618 | app = get_object_or_404(Application, package_name=package_name, |
619 | is_latest=True) |
Thanks Michael (and for the vary_on_header fix too!)