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
=== modified file '.bzrignore'
--- .bzrignore 2012-03-05 17:41:06 +0000
+++ .bzrignore 2012-04-19 21:58:17 +0000
@@ -14,3 +14,4 @@
14django_project/rnrclient.py14django_project/rnrclient.py
15django_project/adminaudit15django_project/adminaudit
16django_project/local.cfg16django_project/local.cfg
17django_project/sreclient.py
1718
=== modified file 'fabtasks/bootstrap.py'
--- fabtasks/bootstrap.py 2012-03-05 17:41:06 +0000
+++ fabtasks/bootstrap.py 2012-04-19 21:58:17 +0000
@@ -127,6 +127,11 @@
127 "django_project/django_openid_auth")127 "django_project/django_openid_auth")
128 _get_or_pull_bzr_branch("lp:convoy", "convoy", revision=20)128 _get_or_pull_bzr_branch("lp:convoy", "convoy", revision=20)
129 _symlink("branches/convoy/convoy", "django_project/convoy")129 _symlink("branches/convoy/convoy", "django_project/convoy")
130 _get_or_pull_bzr_branch(
131 'lp:~canonical-ca-hackers/ubuntu-recommender/'
132 'ubuntu-recommender-client', 'ubuntu-recommender-client')
133 _symlink("branches/ubuntu-recommender-client/sreclient.py",
134 "django_project/sreclient.py")
130135
131def bootstrap():136def bootstrap():
132 virtualenv_create()137 virtualenv_create()
133138
=== modified file 'src/webcatalog/schema.py'
--- src/webcatalog/schema.py 2012-04-11 23:13:34 +0000
+++ src/webcatalog/schema.py 2012-04-19 21:58:17 +0000
@@ -69,7 +69,6 @@
69 class logging(schema.Section):69 class logging(schema.Section):
70 webapp_logging_config = schema.StringOption()70 webapp_logging_config = schema.StringOption()
7171
72 # preflight
73 class preflight(schema.Section):72 class preflight(schema.Section):
74 preflight_base_template = schema.StringOption(73 preflight_base_template = schema.StringOption(
75 default="webcatalog/base.html")74 default="webcatalog/base.html")
@@ -89,3 +88,7 @@
8988
90 class email(schema.Section):89 class email(schema.Section):
91 noreply_from_address = schema.StringOption()90 noreply_from_address = schema.StringOption()
91
92 class recommender(schema.Section):
93 rec_service_root = schema.StringOption(
94 default="http://rec.ubuntu.com/api/1.0")
9295
=== modified file 'src/webcatalog/static/css/carousel.css'
--- src/webcatalog/static/css/carousel.css 2012-04-18 21:27:46 +0000
+++ src/webcatalog/static/css/carousel.css 2012-04-19 21:58:17 +0000
@@ -151,40 +151,33 @@
151 background-color: #333;151 background-color: #333;
152}152}
153153
154#featured-controls,154.carousel-controls {
155#top-rated-controls {
156 width: 70px;155 width: 70px;
157 height: 32px;156 height: 32px;
158 float: right;157 float: right;
159}158}
160159
161#featured-controls .next, #featured-controls .prev,160.carousel-controls .next, .carousel-controls .prev {
162#top-rated-controls .next, #top-rated-controls .prev {
163 background: url(/assets/images/arrow-sliders.png) no-repeat 0 0;161 background: url(/assets/images/arrow-sliders.png) no-repeat 0 0;
164 float: left;162 float: left;
165 z-index: 20;163 z-index: 20;
166 width: 32px;164 width: 32px;
167 height: 29px;165 height: 29px;
168}166}
169#featured-controls .next span, #featured-controls .prev span,167.carousel-controls .next span, .carousel-controls .prev span {
170#top-rated-controls .next span, #top-rated-controls .prev span {
171 position:absolute;168 position:absolute;
172 left: -9999em;169 left: -9999em;
173 height: 0;170 height: 0;
174 width: 0;171 width: 0;
175}172}
176#featured-controls .prev,173.carousel-controls .prev {
177#top-rated-controls .prev {
178 background-position:0 0;174 background-position:0 0;
179}175}
180#featured-controls .next,176.carousel-controls .next {
181#top-rated-controls .next {
182 background-position: -32px 0;177 background-position: -32px 0;
183}178}
184#featured-controls .prev:hover, #featured-controls .prev:focus,179.carousel-controls .prev:hover, .carousel-controls .prev:focus,
185#featured-controls .next:hover, #featured-controls .next:focus,180.carousel-controls .next:hover, .carousel-controls .next:focus {
186#top-rated-controls .prev:hover, #top-rated-controls .prev:focus,
187#top-rated-controls .next:hover, #top-rated-controls .next:focus {
188 outline: none;181 outline: none;
189}182}
190183
191184
=== added file 'src/webcatalog/templates/webcatalog/app_carousel_widget.html'
--- src/webcatalog/templates/webcatalog/app_carousel_widget.html 1970-01-01 00:00:00 +0000
+++ src/webcatalog/templates/webcatalog/app_carousel_widget.html 2012-04-19 21:58:17 +0000
@@ -0,0 +1,43 @@
1{% load webcatalog %}
2<div class="carousel-container">
3 <div class="carousel-wrapper">
4 <div class="carousel">
5 <ol class="carouselol">
6 {% autoescape off %}
7 {% comment %}
8All slides except the first one start off with a "disabled" class, which will
9make them "display: none". This will avoid loading pointless remote resources
10(banners) if Javascript is disabled. The "disabled" class is then removed via
11Javascript.
12 {% endcomment %}
13 <li class="slide{% if forloop.counter > 1%} disabled{% endif %}">
14 <table class="apps"><tr>
15 {% for app in apps %}
16 <td{% if forloop.counter|divisibleby:3 %} class="lastcol"{% endif %}>
17 <a href="{% url wc-package-detail package_name=app.package_name %}"><img class="icon64" src="{{ app.icon_url_or_default }}"/></a>
18 <h4><a href="{% url wc-package-detail package_name=app.package_name %}">{{ app.name }}</a></h4>
19 <p>{{ app.departments.all.0.name }} | <b>{% if app.price %}${{ app.price }}{% else %}FREE{% endif %}</b></p>
20 <p>{{ app.comment }}</p>
21 {% if ratings %}
22 <div class="top-rated-stars">
23 {% rating_summary app.ratings_average 'small' app.ratings_total %}
24 </div>
25 {% endif %}
26 </td>
27 {% if forloop.counter|divisibleby:3 and not forloop.counter|divisibleby:6 %}
28 </tr><tr class="lastrow">
29 {% endif %}
30 {% if forloop.counter|divisibleby:6 and not forloop.last %}
31 </tr></table>
32 </li>
33 <li class="slide{% if forloop.counter > 1%} disabled{% endif %}">
34 <table class="apps"><tr>
35 {% endif %}
36 {% endfor %}
37 </tr></table>
38 </li>
39 {% endautoescape %}
40 </ol>
41 </div>
42 </div>
43</div>
044
=== modified file 'src/webcatalog/templates/webcatalog/featured_apps_widget.html'
--- src/webcatalog/templates/webcatalog/featured_apps_widget.html 2012-04-05 01:09:37 +0000
+++ src/webcatalog/templates/webcatalog/featured_apps_widget.html 2012-04-19 21:58:17 +0000
@@ -1,47 +1,15 @@
1<div id="featured-controls"></div>1<div id="featured-carousel">
2<div class="carousel-controls"></div>
2 <h3>Featured apps on the Ubuntu Software Centre</h3>3 <h3>Featured apps on the Ubuntu Software Centre</h3>
3<div class="carousel-container">4{% with ratings=0 apps=featured_apps %}
4 <div class="carousel-wrapper">5{% include "webcatalog/app_carousel_widget.html" %}
5 <div id="featured-carousel" class="carousel">6{% endwith %}
6 <ol class="carouselol">
7 {% autoescape off %}
8 {% comment %}
9All slides except the first one start off with a "disabled" class, which will
10make them "display: none". This will avoid loading pointless remote resources
11(banners) if Javascript is disabled. The "disabled" class is then removed via
12Javascript.
13 {% endcomment %}
14 <li class="slide{% if forloop.counter > 1%} disabled{% endif %}">
15 <table class="apps"><tr>
16 {% for app in featured_apps %}
17 <td{% if forloop.counter|divisibleby:3 %} class="lastcol"{% endif %}>
18 <a href="{% url wc-package-detail package_name=app.package_name %}"><img class="icon64" src="{{ app.icon_url_or_default }}"/></a>
19 <h4><a href="{% url wc-package-detail package_name=app.package_name %}">{{ app.name }}</a></h4>
20 <p>{{ app.departments.all.0.name }} | <b>{% if app.price %}${{ app.price }}{% else %}FREE{% endif %}</b></p>
21 <p>{{ app.comment }}</p>
22 </td>
23 {% if forloop.counter|divisibleby:3 and not forloop.counter|divisibleby:6 %}
24 </tr><tr class="lastrow">
25 {% endif %}
26 {% if forloop.counter|divisibleby:6 and not forloop.last %}
27 </tr></table>
28 </li>
29 <li class="slide{% if forloop.counter > 1%} disabled{% endif %}">
30 <table class="apps"><tr>
31 {% endif %}
32 {% endfor %}
33 </tr></table>
34 </li>
35 {% endautoescape %}
36 </ol>
37 </div>
38 </div>
39</div>7</div>
40<script type="text/javascript">8<script type="text/javascript">
41YUI({combine: true, comboBase: '{% url wc-combo %}?', root: 'yui/3.4.0/build/'}).use('uwc-carousel', function(Y) {9YUI({combine: true, comboBase: '{% url wc-combo %}?', root: 'yui/3.4.0/build/'}).use('uwc-carousel', function(Y) {
42 var caro = new Y.uwc.Carousel({10 var caro = new Y.uwc.Carousel({
43 nodeContainer: "#featured-carousel",11 nodeContainer: "#featured-carousel .carousel",
44 controlsContainer: "#featured-controls",12 controlsContainer: "#featured-carousel .carousel-controls",
45 containerHeight: 200,13 containerHeight: 200,
46 containerWidth: 912,14 containerWidth: 912,
47 autoPlay: false15 autoPlay: false
4816
=== added file 'src/webcatalog/templates/webcatalog/recommended_apps.html'
--- src/webcatalog/templates/webcatalog/recommended_apps.html 1970-01-01 00:00:00 +0000
+++ src/webcatalog/templates/webcatalog/recommended_apps.html 2012-04-19 21:58:17 +0000
@@ -0,0 +1,27 @@
1{% extends "webcatalog/base.html" %}
2{% load i18n %}
3{% load webcatalog %}
4
5{% block head_extra %}
6 <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"/>
7 <script src="{% url wc-combo %}?yui/3.4.0/build/yui/yui-min.js&js/carousel.js"></script>
8{% endblock %}
9
10{% block title %}Recommended apps for {{ application.name }}{% endblock %}
11{% block header %}Recommended apps for {{ application.name }}{% endblock %}
12
13{% block content %}
14 {% include "webcatalog/breadcrumbs_snippet.html" %}
15 <div id="sc-mockup">
16 <div class="header">
17 {% rating_summary application.ratings_average 'large' application.ratings_total %}
18 <img class="icon64" src="{{ application.icon_url_or_default }}"/>
19 <h2>{{ application.name }}</h2>
20 <p>{{ application.comment }}</p>
21 </div>
22 </div>
23<div class="featured-widget">
24 {% include "webcatalog/recommended_apps_widget.html" %}
25</div>
26{% endblock %}
27
028
=== added file 'src/webcatalog/templates/webcatalog/recommended_apps_widget.html'
--- src/webcatalog/templates/webcatalog/recommended_apps_widget.html 1970-01-01 00:00:00 +0000
+++ src/webcatalog/templates/webcatalog/recommended_apps_widget.html 2012-04-19 21:58:17 +0000
@@ -0,0 +1,20 @@
1<div id="recommended-carousel">
2<div class="carousel-controls"></div>
3 <h3>Other people also downloaded...</h3>
4{% with ratings=1 apps=recommended_apps %}
5{% include "webcatalog/app_carousel_widget.html" %}
6{% endwith %}
7</div>
8<script type="text/javascript">
9YUI({combine: true, comboBase: '{% url wc-combo %}?', root: 'yui/3.4.0/build/'}).use('uwc-carousel', function(Y) {
10 var caro = new Y.uwc.Carousel({
11 nodeContainer: "#recommended-carousel .carousel",
12 controlsContainer: "#recommended-carousel .carousel-controls",
13 containerHeight: 200,
14 containerWidth: 912,
15 autoPlay: false
16 });
17 Y.all('.slide').removeClass('disabled');
18});
19</script>
20
021
=== modified file 'src/webcatalog/templates/webcatalog/top_rated_apps_widget.html'
--- src/webcatalog/templates/webcatalog/top_rated_apps_widget.html 2012-04-05 01:09:37 +0000
+++ src/webcatalog/templates/webcatalog/top_rated_apps_widget.html 2012-04-19 21:58:17 +0000
@@ -1,51 +1,15 @@
1{% load webcatalog %}1<div id="top-rated-carousel">
2<div id="top-rated-controls"></div>2<div class="carousel-controls"></div>
3 <h3>Top rated apps on the Ubuntu Software Centre</h3>3 <h3>Top rated apps on the Ubuntu Software Centre</h3>
4<div class="carousel-container">4{% with ratings=1 apps=top_rated_apps %}
5 <div class="carousel-wrapper">5{% include "webcatalog/app_carousel_widget.html" %}
6 <div id="top-rated-carousel" class="carousel">6{% endwith %}
7 <ol class="carouselol">
8 {% autoescape off %}
9 {% comment %}
10All slides except the first one start off with a "disabled" class, which will
11make them "display: none". This will avoid loading pointless remote resources
12(banners) if Javascript is disabled. The "disabled" class is then removed via
13Javascript.
14 {% endcomment %}
15 <li class="slide{% if forloop.counter > 1%} disabled{% endif %}">
16 <table class="apps"><tr>
17 {% for app in top_rated_apps %}
18 <td{% if forloop.counter|divisibleby:3 %} class="lastcol"{% endif %}>
19 <a href="{% url wc-package-detail package_name=app.package_name %}"><img class="icon64" src="{{ app.icon_url_or_default }}"/></a>
20 <h4><a href="{% url wc-package-detail package_name=app.package_name %}">{{ app.name }}</a></h4>
21 <p>{{ app.departments.all.0.name }} | <b>{% if app.price %}${{ app.price }}{% else %}FREE{% endif %}</b></p>
22 <p>{{ app.comment }}</p>
23 <div class='top-rated-stars'>
24 {% rating_summary app.ratings_average 'small' app.ratings_total %}
25 </div>
26 </td>
27 {% if forloop.counter|divisibleby:3 and not forloop.counter|divisibleby:6 %}
28 </tr><tr class="lastrow">
29 {% endif %}
30 {% if forloop.counter|divisibleby:6 and not forloop.last %}
31 </tr></table>
32 </li>
33 <li class="slide{% if forloop.counter > 1%} disabled{% endif %}">
34 <table class="apps"><tr>
35 {% endif %}
36 {% endfor %}
37 </tr></table>
38 </li>
39 {% endautoescape %}
40 </ol>
41 </div>
42 </div>
43</div>7</div>
44<script type="text/javascript">8<script type="text/javascript">
45YUI({combine: true, comboBase: '{% url wc-combo %}?', root: 'yui/3.4.0/build/'}).use('uwc-carousel', function(Y) {9YUI({combine: true, comboBase: '{% url wc-combo %}?', root: 'yui/3.4.0/build/'}).use('uwc-carousel', function(Y) {
46 var caro = new Y.uwc.Carousel({10 var caro = new Y.uwc.Carousel({
47 nodeContainer: "#top-rated-carousel",11 nodeContainer: "#top-rated-carousel .carousel",
48 controlsContainer: "#top-rated-controls",12 controlsContainer: "#top-rated-carousel .carousel-controls",
49 containerHeight: 200,13 containerHeight: 200,
50 containerWidth: 912,14 containerWidth: 912,
51 autoPlay: false15 autoPlay: false
5216
=== modified file 'src/webcatalog/tests/test_utilities.py'
--- src/webcatalog/tests/test_utilities.py 2012-04-18 22:29:29 +0000
+++ src/webcatalog/tests/test_utilities.py 2012-04-19 21:58:17 +0000
@@ -21,6 +21,7 @@
21 'CreatePNGFromFileTestCase',21 'CreatePNGFromFileTestCase',
22 'IdentityProviderTestCase',22 'IdentityProviderTestCase',
23 'ScreenshotGetterTestCase',23 'ScreenshotGetterTestCase',
24 'WebServicesRecommenderTestCase',
24 ]25 ]
2526
26import json27import json
@@ -33,6 +34,7 @@
33 )34 )
3435
35from django.conf import settings36from django.conf import settings
37from django.core.cache import cache
36from django.test import TestCase38from django.test import TestCase
37from httplib2 import ServerNotFoundError39from httplib2 import ServerNotFoundError
38from mock import patch40from mock import patch
@@ -128,6 +130,53 @@
128 u'consumer_key', u'token_secret']), set(data))130 u'consumer_key', u'token_secret']), set(data))
129131
130132
133class WebServicesRecommenderTestCase(TestCaseWithFactory):
134
135 def setUp(self):
136 super(WebServicesRecommenderTestCase, self).setUp()
137 recommend_app_fn = (
138 'sreclient.SoftwareCenterRecommenderAPI.recommend_app')
139 self.recommend_app_patcher = patch(recommend_app_fn)
140 self.mock_recommend_app = self.recommend_app_patcher.start()
141 self.addCleanup(self.recommend_app_patcher.stop)
142 self.eg_recommends = {
143 u'rid': u'defe066c7ad7c43f71bac58c3f23cc62',
144 u'data': [
145 {u'rating': 4.0, u'package_name': u'nautilus-gksu'},
146 {u'rating': 4.0, u'package_name': u'tribaltrouble2'},
147 {u'rating': 4.0, u'package_name': u'acm'},
148 {u'rating': 4.0, u'package_name': u'zgv'},
149 {u'rating': 3.0, u'package_name': u'nautilus-wallpaper'}
150 ],
151 u'app': u'firefox',
152 }
153 self.mock_recommend_app.return_value = self.eg_recommends
154 cache.clear()
155
156 def test_get_recommends_for_package_uncached(self):
157 result = WebServices().get_recommends_for_package('firefox')
158
159 self.assertEqual(result, self.eg_recommends)
160 self.assertEqual(1, self.mock_recommend_app.call_count)
161
162 def test_get_recommends_caches_result(self):
163 self.assertIs(None, cache.get('get_recommends_for_package-firefox'))
164
165 result = WebServices().get_recommends_for_package('firefox')
166
167 self.assertEqual(
168 self.eg_recommends,
169 cache.get('get_recommends_for_package-firefox'))
170
171 def test_get_recommends_for_package_cached(self):
172 cache.set('get_recommends_for_package-firefox', {'foo': 'bar'})
173
174 result = WebServices().get_recommends_for_package('firefox')
175
176 self.assertEqual({'foo': 'bar'}, result)
177 self.assertEqual(0, self.mock_recommend_app.call_count)
178
179
131class ScreenshotGetterTestCase(TestCase):180class ScreenshotGetterTestCase(TestCase):
132 @patch('webcatalog.utilities.urllib.urlopen')181 @patch('webcatalog.utilities.urllib.urlopen')
133 def test_valid_response_returns_list_of_urls(self, mock_urlopen):182 def test_valid_response_returns_list_of_urls(self, mock_urlopen):
134183
=== modified file 'src/webcatalog/tests/test_views.py'
--- src/webcatalog/tests/test_views.py 2012-04-19 02:59:08 +0000
+++ src/webcatalog/tests/test_views.py 2012-04-19 21:58:17 +0000
@@ -44,6 +44,7 @@
44__all__ = [44__all__ = [
45 'ApplicationDetailNoSeriesTestCase',45 'ApplicationDetailNoSeriesTestCase',
46 'ApplicationDetailTestCase',46 'ApplicationDetailTestCase',
47 'ApplicationRecommendsTestCase',
47 'ApplicationReviewsTestCase',48 'ApplicationReviewsTestCase',
48 'ApplicationScreenshotsTestCase',49 'ApplicationScreenshotsTestCase',
49 'IndexTestCase',50 'IndexTestCase',
@@ -611,7 +612,7 @@
611 with patch_settings(FEATURED_APPS=[app.package_name for app in apps]):612 with patch_settings(FEATURED_APPS=[app.package_name for app in apps]):
612 response = self.client.get(reverse('wc-index'))613 response = self.client.get(reverse('wc-index'))
613614
614 self.assertContains(response, 'id="featured-controls"', 1)615 self.assertContains(response, 'class="carousel-controls"', 1)
615616
616 def test_link_to_dev_site(self):617 def test_link_to_dev_site(self):
617 response = self.client.get(reverse('wc-index'))618 response = self.client.get(reverse('wc-index'))
@@ -660,7 +661,7 @@
660 with patch_settings(NUMBER_TOP_RATED_APPS=2):661 with patch_settings(NUMBER_TOP_RATED_APPS=2):
661 response = self.client.get(reverse('wc-index'))662 response = self.client.get(reverse('wc-index'))
662663
663 self.assertContains(response, 'id="top-rated-controls"', 1)664 self.assertContains(response, 'class="carousel-controls"', 1)
664 self.assertContains(response, high.package_name, 2)665 self.assertContains(response, high.package_name, 2)
665 self.assertContains(response, mid.package_name, 2)666 self.assertContains(response, mid.package_name, 2)
666 self.assertContains(response, low.package_name, 0)667 self.assertContains(response, low.package_name, 0)
@@ -1053,6 +1054,75 @@
1053 self.assertEqual(200, response.status_code)1054 self.assertEqual(200, response.status_code)
10541055
10551056
1057class ApplicationRecommendsTestCase(TestCaseWithFactory):
1058
1059 def setUp(self):
1060 super(ApplicationRecommendsTestCase, self).setUp()
1061 get_recommends_fn = (
1062 'webcatalog.utilities.WebServices.get_recommends_for_package')
1063 self.get_recommends_patcher = patch(get_recommends_fn)
1064 self.mock_get_recommends = self.get_recommends_patcher.start()
1065 self.addCleanup(self.get_recommends_patcher.stop)
1066 self.eg_recommends = {
1067 u'rid': u'defe066c7ad7c43f71bac58c3f23cc62',
1068 u'data': [
1069 {u'rating': 4.0, u'package_name': u'nautilus-gksu'},
1070 {u'rating': 4.0, u'package_name': u'tribaltrouble2'},
1071 {u'rating': 4.0, u'package_name': u'acm'},
1072 {u'rating': 4.0, u'package_name': u'zgv'},
1073 {u'rating': 3.0, u'package_name': u'nautilus-wallpaper'}
1074 ],
1075 u'app': u'firefox',
1076 }
1077 self.mock_get_recommends.return_value = self.eg_recommends
1078 cache.clear()
1079
1080 def test_only_valid_apps(self):
1081 response = self.client.get(reverse('wc-package-recommends',
1082 args=['doesntexist']))
1083
1084 self.assertEqual(404, response.status_code)
1085
1086 def test_renders_recommendations(self):
1087 app = self.factory.make_application(package_name='firefox')
1088 pkgnames = ['nautilus-gksu', 'tribaltrouble2', 'acm', 'zgv',
1089 'nautilus-wallpaper']
1090 for pkgname in pkgnames:
1091 self.factory.make_application(package_name=pkgname)
1092
1093 response = self.client.get(
1094 reverse('wc-package-recommends', args=['firefox']))
1095
1096 self.assertEqual(200, response.status_code)
1097 self.assertTemplateUsed(
1098 response, 'webcatalog/recommended_apps.html')
1099 self.assertContains(response, '<div class="top-rated-stars">', 5)
1100
1101 def test_excludes_non_existing_recommendations(self):
1102 app = self.factory.make_application(package_name='firefox')
1103 pkgnames = ['nautilus-gksu', 'tribaltrouble2', 'acm', 'zgv']
1104 for pkgname in pkgnames:
1105 self.factory.make_application(package_name=pkgname)
1106
1107 response = self.client.get(
1108 reverse('wc-package-recommends', args=['firefox']))
1109
1110 self.assertEqual(200, response.status_code)
1111 self.assertContains(response, '<div class="top-rated-stars">', 4)
1112
1113 def test_renders_widget_only_for_json_requests(self):
1114 app = self.factory.make_application(package_name='firefox')
1115
1116 response = self.client.get(
1117 reverse('wc-package-recommends', args=['firefox']),
1118 HTTP_X_REQUESTED_WITH='XMLHttpRequest')
1119
1120 self.assertTemplateNotUsed(
1121 response, 'webcatalog/recommended_apps.html')
1122 self.assertTemplateUsed(
1123 response, 'webcatalog/recommended_apps_widget.html')
1124
1125
1056class ComboViewTestCase(TestCase):1126class ComboViewTestCase(TestCase):
1057 """Tests for ComboView."""1127 """Tests for ComboView."""
10581128
10591129
=== modified file 'src/webcatalog/urls.py'
--- src/webcatalog/urls.py 2012-04-18 21:27:46 +0000
+++ src/webcatalog/urls.py 2012-04-19 21:58:17 +0000
@@ -46,6 +46,8 @@
46 url(r'^applications/(?P<distro>[-.+\w]+)/(?P<package_name>[-.+:\w]+)/'46 url(r'^applications/(?P<distro>[-.+\w]+)/(?P<package_name>[-.+:\w]+)/'
47 r'reviews/$',47 r'reviews/$',
48 'application_reviews', name="wc-package-reviews"),48 'application_reviews', name="wc-package-reviews"),
49 url(r'^recommends/(?P<package_name>[-.+:\w]+)/$',
50 'application_recommends', name="wc-package-recommends"),
49 url(r'^search/$', 'search', name="wc-search"),51 url(r'^search/$', 'search', name="wc-search"),
50 url(r'^search/(?P<distro>[-.+\w]+)/$', 'search', name="wc-search"),52 url(r'^search/(?P<distro>[-.+\w]+)/$', 'search', name="wc-search"),
51 url(r'^tos/$', name="wc-tos",53 url(r'^tos/$', name="wc-tos",
5254
=== modified file 'src/webcatalog/utilities.py'
--- src/webcatalog/utilities.py 2012-04-19 02:59:08 +0000
+++ src/webcatalog/utilities.py 2012-04-19 21:58:17 +0000
@@ -44,6 +44,7 @@
44 )44 )
45from piston_mini_client.validators import ValidationException45from piston_mini_client.validators import ValidationException
46from rnrclient import RatingsAndReviewsAPI46from rnrclient import RatingsAndReviewsAPI
47from sreclient import SoftwareCenterRecommenderAPI
47from ssoclient import SingleSignOnAPI48from ssoclient import SingleSignOnAPI
4849
4950
@@ -82,6 +83,17 @@
82 cache.set(key, fresh_reviews, settings.REVIEWS_CACHE_TIMEOUT)83 cache.set(key, fresh_reviews, settings.REVIEWS_CACHE_TIMEOUT)
83 return fresh_reviews84 return fresh_reviews
8485
86 def get_recommends_for_package(self, package_name):
87 key = 'get_recommends_for_package-{0}'.format(package_name)
88 cached_recommends = cache.get(key)
89 if cached_recommends is not None:
90 return cached_recommends
91
92 fresh_recommends = self.recommender_api.recommend_app(
93 pkgname=package_name)
94 cache.set(key, fresh_recommends)
95 return fresh_recommends
96
85 def get_screenshots_for_package(self, package_name):97 def get_screenshots_for_package(self, package_name):
86 key = 'get_screenshots_for_package-{0}'.format(98 key = 'get_screenshots_for_package-{0}'.format(
87 package_name)99 package_name)
@@ -152,6 +164,10 @@
152 result.update(data)164 result.update(data)
153 return result165 return result
154166
167 @property
168 def recommender_api(self):
169 return SoftwareCenterRecommenderAPI(service_root=settings.REC_SERVICE_ROOT)
170
155171
156def full_claimed_id(consumer_key):172def full_claimed_id(consumer_key):
157 return '%s/+id/%s' % (settings.OPENID_SSO_SERVER_URL.strip('/'),173 return '%s/+id/%s' % (settings.OPENID_SSO_SERVER_URL.strip('/'),
158174
=== modified file 'src/webcatalog/views.py'
--- src/webcatalog/views.py 2012-04-19 02:10:55 +0000
+++ src/webcatalog/views.py 2012-04-19 21:58:17 +0000
@@ -42,6 +42,7 @@
42 )42 )
43from django.template import RequestContext43from django.template import RequestContext
44from django.utils.translation import ugettext as _44from django.utils.translation import ugettext as _
45from django.views.decorators.vary import vary_on_headers
4546
46from webcatalog.forms import EmailDownloadLinkForm47from webcatalog.forms import EmailDownloadLinkForm
47from webcatalog.models import (48from webcatalog.models import (
@@ -56,6 +57,7 @@
56__metaclass__ = type57__metaclass__ = type
57__all__ = [58__all__ = [
58 'application_detail',59 'application_detail',
60 'application_recommends',
59 'application_reviews',61 'application_reviews',
60 'index',62 'index',
61 'department_overview',63 'department_overview',
@@ -192,6 +194,7 @@
192 request, atts))194 request, atts))
193195
194196
197@vary_on_headers('X_REQUESTED_WITH')
195def application_reviews(request, package_name, distro, page=1):198def application_reviews(request, package_name, distro, page=1):
196 app = get_object_or_404(Application, package_name=package_name,199 app = get_object_or_404(Application, package_name=package_name,
197 distroseries__code_name=distro)200 distroseries__code_name=distro)
@@ -210,6 +213,33 @@
210 return render_to_response(template, RequestContext(request, context))213 return render_to_response(template, RequestContext(request, context))
211214
212215
216@vary_on_headers('X_REQUESTED_WITH')
217def application_recommends(request, package_name):
218 app = get_object_or_404(Application, package_name=package_name)
219
220 recommends = WebServices().get_recommends_for_package(package_name)
221
222 num_recommends = 0
223 recommended_apps = []
224 for result in recommends['data']:
225 recommended_app = Application.objects.find_best(
226 package_name=result['package_name'])
227 if recommended_app:
228 recommended_apps.append(recommended_app)
229 num_recommends += 1
230 if num_recommends > 5:
231 break
232
233 context = dict(application=app, recommended_apps=recommended_apps)
234 if request.is_ajax():
235 template = "webcatalog/recommended_apps_widget.html"
236 else:
237 template = "webcatalog/recommended_apps.html"
238 context['breadcrumbs'] = app.crumbs()
239 return render_to_response(template, RequestContext(request, context))
240
241
242@vary_on_headers('X_REQUESTED_WITH')
213def application_screenshots(request, package_name):243def application_screenshots(request, package_name):
214 app = get_object_or_404(Application, package_name=package_name,244 app = get_object_or_404(Application, package_name=package_name,
215 is_latest=True)245 is_latest=True)

Subscribers

People subscribed via source and target branches