Merge lp:~michael.nelson/ubuntu-webcatalog/978000-recommended-apps into lp:ubuntu-webcatalog

Proposed by Michael Nelson
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
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_REQUESTED_WITH. But these views are also (automatically) cached. For the application_recommends view, rather than not caching, I've created two separate urls so they'll be cached separately... if we think that's the best way forward we can update the two views (application screenshots and reviews). In reality, the bug can only be triggered on a cold cache by visiting the non-ajax page then visiting a page whic requests the ajax version.

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).

To post a comment you must log in.
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.

Revision history for this message
Anthony Lenton (elachuni) wrote :

Thanks Michael (and for the vary_on_header fix too!)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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)

Subscribers

People subscribed via source and target branches