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 | 14 | django_project/rnrclient.py | 14 | django_project/rnrclient.py |
6 | 15 | django_project/adminaudit | 15 | django_project/adminaudit |
7 | 16 | django_project/local.cfg | 16 | django_project/local.cfg |
8 | 17 | django_project/sreclient.py | ||
9 | 17 | 18 | ||
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 | 127 | "django_project/django_openid_auth") | 127 | "django_project/django_openid_auth") |
15 | 128 | _get_or_pull_bzr_branch("lp:convoy", "convoy", revision=20) | 128 | _get_or_pull_bzr_branch("lp:convoy", "convoy", revision=20) |
16 | 129 | _symlink("branches/convoy/convoy", "django_project/convoy") | 129 | _symlink("branches/convoy/convoy", "django_project/convoy") |
17 | 130 | _get_or_pull_bzr_branch( | ||
18 | 131 | 'lp:~canonical-ca-hackers/ubuntu-recommender/' | ||
19 | 132 | 'ubuntu-recommender-client', 'ubuntu-recommender-client') | ||
20 | 133 | _symlink("branches/ubuntu-recommender-client/sreclient.py", | ||
21 | 134 | "django_project/sreclient.py") | ||
22 | 130 | 135 | ||
23 | 131 | def bootstrap(): | 136 | def bootstrap(): |
24 | 132 | virtualenv_create() | 137 | virtualenv_create() |
25 | 133 | 138 | ||
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 | 69 | class logging(schema.Section): | 69 | class logging(schema.Section): |
31 | 70 | webapp_logging_config = schema.StringOption() | 70 | webapp_logging_config = schema.StringOption() |
32 | 71 | 71 | ||
33 | 72 | # preflight | ||
34 | 73 | class preflight(schema.Section): | 72 | class preflight(schema.Section): |
35 | 74 | preflight_base_template = schema.StringOption( | 73 | preflight_base_template = schema.StringOption( |
36 | 75 | default="webcatalog/base.html") | 74 | default="webcatalog/base.html") |
37 | @@ -89,3 +88,7 @@ | |||
38 | 89 | 88 | ||
39 | 90 | class email(schema.Section): | 89 | class email(schema.Section): |
40 | 91 | noreply_from_address = schema.StringOption() | 90 | noreply_from_address = schema.StringOption() |
41 | 91 | |||
42 | 92 | class recommender(schema.Section): | ||
43 | 93 | rec_service_root = schema.StringOption( | ||
44 | 94 | default="http://rec.ubuntu.com/api/1.0") | ||
45 | 92 | 95 | ||
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 | 151 | background-color: #333; | 151 | background-color: #333; |
51 | 152 | } | 152 | } |
52 | 153 | 153 | ||
55 | 154 | #featured-controls, | 154 | .carousel-controls { |
54 | 155 | #top-rated-controls { | ||
56 | 156 | width: 70px; | 155 | width: 70px; |
57 | 157 | height: 32px; | 156 | height: 32px; |
58 | 158 | float: right; | 157 | float: right; |
59 | 159 | } | 158 | } |
60 | 160 | 159 | ||
63 | 161 | #featured-controls .next, #featured-controls .prev, | 160 | .carousel-controls .next, .carousel-controls .prev { |
62 | 162 | #top-rated-controls .next, #top-rated-controls .prev { | ||
64 | 163 | background: url(/assets/images/arrow-sliders.png) no-repeat 0 0; | 161 | background: url(/assets/images/arrow-sliders.png) no-repeat 0 0; |
65 | 164 | float: left; | 162 | float: left; |
66 | 165 | z-index: 20; | 163 | z-index: 20; |
67 | 166 | width: 32px; | 164 | width: 32px; |
68 | 167 | height: 29px; | 165 | height: 29px; |
69 | 168 | } | 166 | } |
72 | 169 | #featured-controls .next span, #featured-controls .prev span, | 167 | .carousel-controls .next span, .carousel-controls .prev span { |
71 | 170 | #top-rated-controls .next span, #top-rated-controls .prev span { | ||
73 | 171 | position:absolute; | 168 | position:absolute; |
74 | 172 | left: -9999em; | 169 | left: -9999em; |
75 | 173 | height: 0; | 170 | height: 0; |
76 | 174 | width: 0; | 171 | width: 0; |
77 | 175 | } | 172 | } |
80 | 176 | #featured-controls .prev, | 173 | .carousel-controls .prev { |
79 | 177 | #top-rated-controls .prev { | ||
81 | 178 | background-position:0 0; | 174 | background-position:0 0; |
82 | 179 | } | 175 | } |
85 | 180 | #featured-controls .next, | 176 | .carousel-controls .next { |
84 | 181 | #top-rated-controls .next { | ||
86 | 182 | background-position: -32px 0; | 177 | background-position: -32px 0; |
87 | 183 | } | 178 | } |
92 | 184 | #featured-controls .prev:hover, #featured-controls .prev:focus, | 179 | .carousel-controls .prev:hover, .carousel-controls .prev:focus, |
93 | 185 | #featured-controls .next:hover, #featured-controls .next:focus, | 180 | .carousel-controls .next:hover, .carousel-controls .next:focus { |
90 | 186 | #top-rated-controls .prev:hover, #top-rated-controls .prev:focus, | ||
91 | 187 | #top-rated-controls .next:hover, #top-rated-controls .next:focus { | ||
94 | 188 | outline: none; | 181 | outline: none; |
95 | 189 | } | 182 | } |
96 | 190 | 183 | ||
97 | 191 | 184 | ||
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 | 1 | {% load webcatalog %} | ||
103 | 2 | <div class="carousel-container"> | ||
104 | 3 | <div class="carousel-wrapper"> | ||
105 | 4 | <div class="carousel"> | ||
106 | 5 | <ol class="carouselol"> | ||
107 | 6 | {% autoescape off %} | ||
108 | 7 | {% comment %} | ||
109 | 8 | All slides except the first one start off with a "disabled" class, which will | ||
110 | 9 | make them "display: none". This will avoid loading pointless remote resources | ||
111 | 10 | (banners) if Javascript is disabled. The "disabled" class is then removed via | ||
112 | 11 | Javascript. | ||
113 | 12 | {% endcomment %} | ||
114 | 13 | <li class="slide{% if forloop.counter > 1%} disabled{% endif %}"> | ||
115 | 14 | <table class="apps"><tr> | ||
116 | 15 | {% for app in apps %} | ||
117 | 16 | <td{% if forloop.counter|divisibleby:3 %} class="lastcol"{% endif %}> | ||
118 | 17 | <a href="{% url wc-package-detail package_name=app.package_name %}"><img class="icon64" src="{{ app.icon_url_or_default }}"/></a> | ||
119 | 18 | <h4><a href="{% url wc-package-detail package_name=app.package_name %}">{{ app.name }}</a></h4> | ||
120 | 19 | <p>{{ app.departments.all.0.name }} | <b>{% if app.price %}${{ app.price }}{% else %}FREE{% endif %}</b></p> | ||
121 | 20 | <p>{{ app.comment }}</p> | ||
122 | 21 | {% if ratings %} | ||
123 | 22 | <div class="top-rated-stars"> | ||
124 | 23 | {% rating_summary app.ratings_average 'small' app.ratings_total %} | ||
125 | 24 | </div> | ||
126 | 25 | {% endif %} | ||
127 | 26 | </td> | ||
128 | 27 | {% if forloop.counter|divisibleby:3 and not forloop.counter|divisibleby:6 %} | ||
129 | 28 | </tr><tr class="lastrow"> | ||
130 | 29 | {% endif %} | ||
131 | 30 | {% if forloop.counter|divisibleby:6 and not forloop.last %} | ||
132 | 31 | </tr></table> | ||
133 | 32 | </li> | ||
134 | 33 | <li class="slide{% if forloop.counter > 1%} disabled{% endif %}"> | ||
135 | 34 | <table class="apps"><tr> | ||
136 | 35 | {% endif %} | ||
137 | 36 | {% endfor %} | ||
138 | 37 | </tr></table> | ||
139 | 38 | </li> | ||
140 | 39 | {% endautoescape %} | ||
141 | 40 | </ol> | ||
142 | 41 | </div> | ||
143 | 42 | </div> | ||
144 | 43 | </div> | ||
145 | 0 | 44 | ||
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 @@ | |||
151 | 1 | <div id="featured-controls"></div> | 1 | <div id="featured-carousel"> |
152 | 2 | <div class="carousel-controls"></div> | ||
153 | 2 | <h3>Featured apps on the Ubuntu Software Centre</h3> | 3 | <h3>Featured apps on the Ubuntu Software Centre</h3> |
190 | 3 | <div class="carousel-container"> | 4 | {% with ratings=0 apps=featured_apps %} |
191 | 4 | <div class="carousel-wrapper"> | 5 | {% include "webcatalog/app_carousel_widget.html" %} |
192 | 5 | <div id="featured-carousel" class="carousel"> | 6 | {% endwith %} |
157 | 6 | <ol class="carouselol"> | ||
158 | 7 | {% autoescape off %} | ||
159 | 8 | {% comment %} | ||
160 | 9 | All slides except the first one start off with a "disabled" class, which will | ||
161 | 10 | make them "display: none". This will avoid loading pointless remote resources | ||
162 | 11 | (banners) if Javascript is disabled. The "disabled" class is then removed via | ||
163 | 12 | Javascript. | ||
164 | 13 | {% endcomment %} | ||
165 | 14 | <li class="slide{% if forloop.counter > 1%} disabled{% endif %}"> | ||
166 | 15 | <table class="apps"><tr> | ||
167 | 16 | {% for app in featured_apps %} | ||
168 | 17 | <td{% if forloop.counter|divisibleby:3 %} class="lastcol"{% endif %}> | ||
169 | 18 | <a href="{% url wc-package-detail package_name=app.package_name %}"><img class="icon64" src="{{ app.icon_url_or_default }}"/></a> | ||
170 | 19 | <h4><a href="{% url wc-package-detail package_name=app.package_name %}">{{ app.name }}</a></h4> | ||
171 | 20 | <p>{{ app.departments.all.0.name }} | <b>{% if app.price %}${{ app.price }}{% else %}FREE{% endif %}</b></p> | ||
172 | 21 | <p>{{ app.comment }}</p> | ||
173 | 22 | </td> | ||
174 | 23 | {% if forloop.counter|divisibleby:3 and not forloop.counter|divisibleby:6 %} | ||
175 | 24 | </tr><tr class="lastrow"> | ||
176 | 25 | {% endif %} | ||
177 | 26 | {% if forloop.counter|divisibleby:6 and not forloop.last %} | ||
178 | 27 | </tr></table> | ||
179 | 28 | </li> | ||
180 | 29 | <li class="slide{% if forloop.counter > 1%} disabled{% endif %}"> | ||
181 | 30 | <table class="apps"><tr> | ||
182 | 31 | {% endif %} | ||
183 | 32 | {% endfor %} | ||
184 | 33 | </tr></table> | ||
185 | 34 | </li> | ||
186 | 35 | {% endautoescape %} | ||
187 | 36 | </ol> | ||
188 | 37 | </div> | ||
189 | 38 | </div> | ||
193 | 39 | </div> | 7 | </div> |
194 | 40 | <script type="text/javascript"> | 8 | <script type="text/javascript"> |
195 | 41 | YUI({combine: true, comboBase: '{% url wc-combo %}?', root: 'yui/3.4.0/build/'}).use('uwc-carousel', function(Y) { | 9 | YUI({combine: true, comboBase: '{% url wc-combo %}?', root: 'yui/3.4.0/build/'}).use('uwc-carousel', function(Y) { |
196 | 42 | var caro = new Y.uwc.Carousel({ | 10 | var caro = new Y.uwc.Carousel({ |
199 | 43 | nodeContainer: "#featured-carousel", | 11 | nodeContainer: "#featured-carousel .carousel", |
200 | 44 | controlsContainer: "#featured-controls", | 12 | controlsContainer: "#featured-carousel .carousel-controls", |
201 | 45 | containerHeight: 200, | 13 | containerHeight: 200, |
202 | 46 | containerWidth: 912, | 14 | containerWidth: 912, |
203 | 47 | autoPlay: false | 15 | autoPlay: false |
204 | 48 | 16 | ||
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 | 1 | {% extends "webcatalog/base.html" %} | ||
210 | 2 | {% load i18n %} | ||
211 | 3 | {% load webcatalog %} | ||
212 | 4 | |||
213 | 5 | {% block head_extra %} | ||
214 | 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"/> | ||
215 | 7 | <script src="{% url wc-combo %}?yui/3.4.0/build/yui/yui-min.js&js/carousel.js"></script> | ||
216 | 8 | {% endblock %} | ||
217 | 9 | |||
218 | 10 | {% block title %}Recommended apps for {{ application.name }}{% endblock %} | ||
219 | 11 | {% block header %}Recommended apps for {{ application.name }}{% endblock %} | ||
220 | 12 | |||
221 | 13 | {% block content %} | ||
222 | 14 | {% include "webcatalog/breadcrumbs_snippet.html" %} | ||
223 | 15 | <div id="sc-mockup"> | ||
224 | 16 | <div class="header"> | ||
225 | 17 | {% rating_summary application.ratings_average 'large' application.ratings_total %} | ||
226 | 18 | <img class="icon64" src="{{ application.icon_url_or_default }}"/> | ||
227 | 19 | <h2>{{ application.name }}</h2> | ||
228 | 20 | <p>{{ application.comment }}</p> | ||
229 | 21 | </div> | ||
230 | 22 | </div> | ||
231 | 23 | <div class="featured-widget"> | ||
232 | 24 | {% include "webcatalog/recommended_apps_widget.html" %} | ||
233 | 25 | </div> | ||
234 | 26 | {% endblock %} | ||
235 | 27 | |||
236 | 0 | 28 | ||
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 | 1 | <div id="recommended-carousel"> | ||
242 | 2 | <div class="carousel-controls"></div> | ||
243 | 3 | <h3>Other people also downloaded...</h3> | ||
244 | 4 | {% with ratings=1 apps=recommended_apps %} | ||
245 | 5 | {% include "webcatalog/app_carousel_widget.html" %} | ||
246 | 6 | {% endwith %} | ||
247 | 7 | </div> | ||
248 | 8 | <script type="text/javascript"> | ||
249 | 9 | YUI({combine: true, comboBase: '{% url wc-combo %}?', root: 'yui/3.4.0/build/'}).use('uwc-carousel', function(Y) { | ||
250 | 10 | var caro = new Y.uwc.Carousel({ | ||
251 | 11 | nodeContainer: "#recommended-carousel .carousel", | ||
252 | 12 | controlsContainer: "#recommended-carousel .carousel-controls", | ||
253 | 13 | containerHeight: 200, | ||
254 | 14 | containerWidth: 912, | ||
255 | 15 | autoPlay: false | ||
256 | 16 | }); | ||
257 | 17 | Y.all('.slide').removeClass('disabled'); | ||
258 | 18 | }); | ||
259 | 19 | </script> | ||
260 | 20 | |||
261 | 0 | 21 | ||
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 @@ | |||
268 | 1 | {% load webcatalog %} | 1 | <div id="top-rated-carousel"> |
269 | 2 | <div id="top-rated-controls"></div> | 2 | <div class="carousel-controls"></div> |
270 | 3 | <h3>Top rated apps on the Ubuntu Software Centre</h3> | 3 | <h3>Top rated apps on the Ubuntu Software Centre</h3> |
310 | 4 | <div class="carousel-container"> | 4 | {% with ratings=1 apps=top_rated_apps %} |
311 | 5 | <div class="carousel-wrapper"> | 5 | {% include "webcatalog/app_carousel_widget.html" %} |
312 | 6 | <div id="top-rated-carousel" class="carousel"> | 6 | {% endwith %} |
274 | 7 | <ol class="carouselol"> | ||
275 | 8 | {% autoescape off %} | ||
276 | 9 | {% comment %} | ||
277 | 10 | All slides except the first one start off with a "disabled" class, which will | ||
278 | 11 | make them "display: none". This will avoid loading pointless remote resources | ||
279 | 12 | (banners) if Javascript is disabled. The "disabled" class is then removed via | ||
280 | 13 | Javascript. | ||
281 | 14 | {% endcomment %} | ||
282 | 15 | <li class="slide{% if forloop.counter > 1%} disabled{% endif %}"> | ||
283 | 16 | <table class="apps"><tr> | ||
284 | 17 | {% for app in top_rated_apps %} | ||
285 | 18 | <td{% if forloop.counter|divisibleby:3 %} class="lastcol"{% endif %}> | ||
286 | 19 | <a href="{% url wc-package-detail package_name=app.package_name %}"><img class="icon64" src="{{ app.icon_url_or_default }}"/></a> | ||
287 | 20 | <h4><a href="{% url wc-package-detail package_name=app.package_name %}">{{ app.name }}</a></h4> | ||
288 | 21 | <p>{{ app.departments.all.0.name }} | <b>{% if app.price %}${{ app.price }}{% else %}FREE{% endif %}</b></p> | ||
289 | 22 | <p>{{ app.comment }}</p> | ||
290 | 23 | <div class='top-rated-stars'> | ||
291 | 24 | {% rating_summary app.ratings_average 'small' app.ratings_total %} | ||
292 | 25 | </div> | ||
293 | 26 | </td> | ||
294 | 27 | {% if forloop.counter|divisibleby:3 and not forloop.counter|divisibleby:6 %} | ||
295 | 28 | </tr><tr class="lastrow"> | ||
296 | 29 | {% endif %} | ||
297 | 30 | {% if forloop.counter|divisibleby:6 and not forloop.last %} | ||
298 | 31 | </tr></table> | ||
299 | 32 | </li> | ||
300 | 33 | <li class="slide{% if forloop.counter > 1%} disabled{% endif %}"> | ||
301 | 34 | <table class="apps"><tr> | ||
302 | 35 | {% endif %} | ||
303 | 36 | {% endfor %} | ||
304 | 37 | </tr></table> | ||
305 | 38 | </li> | ||
306 | 39 | {% endautoescape %} | ||
307 | 40 | </ol> | ||
308 | 41 | </div> | ||
309 | 42 | </div> | ||
313 | 43 | </div> | 7 | </div> |
314 | 44 | <script type="text/javascript"> | 8 | <script type="text/javascript"> |
315 | 45 | YUI({combine: true, comboBase: '{% url wc-combo %}?', root: 'yui/3.4.0/build/'}).use('uwc-carousel', function(Y) { | 9 | YUI({combine: true, comboBase: '{% url wc-combo %}?', root: 'yui/3.4.0/build/'}).use('uwc-carousel', function(Y) { |
316 | 46 | var caro = new Y.uwc.Carousel({ | 10 | var caro = new Y.uwc.Carousel({ |
319 | 47 | nodeContainer: "#top-rated-carousel", | 11 | nodeContainer: "#top-rated-carousel .carousel", |
320 | 48 | controlsContainer: "#top-rated-controls", | 12 | controlsContainer: "#top-rated-carousel .carousel-controls", |
321 | 49 | containerHeight: 200, | 13 | containerHeight: 200, |
322 | 50 | containerWidth: 912, | 14 | containerWidth: 912, |
323 | 51 | autoPlay: false | 15 | autoPlay: false |
324 | 52 | 16 | ||
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 | 21 | 'CreatePNGFromFileTestCase', | 21 | 'CreatePNGFromFileTestCase', |
330 | 22 | 'IdentityProviderTestCase', | 22 | 'IdentityProviderTestCase', |
331 | 23 | 'ScreenshotGetterTestCase', | 23 | 'ScreenshotGetterTestCase', |
332 | 24 | 'WebServicesRecommenderTestCase', | ||
333 | 24 | ] | 25 | ] |
334 | 25 | 26 | ||
335 | 26 | import json | 27 | import json |
336 | @@ -33,6 +34,7 @@ | |||
337 | 33 | ) | 34 | ) |
338 | 34 | 35 | ||
339 | 35 | from django.conf import settings | 36 | from django.conf import settings |
340 | 37 | from django.core.cache import cache | ||
341 | 36 | from django.test import TestCase | 38 | from django.test import TestCase |
342 | 37 | from httplib2 import ServerNotFoundError | 39 | from httplib2 import ServerNotFoundError |
343 | 38 | from mock import patch | 40 | from mock import patch |
344 | @@ -128,6 +130,53 @@ | |||
345 | 128 | u'consumer_key', u'token_secret']), set(data)) | 130 | u'consumer_key', u'token_secret']), set(data)) |
346 | 129 | 131 | ||
347 | 130 | 132 | ||
348 | 133 | class WebServicesRecommenderTestCase(TestCaseWithFactory): | ||
349 | 134 | |||
350 | 135 | def setUp(self): | ||
351 | 136 | super(WebServicesRecommenderTestCase, self).setUp() | ||
352 | 137 | recommend_app_fn = ( | ||
353 | 138 | 'sreclient.SoftwareCenterRecommenderAPI.recommend_app') | ||
354 | 139 | self.recommend_app_patcher = patch(recommend_app_fn) | ||
355 | 140 | self.mock_recommend_app = self.recommend_app_patcher.start() | ||
356 | 141 | self.addCleanup(self.recommend_app_patcher.stop) | ||
357 | 142 | self.eg_recommends = { | ||
358 | 143 | u'rid': u'defe066c7ad7c43f71bac58c3f23cc62', | ||
359 | 144 | u'data': [ | ||
360 | 145 | {u'rating': 4.0, u'package_name': u'nautilus-gksu'}, | ||
361 | 146 | {u'rating': 4.0, u'package_name': u'tribaltrouble2'}, | ||
362 | 147 | {u'rating': 4.0, u'package_name': u'acm'}, | ||
363 | 148 | {u'rating': 4.0, u'package_name': u'zgv'}, | ||
364 | 149 | {u'rating': 3.0, u'package_name': u'nautilus-wallpaper'} | ||
365 | 150 | ], | ||
366 | 151 | u'app': u'firefox', | ||
367 | 152 | } | ||
368 | 153 | self.mock_recommend_app.return_value = self.eg_recommends | ||
369 | 154 | cache.clear() | ||
370 | 155 | |||
371 | 156 | def test_get_recommends_for_package_uncached(self): | ||
372 | 157 | result = WebServices().get_recommends_for_package('firefox') | ||
373 | 158 | |||
374 | 159 | self.assertEqual(result, self.eg_recommends) | ||
375 | 160 | self.assertEqual(1, self.mock_recommend_app.call_count) | ||
376 | 161 | |||
377 | 162 | def test_get_recommends_caches_result(self): | ||
378 | 163 | self.assertIs(None, cache.get('get_recommends_for_package-firefox')) | ||
379 | 164 | |||
380 | 165 | result = WebServices().get_recommends_for_package('firefox') | ||
381 | 166 | |||
382 | 167 | self.assertEqual( | ||
383 | 168 | self.eg_recommends, | ||
384 | 169 | cache.get('get_recommends_for_package-firefox')) | ||
385 | 170 | |||
386 | 171 | def test_get_recommends_for_package_cached(self): | ||
387 | 172 | cache.set('get_recommends_for_package-firefox', {'foo': 'bar'}) | ||
388 | 173 | |||
389 | 174 | result = WebServices().get_recommends_for_package('firefox') | ||
390 | 175 | |||
391 | 176 | self.assertEqual({'foo': 'bar'}, result) | ||
392 | 177 | self.assertEqual(0, self.mock_recommend_app.call_count) | ||
393 | 178 | |||
394 | 179 | |||
395 | 131 | class ScreenshotGetterTestCase(TestCase): | 180 | class ScreenshotGetterTestCase(TestCase): |
396 | 132 | @patch('webcatalog.utilities.urllib.urlopen') | 181 | @patch('webcatalog.utilities.urllib.urlopen') |
397 | 133 | def test_valid_response_returns_list_of_urls(self, mock_urlopen): | 182 | def test_valid_response_returns_list_of_urls(self, mock_urlopen): |
398 | 134 | 183 | ||
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 | 44 | __all__ = [ | 44 | __all__ = [ |
404 | 45 | 'ApplicationDetailNoSeriesTestCase', | 45 | 'ApplicationDetailNoSeriesTestCase', |
405 | 46 | 'ApplicationDetailTestCase', | 46 | 'ApplicationDetailTestCase', |
406 | 47 | 'ApplicationRecommendsTestCase', | ||
407 | 47 | 'ApplicationReviewsTestCase', | 48 | 'ApplicationReviewsTestCase', |
408 | 48 | 'ApplicationScreenshotsTestCase', | 49 | 'ApplicationScreenshotsTestCase', |
409 | 49 | 'IndexTestCase', | 50 | 'IndexTestCase', |
410 | @@ -611,7 +612,7 @@ | |||
411 | 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]): |
412 | 612 | response = self.client.get(reverse('wc-index')) | 613 | response = self.client.get(reverse('wc-index')) |
413 | 613 | 614 | ||
415 | 614 | self.assertContains(response, 'id="featured-controls"', 1) | 615 | self.assertContains(response, 'class="carousel-controls"', 1) |
416 | 615 | 616 | ||
417 | 616 | def test_link_to_dev_site(self): | 617 | def test_link_to_dev_site(self): |
418 | 617 | response = self.client.get(reverse('wc-index')) | 618 | response = self.client.get(reverse('wc-index')) |
419 | @@ -660,7 +661,7 @@ | |||
420 | 660 | with patch_settings(NUMBER_TOP_RATED_APPS=2): | 661 | with patch_settings(NUMBER_TOP_RATED_APPS=2): |
421 | 661 | response = self.client.get(reverse('wc-index')) | 662 | response = self.client.get(reverse('wc-index')) |
422 | 662 | 663 | ||
424 | 663 | self.assertContains(response, 'id="top-rated-controls"', 1) | 664 | self.assertContains(response, 'class="carousel-controls"', 1) |
425 | 664 | self.assertContains(response, high.package_name, 2) | 665 | self.assertContains(response, high.package_name, 2) |
426 | 665 | self.assertContains(response, mid.package_name, 2) | 666 | self.assertContains(response, mid.package_name, 2) |
427 | 666 | self.assertContains(response, low.package_name, 0) | 667 | self.assertContains(response, low.package_name, 0) |
428 | @@ -1053,6 +1054,75 @@ | |||
429 | 1053 | self.assertEqual(200, response.status_code) | 1054 | self.assertEqual(200, response.status_code) |
430 | 1054 | 1055 | ||
431 | 1055 | 1056 | ||
432 | 1057 | class ApplicationRecommendsTestCase(TestCaseWithFactory): | ||
433 | 1058 | |||
434 | 1059 | def setUp(self): | ||
435 | 1060 | super(ApplicationRecommendsTestCase, self).setUp() | ||
436 | 1061 | get_recommends_fn = ( | ||
437 | 1062 | 'webcatalog.utilities.WebServices.get_recommends_for_package') | ||
438 | 1063 | self.get_recommends_patcher = patch(get_recommends_fn) | ||
439 | 1064 | self.mock_get_recommends = self.get_recommends_patcher.start() | ||
440 | 1065 | self.addCleanup(self.get_recommends_patcher.stop) | ||
441 | 1066 | self.eg_recommends = { | ||
442 | 1067 | u'rid': u'defe066c7ad7c43f71bac58c3f23cc62', | ||
443 | 1068 | u'data': [ | ||
444 | 1069 | {u'rating': 4.0, u'package_name': u'nautilus-gksu'}, | ||
445 | 1070 | {u'rating': 4.0, u'package_name': u'tribaltrouble2'}, | ||
446 | 1071 | {u'rating': 4.0, u'package_name': u'acm'}, | ||
447 | 1072 | {u'rating': 4.0, u'package_name': u'zgv'}, | ||
448 | 1073 | {u'rating': 3.0, u'package_name': u'nautilus-wallpaper'} | ||
449 | 1074 | ], | ||
450 | 1075 | u'app': u'firefox', | ||
451 | 1076 | } | ||
452 | 1077 | self.mock_get_recommends.return_value = self.eg_recommends | ||
453 | 1078 | cache.clear() | ||
454 | 1079 | |||
455 | 1080 | def test_only_valid_apps(self): | ||
456 | 1081 | response = self.client.get(reverse('wc-package-recommends', | ||
457 | 1082 | args=['doesntexist'])) | ||
458 | 1083 | |||
459 | 1084 | self.assertEqual(404, response.status_code) | ||
460 | 1085 | |||
461 | 1086 | def test_renders_recommendations(self): | ||
462 | 1087 | app = self.factory.make_application(package_name='firefox') | ||
463 | 1088 | pkgnames = ['nautilus-gksu', 'tribaltrouble2', 'acm', 'zgv', | ||
464 | 1089 | 'nautilus-wallpaper'] | ||
465 | 1090 | for pkgname in pkgnames: | ||
466 | 1091 | self.factory.make_application(package_name=pkgname) | ||
467 | 1092 | |||
468 | 1093 | response = self.client.get( | ||
469 | 1094 | reverse('wc-package-recommends', args=['firefox'])) | ||
470 | 1095 | |||
471 | 1096 | self.assertEqual(200, response.status_code) | ||
472 | 1097 | self.assertTemplateUsed( | ||
473 | 1098 | response, 'webcatalog/recommended_apps.html') | ||
474 | 1099 | self.assertContains(response, '<div class="top-rated-stars">', 5) | ||
475 | 1100 | |||
476 | 1101 | def test_excludes_non_existing_recommendations(self): | ||
477 | 1102 | app = self.factory.make_application(package_name='firefox') | ||
478 | 1103 | pkgnames = ['nautilus-gksu', 'tribaltrouble2', 'acm', 'zgv'] | ||
479 | 1104 | for pkgname in pkgnames: | ||
480 | 1105 | self.factory.make_application(package_name=pkgname) | ||
481 | 1106 | |||
482 | 1107 | response = self.client.get( | ||
483 | 1108 | reverse('wc-package-recommends', args=['firefox'])) | ||
484 | 1109 | |||
485 | 1110 | self.assertEqual(200, response.status_code) | ||
486 | 1111 | self.assertContains(response, '<div class="top-rated-stars">', 4) | ||
487 | 1112 | |||
488 | 1113 | def test_renders_widget_only_for_json_requests(self): | ||
489 | 1114 | app = self.factory.make_application(package_name='firefox') | ||
490 | 1115 | |||
491 | 1116 | response = self.client.get( | ||
492 | 1117 | reverse('wc-package-recommends', args=['firefox']), | ||
493 | 1118 | HTTP_X_REQUESTED_WITH='XMLHttpRequest') | ||
494 | 1119 | |||
495 | 1120 | self.assertTemplateNotUsed( | ||
496 | 1121 | response, 'webcatalog/recommended_apps.html') | ||
497 | 1122 | self.assertTemplateUsed( | ||
498 | 1123 | response, 'webcatalog/recommended_apps_widget.html') | ||
499 | 1124 | |||
500 | 1125 | |||
501 | 1056 | class ComboViewTestCase(TestCase): | 1126 | class ComboViewTestCase(TestCase): |
502 | 1057 | """Tests for ComboView.""" | 1127 | """Tests for ComboView.""" |
503 | 1058 | 1128 | ||
504 | 1059 | 1129 | ||
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 | 46 | url(r'^applications/(?P<distro>[-.+\w]+)/(?P<package_name>[-.+:\w]+)/' | 46 | url(r'^applications/(?P<distro>[-.+\w]+)/(?P<package_name>[-.+:\w]+)/' |
510 | 47 | r'reviews/$', | 47 | r'reviews/$', |
511 | 48 | 'application_reviews', name="wc-package-reviews"), | 48 | 'application_reviews', name="wc-package-reviews"), |
512 | 49 | url(r'^recommends/(?P<package_name>[-.+:\w]+)/$', | ||
513 | 50 | 'application_recommends', name="wc-package-recommends"), | ||
514 | 49 | url(r'^search/$', 'search', name="wc-search"), | 51 | url(r'^search/$', 'search', name="wc-search"), |
515 | 50 | url(r'^search/(?P<distro>[-.+\w]+)/$', 'search', name="wc-search"), | 52 | url(r'^search/(?P<distro>[-.+\w]+)/$', 'search', name="wc-search"), |
516 | 51 | url(r'^tos/$', name="wc-tos", | 53 | url(r'^tos/$', name="wc-tos", |
517 | 52 | 54 | ||
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 | 44 | ) | 44 | ) |
523 | 45 | from piston_mini_client.validators import ValidationException | 45 | from piston_mini_client.validators import ValidationException |
524 | 46 | from rnrclient import RatingsAndReviewsAPI | 46 | from rnrclient import RatingsAndReviewsAPI |
525 | 47 | from sreclient import SoftwareCenterRecommenderAPI | ||
526 | 47 | from ssoclient import SingleSignOnAPI | 48 | from ssoclient import SingleSignOnAPI |
527 | 48 | 49 | ||
528 | 49 | 50 | ||
529 | @@ -82,6 +83,17 @@ | |||
530 | 82 | cache.set(key, fresh_reviews, settings.REVIEWS_CACHE_TIMEOUT) | 83 | cache.set(key, fresh_reviews, settings.REVIEWS_CACHE_TIMEOUT) |
531 | 83 | return fresh_reviews | 84 | return fresh_reviews |
532 | 84 | 85 | ||
533 | 86 | def get_recommends_for_package(self, package_name): | ||
534 | 87 | key = 'get_recommends_for_package-{0}'.format(package_name) | ||
535 | 88 | cached_recommends = cache.get(key) | ||
536 | 89 | if cached_recommends is not None: | ||
537 | 90 | return cached_recommends | ||
538 | 91 | |||
539 | 92 | fresh_recommends = self.recommender_api.recommend_app( | ||
540 | 93 | pkgname=package_name) | ||
541 | 94 | cache.set(key, fresh_recommends) | ||
542 | 95 | return fresh_recommends | ||
543 | 96 | |||
544 | 85 | def get_screenshots_for_package(self, package_name): | 97 | def get_screenshots_for_package(self, package_name): |
545 | 86 | key = 'get_screenshots_for_package-{0}'.format( | 98 | key = 'get_screenshots_for_package-{0}'.format( |
546 | 87 | package_name) | 99 | package_name) |
547 | @@ -152,6 +164,10 @@ | |||
548 | 152 | result.update(data) | 164 | result.update(data) |
549 | 153 | return result | 165 | return result |
550 | 154 | 166 | ||
551 | 167 | @property | ||
552 | 168 | def recommender_api(self): | ||
553 | 169 | return SoftwareCenterRecommenderAPI(service_root=settings.REC_SERVICE_ROOT) | ||
554 | 170 | |||
555 | 155 | 171 | ||
556 | 156 | def full_claimed_id(consumer_key): | 172 | def full_claimed_id(consumer_key): |
557 | 157 | return '%s/+id/%s' % (settings.OPENID_SSO_SERVER_URL.strip('/'), | 173 | return '%s/+id/%s' % (settings.OPENID_SSO_SERVER_URL.strip('/'), |
558 | 158 | 174 | ||
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 | 42 | ) | 42 | ) |
564 | 43 | from django.template import RequestContext | 43 | from django.template import RequestContext |
565 | 44 | from django.utils.translation import ugettext as _ | 44 | from django.utils.translation import ugettext as _ |
566 | 45 | from django.views.decorators.vary import vary_on_headers | ||
567 | 45 | 46 | ||
568 | 46 | from webcatalog.forms import EmailDownloadLinkForm | 47 | from webcatalog.forms import EmailDownloadLinkForm |
569 | 47 | from webcatalog.models import ( | 48 | from webcatalog.models import ( |
570 | @@ -56,6 +57,7 @@ | |||
571 | 56 | __metaclass__ = type | 57 | __metaclass__ = type |
572 | 57 | __all__ = [ | 58 | __all__ = [ |
573 | 58 | 'application_detail', | 59 | 'application_detail', |
574 | 60 | 'application_recommends', | ||
575 | 59 | 'application_reviews', | 61 | 'application_reviews', |
576 | 60 | 'index', | 62 | 'index', |
577 | 61 | 'department_overview', | 63 | 'department_overview', |
578 | @@ -192,6 +194,7 @@ | |||
579 | 192 | request, atts)) | 194 | request, atts)) |
580 | 193 | 195 | ||
581 | 194 | 196 | ||
582 | 197 | @vary_on_headers('X_REQUESTED_WITH') | ||
583 | 195 | def application_reviews(request, package_name, distro, page=1): | 198 | def application_reviews(request, package_name, distro, page=1): |
584 | 196 | app = get_object_or_404(Application, package_name=package_name, | 199 | app = get_object_or_404(Application, package_name=package_name, |
585 | 197 | distroseries__code_name=distro) | 200 | distroseries__code_name=distro) |
586 | @@ -210,6 +213,33 @@ | |||
587 | 210 | return render_to_response(template, RequestContext(request, context)) | 213 | return render_to_response(template, RequestContext(request, context)) |
588 | 211 | 214 | ||
589 | 212 | 215 | ||
590 | 216 | @vary_on_headers('X_REQUESTED_WITH') | ||
591 | 217 | def application_recommends(request, package_name): | ||
592 | 218 | app = get_object_or_404(Application, package_name=package_name) | ||
593 | 219 | |||
594 | 220 | recommends = WebServices().get_recommends_for_package(package_name) | ||
595 | 221 | |||
596 | 222 | num_recommends = 0 | ||
597 | 223 | recommended_apps = [] | ||
598 | 224 | for result in recommends['data']: | ||
599 | 225 | recommended_app = Application.objects.find_best( | ||
600 | 226 | package_name=result['package_name']) | ||
601 | 227 | if recommended_app: | ||
602 | 228 | recommended_apps.append(recommended_app) | ||
603 | 229 | num_recommends += 1 | ||
604 | 230 | if num_recommends > 5: | ||
605 | 231 | break | ||
606 | 232 | |||
607 | 233 | context = dict(application=app, recommended_apps=recommended_apps) | ||
608 | 234 | if request.is_ajax(): | ||
609 | 235 | template = "webcatalog/recommended_apps_widget.html" | ||
610 | 236 | else: | ||
611 | 237 | template = "webcatalog/recommended_apps.html" | ||
612 | 238 | context['breadcrumbs'] = app.crumbs() | ||
613 | 239 | return render_to_response(template, RequestContext(request, context)) | ||
614 | 240 | |||
615 | 241 | |||
616 | 242 | @vary_on_headers('X_REQUESTED_WITH') | ||
617 | 213 | def application_screenshots(request, package_name): | 243 | def application_screenshots(request, package_name): |
618 | 214 | app = get_object_or_404(Application, package_name=package_name, | 244 | app = get_object_or_404(Application, package_name=package_name, |
619 | 215 | is_latest=True) | 245 | is_latest=True) |
Thanks Michael (and for the vary_on_header fix too!)