Merge lp:~elachuni/ubuntu-webcatalog/lazy-screenshots into lp:ubuntu-webcatalog

Proposed by Anthony Lenton
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
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_app_install_data is run.

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_app_install_data is run.

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.ubuntu.com is included statically. Screenshots are checked via AJAX. If one or more screenshots are retrieved, revert to cases a) or b).

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.

To post a comment you must log in.
Revision history for this message
Danny Tamez (zematynnad) wrote :

Nice work Anthony.

review: Approve

Preview Diff

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

Subscribers

People subscribed via source and target branches