Merge lp:~elachuni/ubuntu-webcatalog/lazy-screenshots into lp:ubuntu-webcatalog
- lazy-screenshots
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Anthony Lenton | ||||
Approved revision: | 107 | ||||
Merged at revision: | 106 | ||||
Proposed branch: | lp:~elachuni/ubuntu-webcatalog/lazy-screenshots | ||||
Merge into: | lp:ubuntu-webcatalog | ||||
Diff against target: |
760 lines (+302/-135) 15 files modified
django_project/config/main.cfg (+3/-1) src/webcatalog/admin.py (+12/-0) src/webcatalog/api/urls.py (+8/-7) src/webcatalog/management/commands/import_app_install_data.py (+0/-25) src/webcatalog/static/css/carousel.css (+13/-30) src/webcatalog/static/js/carousel.js (+14/-13) src/webcatalog/templates/webcatalog/application_detail.html (+0/-4) src/webcatalog/templates/webcatalog/application_screenshots.html (+25/-0) src/webcatalog/templates/webcatalog/screenshot_widget.html (+67/-23) src/webcatalog/tests/test_commands.py (+0/-30) src/webcatalog/tests/test_utilities.py (+34/-0) src/webcatalog/tests/test_views.py (+89/-2) src/webcatalog/urls.py (+2/-0) src/webcatalog/utilities.py (+22/-0) src/webcatalog/views.py (+13/-0) |
||||
To merge this branch: | bzr merge lp:~elachuni/ubuntu-webcatalog/lazy-screenshots | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Danny Tamez (community) | Approve | ||
Review via email: mp+102602@code.launchpad.net |
Commit message
Made screenshots load on demand when the details for an app are requested, instead of preloading them all when the import_
Description of the change
This branch makes screenshots load on demand when the details for an app are requested, instead of preloading them all when the import_
The screenshots carousel widget was improved to fetch screenshots for an app via AJAX. The following cases were checked:
a) 2+ screenshots: The first screenshot is displayed by default, but a carousel widget is added via JS on page load.
b) 1 screenshots: The screenshot is included statically
c) 0 screenshots: A link to screenshots.
While I was there I added caching the caching middleware to the dev environment and excepted all of the api calls, so that we can enable caching on staging and prod too.
Preview Diff
1 | === modified file 'django_project/config/main.cfg' |
2 | --- django_project/config/main.cfg 2012-03-27 20:43:10 +0000 |
3 | +++ django_project/config/main.cfg 2012-04-18 22:40:24 +0000 |
4 | @@ -27,12 +27,14 @@ |
5 | pgtools |
6 | login_url = /openid/login/ |
7 | managers = %(admins)s |
8 | -middleware_classes = django.middleware.common.CommonMiddleware |
9 | +middleware_classes = django.middleware.cache.UpdateCacheMiddleware |
10 | + django.middleware.common.CommonMiddleware |
11 | django.contrib.sessions.middleware.SessionMiddleware |
12 | django.middleware.csrf.CsrfViewMiddleware |
13 | django.contrib.auth.middleware.AuthenticationMiddleware |
14 | django.contrib.messages.middleware.MessageMiddleware |
15 | webcatalog.middleware.exception.LogExceptionMiddleware |
16 | + django.middleware.cache.FetchFromCacheMiddleware |
17 | fixture_dirs = |
18 | |
19 | secret_key = eepu9Av5ixage9ahhodovahfaiFoorodahf6keip3eichaeW9f |
20 | |
21 | === modified file 'src/webcatalog/admin.py' |
22 | --- src/webcatalog/admin.py 2012-03-20 22:23:31 +0000 |
23 | +++ src/webcatalog/admin.py 2012-04-18 22:40:24 +0000 |
24 | @@ -24,6 +24,7 @@ |
25 | from django.contrib import admin |
26 | from webcatalog.models import ( |
27 | Application, |
28 | + ApplicationMedia, |
29 | Department, |
30 | DistroSeries, |
31 | Exhibit, |
32 | @@ -57,7 +58,18 @@ |
33 | class DepartmentAdmin(admin.ModelAdmin): |
34 | prepopulated_fields = {"slug": ("name",)} |
35 | |
36 | + |
37 | +class ApplicationMediaAdmin(admin.ModelAdmin): |
38 | + search_fields = ('application__package_name',) |
39 | + list_filter = ('application__distroseries',) |
40 | + list_display = ('__unicode__', 'distroseries', 'url') |
41 | + raw_id_fields = ('application',) |
42 | + |
43 | + def distroseries(self, obj): |
44 | + return obj.application.distroseries |
45 | + |
46 | admin.site.register(Application, ApplicationAdmin) |
47 | +admin.site.register(ApplicationMedia, ApplicationMediaAdmin) |
48 | admin.site.register(Department, DepartmentAdmin) |
49 | admin.site.register(DistroSeries) |
50 | admin.site.register(Exhibit, ExhibitAdmin) |
51 | |
52 | === modified file 'src/webcatalog/api/urls.py' |
53 | --- src/webcatalog/api/urls.py 2012-01-06 17:54:47 +0000 |
54 | +++ src/webcatalog/api/urls.py 2012-04-18 22:40:24 +0000 |
55 | @@ -15,6 +15,7 @@ |
56 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
57 | |
58 | from django.conf.urls.defaults import url, patterns |
59 | +from django.views.decorators.cache import never_cache |
60 | |
61 | from piston.resource import Resource |
62 | from webcatalog.api.handlers import ( |
63 | @@ -34,13 +35,13 @@ |
64 | super(CSRFExemptResource, self).__init__(handler, authentication) |
65 | self.csrf_exempt = True |
66 | |
67 | -server_status_resource = Resource(handler=ServerStatusHandler) |
68 | -list_machines_resource = Resource(handler=ListMachinesHandler, |
69 | - authentication=auth) |
70 | -machine_resource = CSRFExemptResource(handler=MachineHandler, |
71 | - authentication=auth) |
72 | -packages_resource = CSRFExemptResource(handler=PackagesHandler, |
73 | - authentication=auth) |
74 | +server_status_resource = never_cache(Resource(handler=ServerStatusHandler)) |
75 | +list_machines_resource = never_cache(Resource(handler=ListMachinesHandler, |
76 | + authentication=auth)) |
77 | +machine_resource = never_cache(CSRFExemptResource(handler=MachineHandler, |
78 | + authentication=auth)) |
79 | +packages_resource = never_cache(CSRFExemptResource(handler=PackagesHandler, |
80 | + authentication=auth)) |
81 | |
82 | urlpatterns = patterns('', |
83 | # get status of the service (usually just "ok", might be "read-only") |
84 | |
85 | === modified file 'src/webcatalog/management/commands/import_app_install_data.py' |
86 | --- src/webcatalog/management/commands/import_app_install_data.py 2012-04-03 17:45:07 +0000 |
87 | +++ src/webcatalog/management/commands/import_app_install_data.py 2012-04-18 22:40:24 +0000 |
88 | @@ -22,7 +22,6 @@ |
89 | with_statement, |
90 | ) |
91 | |
92 | -import json |
93 | import os |
94 | import re |
95 | import shutil |
96 | @@ -32,7 +31,6 @@ |
97 | from glob import iglob |
98 | from optparse import make_option |
99 | from tempfile import NamedTemporaryFile |
100 | -from urlparse import urljoin |
101 | |
102 | import apt |
103 | from apt.cache import FetchFailedException |
104 | @@ -166,27 +164,6 @@ |
105 | |
106 | shutil.rmtree(data_dir) |
107 | |
108 | - def get_screenshot_urls_from_server(self, app): |
109 | - url = urljoin(settings.SCREENSHOTS_BASE_URL, |
110 | - '/json/package/{0}'.format(app.package_name)) |
111 | - try: |
112 | - response = urllib.urlopen(url) |
113 | - if response.getcode() == 200: |
114 | - data = json.loads(response.read()) |
115 | - for entry in data['screenshots']: |
116 | - screenshot_url = entry['large_image_url'] |
117 | - |
118 | - self.output("Adding {0} to {1}\n".format( |
119 | - screenshot_url, |
120 | - app.package_name), 2) |
121 | - app.applicationmedia_set.get_or_create( |
122 | - media_type='screenshot', |
123 | - url=screenshot_url, |
124 | - ) |
125 | - app.save() |
126 | - except (IOError, TypeError): |
127 | - pass |
128 | - |
129 | def get_package_data_for_series(self, package_name, distroseries, |
130 | working_dir): |
131 | install_data_url = self.get_package_uri_for_series( |
132 | @@ -216,7 +193,6 @@ |
133 | app.distroseries = distroseries |
134 | app.save() |
135 | app.update_departments() |
136 | - self.get_screenshot_urls_from_server(app) |
137 | self.add_icon_to_app(app, icon_dir) |
138 | if self.verbosity > 1: |
139 | self.output(u"{0} created.\n".format(app.name).encode( |
140 | @@ -338,7 +314,6 @@ |
141 | version=version, |
142 | comment=app_comment, |
143 | ) |
144 | - self.get_screenshot_urls_from_server(app) |
145 | self.fetch_icon_from_extras(app, candidate) |
146 | if self.verbosity == 1: |
147 | self.output("\n", 1) |
148 | |
149 | === modified file 'src/webcatalog/static/css/carousel.css' |
150 | --- src/webcatalog/static/css/carousel.css 2012-04-12 09:55:28 +0000 |
151 | +++ src/webcatalog/static/css/carousel.css 2012-04-18 22:40:24 +0000 |
152 | @@ -16,7 +16,6 @@ |
153 | z-index: 20; |
154 | left: 50%; |
155 | bottom: 5px; |
156 | - margin-left: -35px; |
157 | } |
158 | .carousel .pagination li { |
159 | float: left; |
160 | @@ -202,40 +201,24 @@ |
161 | } |
162 | |
163 | /* Screenshots carousel */ |
164 | -.screenshot .carousel-wrapper { |
165 | +div.screenshot .carousel-wrapper { |
166 | height: 190px; |
167 | width: 233px; |
168 | } |
169 | -.screenshot .carousel .pagination li a.active { |
170 | +div.screenshot .carousel-wrapper .slide { |
171 | + margin: 0; |
172 | + width: 233px; |
173 | +} |
174 | +div.screenshot .carousel-wrapper .slide img { |
175 | + margin: 0 auto; |
176 | + display: block |
177 | +} |
178 | +div.screenshot .carousel .pagination li a.active { |
179 | background-color: #DD4814; |
180 | } |
181 | -#screenshots-controls { |
182 | - width: 70px; |
183 | - height: 32px; |
184 | - margin-left: 94px; |
185 | -} |
186 | -#screenshots-controls .next, #screenshots-controls .prev { |
187 | - background: url(/assets/images/arrow-sliders.png) no-repeat 0 0; |
188 | - float: left; |
189 | - z-index: 20; |
190 | - width: 32px; |
191 | - height: 29px; |
192 | -} |
193 | -#screenshots-controls .next span, #screenshots-controls .prev span { |
194 | - position:absolute; |
195 | - left: -9999em; |
196 | - height: 0; |
197 | - width: 0; |
198 | -} |
199 | -#screenshots-controls .prev { |
200 | - background-position:0 0; |
201 | -} |
202 | -#screenshots-controls .next { |
203 | - background-position: -32px 0; |
204 | -} |
205 | -#screenshots-controls .prev:hover, #screenshots-controls .prev:focus, |
206 | -#screenshots-controls .next:hover, #screenshots-controls .next:focus { |
207 | - outline: none; |
208 | +div.screenshot .carousel .pagination { |
209 | + bottom: 0; |
210 | +} |
211 | |
212 | .top-rated-stars { |
213 | position: relative; |
214 | |
215 | === modified file 'src/webcatalog/static/js/carousel.js' |
216 | --- src/webcatalog/static/js/carousel.js 2012-03-08 18:00:09 +0000 |
217 | +++ src/webcatalog/static/js/carousel.js 2012-04-18 22:40:24 +0000 |
218 | @@ -87,16 +87,21 @@ |
219 | this.gotoSlide(parseInt(targetClass.replace("p-", ""), 10)); |
220 | } |
221 | }, this); |
222 | + // Center pagination |
223 | + olNode.setStyle('left', parseInt(( |
224 | + parseInt(olNode.get('parentNode').getComputedStyle('width')) - |
225 | + parseInt(olNode.getComputedStyle('width'))) / 2) + 'px'); |
226 | }, |
227 | generateControls: function() { |
228 | - var prev = Y.Node.create('<a href="#" class="prev"><span>Previous Slide</span></a>'), |
229 | - next = Y.Node.create('<a href="#" class="next"><span>Next Slide</span></a>'), |
230 | - controlsContainer = this.get("controlsContainer"); |
231 | - |
232 | - next.on("click", this.next, this); |
233 | - prev.on("click", this.prev, this); |
234 | - controlsContainer.appendChild(prev); |
235 | - controlsContainer.appendChild(next); |
236 | + var controlsContainer = this.get("controlsContainer"); |
237 | + if (controlsContainer) { |
238 | + var prev = Y.Node.create('<a href="#" class="prev"><span>Previous Slide</span></a>'), |
239 | + next = Y.Node.create('<a href="#" class="next"><span>Next Slide</span></a>'); |
240 | + next.on("click", this.next, this); |
241 | + prev.on("click", this.prev, this); |
242 | + controlsContainer.appendChild(prev); |
243 | + controlsContainer.appendChild(next); |
244 | + } |
245 | }, |
246 | advance: function(e, val){ |
247 | if (e) { |
248 | @@ -171,11 +176,7 @@ |
249 | }, |
250 | controlsContainer: { |
251 | setter: function(sel) { |
252 | - var n = Y.one(sel); |
253 | - if (!n) { |
254 | - Y.log('UWC:Carousel - invalid selector provided: ' + sel); |
255 | - } |
256 | - return n; |
257 | + return Y.one(sel); |
258 | } |
259 | }, |
260 | slideAnimDuration: { value: 1 }, |
261 | |
262 | === added file 'src/webcatalog/static/light/images/bullet.png' |
263 | Binary files src/webcatalog/static/light/images/bullet.png 1970-01-01 00:00:00 +0000 and src/webcatalog/static/light/images/bullet.png 2012-04-18 22:40:24 +0000 differ |
264 | === modified file 'src/webcatalog/templates/webcatalog/application_detail.html' |
265 | --- src/webcatalog/templates/webcatalog/application_detail.html 2012-04-05 01:09:37 +0000 |
266 | +++ src/webcatalog/templates/webcatalog/application_detail.html 2012-04-18 22:40:24 +0000 |
267 | @@ -83,11 +83,7 @@ |
268 | </div> |
269 | <div class="description"> |
270 | <div class="screenshot"> |
271 | - {% if application.screenshots %} |
272 | {% include "webcatalog/screenshot_widget.html" %} |
273 | - {% else %} |
274 | - <img src="http://screenshots.ubuntu.com/thumbnail-with-version/{{ application.package_name }}/ignored" /> |
275 | - {% endif %} |
276 | </div> |
277 | <p>{{ application.description|htmlize_package_description }}</p> |
278 | <div class="install-button">{% install_options application %}</div> |
279 | |
280 | === added file 'src/webcatalog/templates/webcatalog/application_screenshots.html' |
281 | --- src/webcatalog/templates/webcatalog/application_screenshots.html 1970-01-01 00:00:00 +0000 |
282 | +++ src/webcatalog/templates/webcatalog/application_screenshots.html 2012-04-18 22:40:24 +0000 |
283 | @@ -0,0 +1,25 @@ |
284 | +{% extends "webcatalog/base.html" %} |
285 | +{% load i18n %} |
286 | +{% load webcatalog %} |
287 | + |
288 | +{% block title %}Screenshots for {{ application.name }}{% endblock %} |
289 | +{% block header %}Screenshots for for {{ application.name }}{% endblock %} |
290 | + |
291 | +{% block content %} |
292 | + {% include "webcatalog/breadcrumbs_snippet.html" %} |
293 | + <div id="sc-mockup"> |
294 | + <div class="header"> |
295 | + {% rating_summary application.ratings_average 'large' application.ratings_total %} |
296 | + <img class="icon64" src="{{ application.icon_url_or_default }}"/> |
297 | + <h2>{{ application.name }}</h2> |
298 | + <p>{{ application.comment }}</p> |
299 | + </div> |
300 | + <ul> |
301 | + {% for screenshot in screenshots %} |
302 | + <li class="screenshot"> |
303 | + <img src="{{ screenshot }}"> |
304 | + </li> |
305 | + {% endfor %} |
306 | + </ul> |
307 | + </div> |
308 | +{% endblock %} |
309 | |
310 | === modified file 'src/webcatalog/templates/webcatalog/screenshot_widget.html' |
311 | --- src/webcatalog/templates/webcatalog/screenshot_widget.html 2012-03-20 10:04:41 +0000 |
312 | +++ src/webcatalog/templates/webcatalog/screenshot_widget.html 2012-04-18 22:40:24 +0000 |
313 | @@ -1,28 +1,72 @@ |
314 | -{% if application.screenshots|length_is:1 %} |
315 | - <img src="{{ application.screenshots.0 }}" /> |
316 | +{% load url from future %} |
317 | +<div id="screenshots_placeholder"> |
318 | +{% if application.screenshots|length_is:0 %} |
319 | + <img src="http://screenshots.ubuntu.com/thumbnail-with-version/{{ application.package_name }}/ignored" /> |
320 | {% else %} |
321 | - <div class="carousel-wrapper"> |
322 | - <div id="screenshots-carousel" class="carousel"> |
323 | - <ol class="carouselol"> |
324 | - {% for screenshot in application.screenshots %} |
325 | - <li class="slide{% if forloop.counter > 1 %} disabled{% endif %}"> |
326 | - <img src="{{ screenshot }}" /> |
327 | - </li> |
328 | - {% endfor %} |
329 | - </ol> |
330 | + <div class="carousel-wrapper"> |
331 | + <div id="screenshots-carousel" class="carousel"> |
332 | + <ol class="carouselol">{% for screenshot in application.screenshots %} |
333 | + <li class="slide"><img src="{{ screenshot }}" /></li>{% endfor %} |
334 | + </ol> |
335 | + </div> |
336 | </div> |
337 | - </div> |
338 | - <div id="screenshots-controls"></div> |
339 | +{% endif %} |
340 | +</div> |
341 | + |
342 | +{% comment %} |
343 | +If there are multiple screenshots, display a carousel. If there are none, |
344 | +we could retrieve multiple screenshots via AJAX. So only skip this section |
345 | +if the app has exactly 1 screenshot. |
346 | +{% endcomment %} |
347 | +{% if not application.screenshots|length_is:1 %} |
348 | <script type="text/javascript"> |
349 | - YUI({combine: true, comboBase: '{% url wc-combo %}?', root: 'yui/3.4.0/build/'}).use('uwc-carousel', function(Y) { |
350 | - var caro = new Y.uwc.Carousel({ |
351 | - nodeContainer: "#screenshots-carousel", |
352 | - controlsContainer: "#screenshots-controls", |
353 | - containerHeight: 170, |
354 | - containerWidth: 225, |
355 | - autoPlay: true |
356 | - }); |
357 | - Y.all('.slide').removeClass('disabled'); |
358 | - }); |
359 | + YUI({combine: true, comboBase: '{% url 'wc-combo' %}?', root: 'yui/3.4.0/build/'}).use('io-base', 'uwc-carousel', function(Y) { |
360 | + |
361 | + function setup_carousel() { |
362 | + var caro = new Y.uwc.Carousel({ |
363 | + nodeContainer: "#screenshots-carousel", |
364 | + controlsContainer: "#nocontrols", |
365 | + autoPlay: false, |
366 | + slideAnimDuration: 0.6 |
367 | + }); |
368 | + Y.all('.slide').removeClass('disabled'); |
369 | + } |
370 | + |
371 | +{% if application.screenshots|length_is:0 %} |
372 | + var uri = "{% url 'wc-package-screenshots' application.package_name %}"; |
373 | + function complete(id, obj) { |
374 | + var id = id; // Transaction ID. |
375 | + var json = JSON.parse(obj.responseText); |
376 | + if (json.length > 1) { |
377 | + var content_div = document.createElement('div'); |
378 | + content_div.className = 'carousel-wrapper'; |
379 | + var carousel_div = document.createElement('div'); |
380 | + carousel_div.id = 'screenshots-carousel'; |
381 | + carousel_div.className = 'carousel'; |
382 | + content_div.appendChild(carousel_div) |
383 | + var ol = document.createElement('ol') |
384 | + ol.className = 'carouselol'; |
385 | + carousel_div.appendChild(ol); |
386 | + for (var i in json) { |
387 | + var url = json[i]; |
388 | + var li = document.createElement('li'); |
389 | + li.className = 'slide'; |
390 | + ol.appendChild(li); |
391 | + var img = document.createElement('img'); |
392 | + img.src = url; |
393 | + li.appendChild(img); |
394 | + } |
395 | + var screenshots_div = Y.one('#screenshots_placeholder'); |
396 | + screenshots_div.get('childNodes').remove(); |
397 | + screenshots_div.appendChild(content_div); |
398 | + setup_carousel(); |
399 | + } |
400 | + }; |
401 | + Y.on('io:complete', complete, Y); |
402 | + var request = Y.io(uri); |
403 | +{% else %} |
404 | + setup_carousel(); |
405 | +{% endif %} |
406 | + }); |
407 | </script> |
408 | {% endif %} |
409 | |
410 | === modified file 'src/webcatalog/tests/test_commands.py' |
411 | --- src/webcatalog/tests/test_commands.py 2012-04-17 17:09:03 +0000 |
412 | +++ src/webcatalog/tests/test_commands.py 2012-04-18 22:40:24 +0000 |
413 | @@ -149,11 +149,6 @@ |
414 | 'DISK_APT_CACHE_LOCATION', self.tmp_apt_cache) |
415 | self.patch_cache_setting.start() |
416 | |
417 | - self.patch_get_screenshot_urls = patch( |
418 | - 'webcatalog.management.commands.import_app_install_data.' |
419 | - 'Command.get_screenshot_urls_from_server') |
420 | - self.patch_get_screenshot_urls.start() |
421 | - |
422 | if self.use_mock_apt_cache: |
423 | # The apt.Cache class is patched to return our own mock which |
424 | # will be subscriptable with a pre-set dict with mock ock apt |
425 | @@ -165,8 +160,6 @@ |
426 | |
427 | def tearDown(self): |
428 | self.patch_cache_setting.stop() |
429 | - if hasattr(self, 'patch_get_screenshot_urls'): |
430 | - self.patch_get_screenshot_urls.stop() |
431 | shutil.rmtree(self.tmp_apt_cache) |
432 | if self.use_mock_apt_cache: |
433 | self.patch_cache.stop() |
434 | @@ -290,29 +283,6 @@ |
435 | self.assertTrue(firefox.icon.size > 1) |
436 | self.assertTrue(scribus.icon.size > 1) |
437 | |
438 | - def test_screenshots_added_to_the_model(self): |
439 | - fn = 'webcatalog.management.commands.import_app_install_data.urllib' |
440 | - self.patch_get_screenshot_urls.stop() |
441 | - del self.patch_get_screenshot_urls |
442 | - with patch(fn) as mock_urllib: |
443 | - mock_response = Mock() |
444 | - mock_response.getcode.return_value = 200 |
445 | - mock_response.read.return_value = json.dumps( |
446 | - {'screenshots': [ |
447 | - {'large_image_url': 'http://example.com/1.png'} |
448 | - ]} |
449 | - ) |
450 | - mock_urllib.urlopen.return_value = mock_response |
451 | - |
452 | - call_command('import_app_install_data', 'natty', |
453 | - local_app_install_data=self.deb_location, |
454 | - local_app_install_data_partner=self.deb_location, |
455 | - verbosity=0) |
456 | - |
457 | - scribus = Application.objects.get(package_name='scribus') |
458 | - |
459 | - self.assertEqual(scribus.screenshots, ["http://example.com/1.png"]) |
460 | - |
461 | def get_package_uri_for_series(self): |
462 | uri = import_app_install_data.Command().get_package_uri_for_series( |
463 | 'app-install-data', 'natty') |
464 | |
465 | === modified file 'src/webcatalog/tests/test_utilities.py' |
466 | --- src/webcatalog/tests/test_utilities.py 2012-03-05 08:11:24 +0000 |
467 | +++ src/webcatalog/tests/test_utilities.py 2012-04-18 22:40:24 +0000 |
468 | @@ -20,8 +20,10 @@ |
469 | __all__ = [ |
470 | 'CreatePNGFromFileTestCase', |
471 | 'IdentityProviderTestCase', |
472 | + 'ScreenshotGetterTestCase', |
473 | ] |
474 | |
475 | +import json |
476 | import os |
477 | |
478 | from webcatalog.tests.factory import TestCaseWithFactory |
479 | @@ -124,3 +126,35 @@ |
480 | u'displayname', u'name', u'unverified_emails', u'verified_emails', |
481 | u'consumer_secret', u'token', u'openid_identifier', |
482 | u'consumer_key', u'token_secret']), set(data)) |
483 | + |
484 | + |
485 | +class ScreenshotGetterTestCase(TestCase): |
486 | + @patch('webcatalog.utilities.urllib.urlopen') |
487 | + def test_valid_response_returns_list_of_urls(self, mock_urlopen): |
488 | + response = json.dumps({'package': 'digikam', 'screenshots': [ |
489 | + {'version': '1.0', 'large_image_url': 'http://example.com/foo'}, |
490 | + {'version': '1.1', 'large_image_url': 'http://example.com/bar'}, |
491 | + ]}) |
492 | + mock_urlopen.return_value.getcode.return_value = 200 |
493 | + mock_urlopen.return_value.read.return_value = response |
494 | + expected = ['http://example.com/foo', 'http://example.com/bar'] |
495 | + self.assertEqual(expected, |
496 | + WebServices().get_screenshots_for_package('digikam')) |
497 | + |
498 | + @patch('webcatalog.utilities.urllib.urlopen') |
499 | + def test_not_found_response_returns_empty_list(self, mock_urlopen): |
500 | + mock_urlopen.return_value.getcode.return_value = 404 |
501 | + self.assertEqual([], WebServices().get_screenshots_for_package('foo')) |
502 | + |
503 | + @patch('webcatalog.utilities.urllib.urlopen') |
504 | + def test_invalid_response_returns_empty_list(self, mock_urlopen): |
505 | + mock_urlopen.return_value.getcode.return_value = 200 |
506 | + mock_urlopen.return_value.read.return_value = 'invalid json' |
507 | + self.assertEqual([], WebServices().get_screenshots_for_package('foo')) |
508 | + |
509 | + @patch('webcatalog.utilities.urllib.urlopen') |
510 | + def test_unexpected_response_returns_empty_list(self, mock_urlopen): |
511 | + response = json.dumps({'package': 'The Spanish Inquisition'}) |
512 | + mock_urlopen.return_value.getcode.return_value = 200 |
513 | + mock_urlopen.return_value.read.return_value = response |
514 | + self.assertEqual([], WebServices().get_screenshots_for_package('foo')) |
515 | |
516 | === modified file 'src/webcatalog/tests/test_views.py' |
517 | --- src/webcatalog/tests/test_views.py 2012-04-13 21:06:13 +0000 |
518 | +++ src/webcatalog/tests/test_views.py 2012-04-18 22:40:24 +0000 |
519 | @@ -22,12 +22,14 @@ |
520 | with_statement, |
521 | ) |
522 | |
523 | +import json |
524 | import re |
525 | |
526 | from decimal import Decimal |
527 | |
528 | from django.conf import settings |
529 | from django.core import mail |
530 | +from django.core.cache import cache |
531 | from django.core.urlresolvers import reverse |
532 | from django.test import TestCase |
533 | |
534 | @@ -43,6 +45,7 @@ |
535 | 'ApplicationDetailNoSeriesTestCase', |
536 | 'ApplicationDetailTestCase', |
537 | 'ApplicationReviewsTestCase', |
538 | + 'ApplicationScreenshotsTestCase', |
539 | 'IndexTestCase', |
540 | 'TermsOfServiceTestCase', |
541 | 'OverviewTestCase', |
542 | @@ -59,6 +62,9 @@ |
543 | |
544 | |
545 | class ApplicationDetailTestCase(TestCaseWithFactory): |
546 | + def setUp(self): |
547 | + super(ApplicationDetailTestCase, self).setUp() |
548 | + cache.clear() |
549 | |
550 | def get_app_details_url(self, app): |
551 | return reverse('wc-package-detail', |
552 | @@ -366,6 +372,10 @@ |
553 | |
554 | |
555 | class SearchTestCase(TestCaseWithFactory): |
556 | + def setUp(self): |
557 | + super(SearchTestCase, self).setUp() |
558 | + cache.clear() |
559 | + |
560 | def test_no_search_retrieves_no_apps(self): |
561 | # Ensure there's some data in the DB |
562 | distro = self.factory.make_distroseries(code_name='lucid') |
563 | @@ -474,9 +484,9 @@ |
564 | |
565 | def test_response_shows_number_of_apps_found(self): |
566 | distro = self.factory.make_distroseries(code_name='lucid') |
567 | - app1 = self.factory.make_application(package_name='foo', |
568 | + self.factory.make_application(package_name='foo', |
569 | distroseries=distro) |
570 | - app2 = self.factory.make_application(package_name='foobar', |
571 | + self.factory.make_application(package_name='foobar', |
572 | distroseries=distro) |
573 | url = reverse('wc-search', args=[distro.code_name]) |
574 | |
575 | @@ -536,6 +546,10 @@ |
576 | |
577 | |
578 | class IndexTestCase(TestCaseWithFactory): |
579 | + def setUp(self): |
580 | + super(IndexTestCase, self).setUp() |
581 | + cache.clear() |
582 | + |
583 | def test_index_contains_links_to_departments(self): |
584 | dept = self.factory.make_department('foo') |
585 | |
586 | @@ -671,6 +685,12 @@ |
587 | self.assertContains(response, expected, 2) |
588 | self.assertNotContains(response, app_not_included.package_name) |
589 | |
590 | + def test_includes_cache_headers(self): |
591 | + with patch_settings(CACHE_MIDDLEWARE_SECONDS=600): |
592 | + response = self.client.get(reverse('wc-index')) |
593 | + |
594 | + self.assertEqual('max-age=600', response['cache-control']) |
595 | + |
596 | |
597 | class TermsOfServiceTestCase(TestCase): |
598 | |
599 | @@ -686,6 +706,9 @@ |
600 | |
601 | |
602 | class OverviewTestCase(TestCaseWithFactory): |
603 | + def setUp(self): |
604 | + super(OverviewTestCase, self).setUp() |
605 | + cache.clear() |
606 | |
607 | def test_department_contains_links_to_subdepartments(self): |
608 | dept = self.factory.make_department('foo') |
609 | @@ -946,6 +969,70 @@ |
610 | self.assertContains(response, 'review_summary2') |
611 | |
612 | |
613 | +class ApplicationScreenshotsTestCase(TestCaseWithFactory): |
614 | + def setUp(self): |
615 | + super(ApplicationScreenshotsTestCase, self).setUp() |
616 | + self.app = self.factory.make_application() |
617 | + self.expected = ['http://example.com/foo', 'http://example.com/bar'] |
618 | + |
619 | + def test_only_valid_apps(self): |
620 | + response = self.client.get(reverse('wc-package-screenshots', |
621 | + args=['doesntexist'])) |
622 | + |
623 | + self.assertEqual(404, response.status_code) |
624 | + |
625 | + @patch('webcatalog.utilities.urllib.urlopen') |
626 | + def test_uncached_calls_to_rnr_api(self, mock_urlopen): |
627 | + self.client.get(reverse('wc-package-screenshots', |
628 | + args=[self.app.package_name])) |
629 | + |
630 | + self.assertEqual(1, mock_urlopen.call_count) |
631 | + |
632 | + @patch('webcatalog.utilities.urllib.urlopen') |
633 | + def test_second_call_cached(self, mock_urlopen): |
634 | + for count in range(2): |
635 | + self.client.get(reverse('wc-package-screenshots', |
636 | + args=[self.app.package_name])) |
637 | + |
638 | + self.assertEqual(1, mock_urlopen.call_count) |
639 | + |
640 | + @patch('webcatalog.utilities.WebServices.get_screenshots_for_package') |
641 | + def test_renders_screenshots(self, mock_get_screenshots): |
642 | + mock_get_screenshots.return_value = self.expected |
643 | + |
644 | + response = self.client.get(reverse('wc-package-screenshots', |
645 | + args=[self.app.package_name])) |
646 | + |
647 | + self.assertTemplateUsed( |
648 | + response, 'webcatalog/application_screenshots.html') |
649 | + for url in self.expected: |
650 | + self.assertContains(response, url) |
651 | + |
652 | + @patch('webcatalog.utilities.WebServices.get_screenshots_for_package') |
653 | + def test_renders_json_for_json_request(self, mock_get_screenshots): |
654 | + mock_get_screenshots.return_value = self.expected |
655 | + |
656 | + response = self.client.get(reverse('wc-package-screenshots', |
657 | + args=[self.app.package_name]), |
658 | + HTTP_X_REQUESTED_WITH='XMLHttpRequest') |
659 | + |
660 | + self.assertTemplateNotUsed( |
661 | + response, 'webcatalog/application_review_list.html') |
662 | + self.assertEqual('application/json', response['content-type']) |
663 | + self.assertEqual(json.dumps(self.expected), response.content) |
664 | + |
665 | + @patch('webcatalog.utilities.WebServices.get_screenshots_for_package') |
666 | + def test_has_caching_headers(self, mock_get_screenshots): |
667 | + mock_get_screenshots.return_value = self.expected |
668 | + |
669 | + with patch_settings(CACHE_MIDDLEWARE_SECONDS=600): |
670 | + response = self.client.get(reverse('wc-package-screenshots', |
671 | + args=[self.app.package_name]), |
672 | + HTTP_X_REQUESTED_WITH='XMLHttpRequest') |
673 | + |
674 | + self.assertEqual('max-age=600', response['cache-control']) |
675 | + |
676 | + |
677 | class ComboViewTestCase(TestCase): |
678 | """Tests for ComboView.""" |
679 | |
680 | |
681 | === modified file 'src/webcatalog/urls.py' |
682 | --- src/webcatalog/urls.py 2012-03-20 11:12:02 +0000 |
683 | +++ src/webcatalog/urls.py 2012-04-18 22:40:24 +0000 |
684 | @@ -36,6 +36,8 @@ |
685 | 'department_overview', name='wc-department'), |
686 | url(r'^department/(?P<dept_slug_or_id>[\w\d-]+)/$', 'department_overview', |
687 | name='wc-department'), |
688 | + url(r'^applications/screenshots/(?P<package_name>[-.+:\w]+)/$', |
689 | + 'application_screenshots', name="wc-package-screenshots"), |
690 | url(r'^applications/(?P<distro>[-.+\w]+)/(?P<package_name>[-.+:\w]+)/$', |
691 | 'application_detail', name="wc-package-detail"), |
692 | # The following URL redirects to the series-specific URL above. |
693 | |
694 | === modified file 'src/webcatalog/utilities.py' |
695 | --- src/webcatalog/utilities.py 2012-03-19 20:43:17 +0000 |
696 | +++ src/webcatalog/utilities.py 2012-04-18 22:40:24 +0000 |
697 | @@ -28,10 +28,12 @@ |
698 | 'WebServiceError', |
699 | ] |
700 | |
701 | +import json |
702 | import logging |
703 | import math |
704 | import re |
705 | import urllib |
706 | +from urlparse import urljoin |
707 | |
708 | from django.conf import settings |
709 | from django.core.cache import cache |
710 | @@ -77,6 +79,26 @@ |
711 | cache.set(key, fresh_reviews, settings.REVIEWS_CACHE_TIMEOUT) |
712 | return fresh_reviews |
713 | |
714 | + def get_screenshots_for_package(self, package_name): |
715 | + key = 'get_screenshots_for_package-{0}'.format( |
716 | + package_name) |
717 | + cached_screenshots = cache.get(key) |
718 | + if cached_screenshots is not None: |
719 | + return cached_screenshots |
720 | + url = urljoin(settings.SCREENSHOTS_BASE_URL, |
721 | + '/json/package/{0}'.format(package_name)) |
722 | + fresh_screenshots = [] |
723 | + try: |
724 | + response = urllib.urlopen(url) |
725 | + if response.getcode() == 200: |
726 | + data = json.loads(response.read()) |
727 | + fresh_screenshots = [entry['large_image_url'] |
728 | + for entry in data.get('screenshots', [])] |
729 | + except (IOError, ValueError, TypeError): |
730 | + pass |
731 | + cache.set(key, fresh_screenshots, settings.REVIEWS_CACHE_TIMEOUT) |
732 | + return fresh_screenshots |
733 | + |
734 | def _fake_validate_token(self, token, openid_identifier, signature): |
735 | """ This is a version of validate_token that gets the |
736 | token_secret, consumer_secret from the plaintext signature. |
737 | |
738 | === modified file 'src/webcatalog/views.py' |
739 | --- src/webcatalog/views.py 2012-04-13 21:17:09 +0000 |
740 | +++ src/webcatalog/views.py 2012-04-18 22:40:24 +0000 |
741 | @@ -210,6 +210,19 @@ |
742 | return render_to_response(template, RequestContext(request, context)) |
743 | |
744 | |
745 | +def application_screenshots(request, package_name): |
746 | + app = get_object_or_404(Application, package_name=package_name) |
747 | + screenshots = WebServices().get_screenshots_for_package(package_name) |
748 | + if request.is_ajax(): |
749 | + return HttpResponse(json.dumps(screenshots), |
750 | + mimetype='application/json') |
751 | + else: |
752 | + context = dict(application=app, screenshots=screenshots, |
753 | + breadcrumbs=app.crumbs()) |
754 | + return render_to_response('webcatalog/application_screenshots.html', |
755 | + RequestContext(request, context)) |
756 | + |
757 | + |
758 | def combo_view(request): |
759 | """Handle a request for combining a set of files.""" |
760 | fnames = parse_qs(request.META.get("QUERY_STRING", "")) |
Nice work Anthony.