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
=== modified file 'django_project/config/main.cfg'
--- django_project/config/main.cfg 2012-03-27 20:43:10 +0000
+++ django_project/config/main.cfg 2012-04-18 22:40:24 +0000
@@ -27,12 +27,14 @@
27 pgtools27 pgtools
28login_url = /openid/login/28login_url = /openid/login/
29managers = %(admins)s29managers = %(admins)s
30middleware_classes = django.middleware.common.CommonMiddleware30middleware_classes = django.middleware.cache.UpdateCacheMiddleware
31 django.middleware.common.CommonMiddleware
31 django.contrib.sessions.middleware.SessionMiddleware32 django.contrib.sessions.middleware.SessionMiddleware
32 django.middleware.csrf.CsrfViewMiddleware33 django.middleware.csrf.CsrfViewMiddleware
33 django.contrib.auth.middleware.AuthenticationMiddleware34 django.contrib.auth.middleware.AuthenticationMiddleware
34 django.contrib.messages.middleware.MessageMiddleware35 django.contrib.messages.middleware.MessageMiddleware
35 webcatalog.middleware.exception.LogExceptionMiddleware36 webcatalog.middleware.exception.LogExceptionMiddleware
37 django.middleware.cache.FetchFromCacheMiddleware
36fixture_dirs =38fixture_dirs =
3739
38secret_key = eepu9Av5ixage9ahhodovahfaiFoorodahf6keip3eichaeW9f40secret_key = eepu9Av5ixage9ahhodovahfaiFoorodahf6keip3eichaeW9f
3941
=== modified file 'src/webcatalog/admin.py'
--- src/webcatalog/admin.py 2012-03-20 22:23:31 +0000
+++ src/webcatalog/admin.py 2012-04-18 22:40:24 +0000
@@ -24,6 +24,7 @@
24from django.contrib import admin24from django.contrib import admin
25from webcatalog.models import (25from webcatalog.models import (
26 Application,26 Application,
27 ApplicationMedia,
27 Department,28 Department,
28 DistroSeries,29 DistroSeries,
29 Exhibit,30 Exhibit,
@@ -57,7 +58,18 @@
57class DepartmentAdmin(admin.ModelAdmin):58class DepartmentAdmin(admin.ModelAdmin):
58 prepopulated_fields = {"slug": ("name",)}59 prepopulated_fields = {"slug": ("name",)}
5960
61
62class ApplicationMediaAdmin(admin.ModelAdmin):
63 search_fields = ('application__package_name',)
64 list_filter = ('application__distroseries',)
65 list_display = ('__unicode__', 'distroseries', 'url')
66 raw_id_fields = ('application',)
67
68 def distroseries(self, obj):
69 return obj.application.distroseries
70
60admin.site.register(Application, ApplicationAdmin)71admin.site.register(Application, ApplicationAdmin)
72admin.site.register(ApplicationMedia, ApplicationMediaAdmin)
61admin.site.register(Department, DepartmentAdmin)73admin.site.register(Department, DepartmentAdmin)
62admin.site.register(DistroSeries)74admin.site.register(DistroSeries)
63admin.site.register(Exhibit, ExhibitAdmin)75admin.site.register(Exhibit, ExhibitAdmin)
6476
=== modified file 'src/webcatalog/api/urls.py'
--- src/webcatalog/api/urls.py 2012-01-06 17:54:47 +0000
+++ src/webcatalog/api/urls.py 2012-04-18 22:40:24 +0000
@@ -15,6 +15,7 @@
15# along with this program. If not, see <http://www.gnu.org/licenses/>.15# along with this program. If not, see <http://www.gnu.org/licenses/>.
1616
17from django.conf.urls.defaults import url, patterns17from django.conf.urls.defaults import url, patterns
18from django.views.decorators.cache import never_cache
1819
19from piston.resource import Resource20from piston.resource import Resource
20from webcatalog.api.handlers import (21from webcatalog.api.handlers import (
@@ -34,13 +35,13 @@
34 super(CSRFExemptResource, self).__init__(handler, authentication)35 super(CSRFExemptResource, self).__init__(handler, authentication)
35 self.csrf_exempt = True36 self.csrf_exempt = True
3637
37server_status_resource = Resource(handler=ServerStatusHandler)38server_status_resource = never_cache(Resource(handler=ServerStatusHandler))
38list_machines_resource = Resource(handler=ListMachinesHandler,39list_machines_resource = never_cache(Resource(handler=ListMachinesHandler,
39 authentication=auth)40 authentication=auth))
40machine_resource = CSRFExemptResource(handler=MachineHandler,41machine_resource = never_cache(CSRFExemptResource(handler=MachineHandler,
41 authentication=auth)42 authentication=auth))
42packages_resource = CSRFExemptResource(handler=PackagesHandler,43packages_resource = never_cache(CSRFExemptResource(handler=PackagesHandler,
43 authentication=auth)44 authentication=auth))
4445
45urlpatterns = patterns('',46urlpatterns = patterns('',
46 # get status of the service (usually just "ok", might be "read-only")47 # get status of the service (usually just "ok", might be "read-only")
4748
=== modified file 'src/webcatalog/management/commands/import_app_install_data.py'
--- src/webcatalog/management/commands/import_app_install_data.py 2012-04-03 17:45:07 +0000
+++ src/webcatalog/management/commands/import_app_install_data.py 2012-04-18 22:40:24 +0000
@@ -22,7 +22,6 @@
22 with_statement,22 with_statement,
23 )23 )
2424
25import json
26import os25import os
27import re26import re
28import shutil27import shutil
@@ -32,7 +31,6 @@
32from glob import iglob31from glob import iglob
33from optparse import make_option32from optparse import make_option
34from tempfile import NamedTemporaryFile33from tempfile import NamedTemporaryFile
35from urlparse import urljoin
3634
37import apt35import apt
38from apt.cache import FetchFailedException36from apt.cache import FetchFailedException
@@ -166,27 +164,6 @@
166164
167 shutil.rmtree(data_dir)165 shutil.rmtree(data_dir)
168166
169 def get_screenshot_urls_from_server(self, app):
170 url = urljoin(settings.SCREENSHOTS_BASE_URL,
171 '/json/package/{0}'.format(app.package_name))
172 try:
173 response = urllib.urlopen(url)
174 if response.getcode() == 200:
175 data = json.loads(response.read())
176 for entry in data['screenshots']:
177 screenshot_url = entry['large_image_url']
178
179 self.output("Adding {0} to {1}\n".format(
180 screenshot_url,
181 app.package_name), 2)
182 app.applicationmedia_set.get_or_create(
183 media_type='screenshot',
184 url=screenshot_url,
185 )
186 app.save()
187 except (IOError, TypeError):
188 pass
189
190 def get_package_data_for_series(self, package_name, distroseries,167 def get_package_data_for_series(self, package_name, distroseries,
191 working_dir):168 working_dir):
192 install_data_url = self.get_package_uri_for_series(169 install_data_url = self.get_package_uri_for_series(
@@ -216,7 +193,6 @@
216 app.distroseries = distroseries193 app.distroseries = distroseries
217 app.save()194 app.save()
218 app.update_departments()195 app.update_departments()
219 self.get_screenshot_urls_from_server(app)
220 self.add_icon_to_app(app, icon_dir)196 self.add_icon_to_app(app, icon_dir)
221 if self.verbosity > 1:197 if self.verbosity > 1:
222 self.output(u"{0} created.\n".format(app.name).encode(198 self.output(u"{0} created.\n".format(app.name).encode(
@@ -338,7 +314,6 @@
338 version=version,314 version=version,
339 comment=app_comment,315 comment=app_comment,
340 )316 )
341 self.get_screenshot_urls_from_server(app)
342 self.fetch_icon_from_extras(app, candidate)317 self.fetch_icon_from_extras(app, candidate)
343 if self.verbosity == 1:318 if self.verbosity == 1:
344 self.output("\n", 1)319 self.output("\n", 1)
345320
=== modified file 'src/webcatalog/static/css/carousel.css'
--- src/webcatalog/static/css/carousel.css 2012-04-12 09:55:28 +0000
+++ src/webcatalog/static/css/carousel.css 2012-04-18 22:40:24 +0000
@@ -16,7 +16,6 @@
16 z-index: 20;16 z-index: 20;
17 left: 50%;17 left: 50%;
18 bottom: 5px;18 bottom: 5px;
19 margin-left: -35px;
20}19}
21.carousel .pagination li {20.carousel .pagination li {
22 float: left;21 float: left;
@@ -202,40 +201,24 @@
202}201}
203202
204/* Screenshots carousel */203/* Screenshots carousel */
205.screenshot .carousel-wrapper {204div.screenshot .carousel-wrapper {
206 height: 190px;205 height: 190px;
207 width: 233px;206 width: 233px;
208}207}
209.screenshot .carousel .pagination li a.active {208div.screenshot .carousel-wrapper .slide {
209 margin: 0;
210 width: 233px;
211}
212div.screenshot .carousel-wrapper .slide img {
213 margin: 0 auto;
214 display: block
215}
216div.screenshot .carousel .pagination li a.active {
210 background-color: #DD4814;217 background-color: #DD4814;
211}218}
212#screenshots-controls {219div.screenshot .carousel .pagination {
213 width: 70px;220 bottom: 0;
214 height: 32px;221}
215 margin-left: 94px;
216}
217#screenshots-controls .next, #screenshots-controls .prev {
218 background: url(/assets/images/arrow-sliders.png) no-repeat 0 0;
219 float: left;
220 z-index: 20;
221 width: 32px;
222 height: 29px;
223}
224#screenshots-controls .next span, #screenshots-controls .prev span {
225 position:absolute;
226 left: -9999em;
227 height: 0;
228 width: 0;
229}
230#screenshots-controls .prev {
231 background-position:0 0;
232}
233#screenshots-controls .next {
234 background-position: -32px 0;
235}
236#screenshots-controls .prev:hover, #screenshots-controls .prev:focus,
237#screenshots-controls .next:hover, #screenshots-controls .next:focus {
238 outline: none;
239222
240.top-rated-stars {223.top-rated-stars {
241 position: relative;224 position: relative;
242225
=== modified file 'src/webcatalog/static/js/carousel.js'
--- src/webcatalog/static/js/carousel.js 2012-03-08 18:00:09 +0000
+++ src/webcatalog/static/js/carousel.js 2012-04-18 22:40:24 +0000
@@ -87,16 +87,21 @@
87 this.gotoSlide(parseInt(targetClass.replace("p-", ""), 10));87 this.gotoSlide(parseInt(targetClass.replace("p-", ""), 10));
88 }88 }
89 }, this);89 }, this);
90 // Center pagination
91 olNode.setStyle('left', parseInt((
92 parseInt(olNode.get('parentNode').getComputedStyle('width')) -
93 parseInt(olNode.getComputedStyle('width'))) / 2) + 'px');
90 },94 },
91 generateControls: function() {95 generateControls: function() {
92 var prev = Y.Node.create('<a href="#" class="prev"><span>Previous Slide</span></a>'),96 var controlsContainer = this.get("controlsContainer");
93 next = Y.Node.create('<a href="#" class="next"><span>Next Slide</span></a>'),97 if (controlsContainer) {
94 controlsContainer = this.get("controlsContainer");98 var prev = Y.Node.create('<a href="#" class="prev"><span>Previous Slide</span></a>'),
9599 next = Y.Node.create('<a href="#" class="next"><span>Next Slide</span></a>');
96 next.on("click", this.next, this);100 next.on("click", this.next, this);
97 prev.on("click", this.prev, this);101 prev.on("click", this.prev, this);
98 controlsContainer.appendChild(prev);102 controlsContainer.appendChild(prev);
99 controlsContainer.appendChild(next);103 controlsContainer.appendChild(next);
104 }
100 },105 },
101 advance: function(e, val){106 advance: function(e, val){
102 if (e) {107 if (e) {
@@ -171,11 +176,7 @@
171 },176 },
172 controlsContainer: {177 controlsContainer: {
173 setter: function(sel) {178 setter: function(sel) {
174 var n = Y.one(sel);179 return Y.one(sel);
175 if (!n) {
176 Y.log('UWC:Carousel - invalid selector provided: ' + sel);
177 }
178 return n;
179 }180 }
180 },181 },
181 slideAnimDuration: { value: 1 },182 slideAnimDuration: { value: 1 },
182183
=== added file 'src/webcatalog/static/light/images/bullet.png'
183Binary 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 differ184Binary 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
=== modified file 'src/webcatalog/templates/webcatalog/application_detail.html'
--- src/webcatalog/templates/webcatalog/application_detail.html 2012-04-05 01:09:37 +0000
+++ src/webcatalog/templates/webcatalog/application_detail.html 2012-04-18 22:40:24 +0000
@@ -83,11 +83,7 @@
83 </div>83 </div>
84 <div class="description">84 <div class="description">
85 <div class="screenshot">85 <div class="screenshot">
86 {% if application.screenshots %}
87 {% include "webcatalog/screenshot_widget.html" %}86 {% include "webcatalog/screenshot_widget.html" %}
88 {% else %}
89 <img src="http://screenshots.ubuntu.com/thumbnail-with-version/{{ application.package_name }}/ignored" />
90 {% endif %}
91 </div>87 </div>
92 <p>{{ application.description|htmlize_package_description }}</p>88 <p>{{ application.description|htmlize_package_description }}</p>
93 <div class="install-button">{% install_options application %}</div>89 <div class="install-button">{% install_options application %}</div>
9490
=== added file 'src/webcatalog/templates/webcatalog/application_screenshots.html'
--- src/webcatalog/templates/webcatalog/application_screenshots.html 1970-01-01 00:00:00 +0000
+++ src/webcatalog/templates/webcatalog/application_screenshots.html 2012-04-18 22:40:24 +0000
@@ -0,0 +1,25 @@
1{% extends "webcatalog/base.html" %}
2{% load i18n %}
3{% load webcatalog %}
4
5{% block title %}Screenshots for {{ application.name }}{% endblock %}
6{% block header %}Screenshots for for {{ application.name }}{% endblock %}
7
8{% block content %}
9 {% include "webcatalog/breadcrumbs_snippet.html" %}
10 <div id="sc-mockup">
11 <div class="header">
12 {% rating_summary application.ratings_average 'large' application.ratings_total %}
13 <img class="icon64" src="{{ application.icon_url_or_default }}"/>
14 <h2>{{ application.name }}</h2>
15 <p>{{ application.comment }}</p>
16 </div>
17 <ul>
18 {% for screenshot in screenshots %}
19 <li class="screenshot">
20 <img src="{{ screenshot }}">
21 </li>
22 {% endfor %}
23 </ul>
24 </div>
25{% endblock %}
026
=== modified file 'src/webcatalog/templates/webcatalog/screenshot_widget.html'
--- src/webcatalog/templates/webcatalog/screenshot_widget.html 2012-03-20 10:04:41 +0000
+++ src/webcatalog/templates/webcatalog/screenshot_widget.html 2012-04-18 22:40:24 +0000
@@ -1,28 +1,72 @@
1{% if application.screenshots|length_is:1 %}1{% load url from future %}
2 <img src="{{ application.screenshots.0 }}" />2<div id="screenshots_placeholder">
3{% if application.screenshots|length_is:0 %}
4 <img src="http://screenshots.ubuntu.com/thumbnail-with-version/{{ application.package_name }}/ignored" />
3{% else %}5{% else %}
4 <div class="carousel-wrapper">6 <div class="carousel-wrapper">
5 <div id="screenshots-carousel" class="carousel">7 <div id="screenshots-carousel" class="carousel">
6 <ol class="carouselol">8 <ol class="carouselol">{% for screenshot in application.screenshots %}
7 {% for screenshot in application.screenshots %}9 <li class="slide"><img src="{{ screenshot }}" /></li>{% endfor %}
8 <li class="slide{% if forloop.counter > 1 %} disabled{% endif %}">10 </ol>
9 <img src="{{ screenshot }}" />11 </div>
10 </li>
11 {% endfor %}
12 </ol>
13 </div>12 </div>
14 </div>13{% endif %}
15 <div id="screenshots-controls"></div>14</div>
15
16{% comment %}
17If there are multiple screenshots, display a carousel. If there are none,
18we could retrieve multiple screenshots via AJAX. So only skip this section
19if the app has exactly 1 screenshot.
20{% endcomment %}
21{% if not application.screenshots|length_is:1 %}
16 <script type="text/javascript">22 <script type="text/javascript">
17 YUI({combine: true, comboBase: '{% url wc-combo %}?', root: 'yui/3.4.0/build/'}).use('uwc-carousel', function(Y) {23 YUI({combine: true, comboBase: '{% url 'wc-combo' %}?', root: 'yui/3.4.0/build/'}).use('io-base', 'uwc-carousel', function(Y) {
18 var caro = new Y.uwc.Carousel({24
19 nodeContainer: "#screenshots-carousel",25 function setup_carousel() {
20 controlsContainer: "#screenshots-controls",26 var caro = new Y.uwc.Carousel({
21 containerHeight: 170,27 nodeContainer: "#screenshots-carousel",
22 containerWidth: 225,28 controlsContainer: "#nocontrols",
23 autoPlay: true29 autoPlay: false,
24 });30 slideAnimDuration: 0.6
25 Y.all('.slide').removeClass('disabled');31 });
26 });32 Y.all('.slide').removeClass('disabled');
33 }
34
35{% if application.screenshots|length_is:0 %}
36 var uri = "{% url 'wc-package-screenshots' application.package_name %}";
37 function complete(id, obj) {
38 var id = id; // Transaction ID.
39 var json = JSON.parse(obj.responseText);
40 if (json.length > 1) {
41 var content_div = document.createElement('div');
42 content_div.className = 'carousel-wrapper';
43 var carousel_div = document.createElement('div');
44 carousel_div.id = 'screenshots-carousel';
45 carousel_div.className = 'carousel';
46 content_div.appendChild(carousel_div)
47 var ol = document.createElement('ol')
48 ol.className = 'carouselol';
49 carousel_div.appendChild(ol);
50 for (var i in json) {
51 var url = json[i];
52 var li = document.createElement('li');
53 li.className = 'slide';
54 ol.appendChild(li);
55 var img = document.createElement('img');
56 img.src = url;
57 li.appendChild(img);
58 }
59 var screenshots_div = Y.one('#screenshots_placeholder');
60 screenshots_div.get('childNodes').remove();
61 screenshots_div.appendChild(content_div);
62 setup_carousel();
63 }
64 };
65 Y.on('io:complete', complete, Y);
66 var request = Y.io(uri);
67{% else %}
68 setup_carousel();
69{% endif %}
70 });
27 </script>71 </script>
28{% endif %}72{% endif %}
2973
=== modified file 'src/webcatalog/tests/test_commands.py'
--- src/webcatalog/tests/test_commands.py 2012-04-17 17:09:03 +0000
+++ src/webcatalog/tests/test_commands.py 2012-04-18 22:40:24 +0000
@@ -149,11 +149,6 @@
149 'DISK_APT_CACHE_LOCATION', self.tmp_apt_cache)149 'DISK_APT_CACHE_LOCATION', self.tmp_apt_cache)
150 self.patch_cache_setting.start()150 self.patch_cache_setting.start()
151151
152 self.patch_get_screenshot_urls = patch(
153 'webcatalog.management.commands.import_app_install_data.'
154 'Command.get_screenshot_urls_from_server')
155 self.patch_get_screenshot_urls.start()
156
157 if self.use_mock_apt_cache:152 if self.use_mock_apt_cache:
158 # The apt.Cache class is patched to return our own mock which153 # The apt.Cache class is patched to return our own mock which
159 # will be subscriptable with a pre-set dict with mock ock apt154 # will be subscriptable with a pre-set dict with mock ock apt
@@ -165,8 +160,6 @@
165160
166 def tearDown(self):161 def tearDown(self):
167 self.patch_cache_setting.stop()162 self.patch_cache_setting.stop()
168 if hasattr(self, 'patch_get_screenshot_urls'):
169 self.patch_get_screenshot_urls.stop()
170 shutil.rmtree(self.tmp_apt_cache)163 shutil.rmtree(self.tmp_apt_cache)
171 if self.use_mock_apt_cache:164 if self.use_mock_apt_cache:
172 self.patch_cache.stop()165 self.patch_cache.stop()
@@ -290,29 +283,6 @@
290 self.assertTrue(firefox.icon.size > 1)283 self.assertTrue(firefox.icon.size > 1)
291 self.assertTrue(scribus.icon.size > 1)284 self.assertTrue(scribus.icon.size > 1)
292285
293 def test_screenshots_added_to_the_model(self):
294 fn = 'webcatalog.management.commands.import_app_install_data.urllib'
295 self.patch_get_screenshot_urls.stop()
296 del self.patch_get_screenshot_urls
297 with patch(fn) as mock_urllib:
298 mock_response = Mock()
299 mock_response.getcode.return_value = 200
300 mock_response.read.return_value = json.dumps(
301 {'screenshots': [
302 {'large_image_url': 'http://example.com/1.png'}
303 ]}
304 )
305 mock_urllib.urlopen.return_value = mock_response
306
307 call_command('import_app_install_data', 'natty',
308 local_app_install_data=self.deb_location,
309 local_app_install_data_partner=self.deb_location,
310 verbosity=0)
311
312 scribus = Application.objects.get(package_name='scribus')
313
314 self.assertEqual(scribus.screenshots, ["http://example.com/1.png"])
315
316 def get_package_uri_for_series(self):286 def get_package_uri_for_series(self):
317 uri = import_app_install_data.Command().get_package_uri_for_series(287 uri = import_app_install_data.Command().get_package_uri_for_series(
318 'app-install-data', 'natty')288 'app-install-data', 'natty')
319289
=== modified file 'src/webcatalog/tests/test_utilities.py'
--- src/webcatalog/tests/test_utilities.py 2012-03-05 08:11:24 +0000
+++ src/webcatalog/tests/test_utilities.py 2012-04-18 22:40:24 +0000
@@ -20,8 +20,10 @@
20__all__ = [20__all__ = [
21 'CreatePNGFromFileTestCase',21 'CreatePNGFromFileTestCase',
22 'IdentityProviderTestCase',22 'IdentityProviderTestCase',
23 'ScreenshotGetterTestCase',
23 ]24 ]
2425
26import json
25import os27import os
2628
27from webcatalog.tests.factory import TestCaseWithFactory29from webcatalog.tests.factory import TestCaseWithFactory
@@ -124,3 +126,35 @@
124 u'displayname', u'name', u'unverified_emails', u'verified_emails',126 u'displayname', u'name', u'unverified_emails', u'verified_emails',
125 u'consumer_secret', u'token', u'openid_identifier',127 u'consumer_secret', u'token', u'openid_identifier',
126 u'consumer_key', u'token_secret']), set(data))128 u'consumer_key', u'token_secret']), set(data))
129
130
131class ScreenshotGetterTestCase(TestCase):
132 @patch('webcatalog.utilities.urllib.urlopen')
133 def test_valid_response_returns_list_of_urls(self, mock_urlopen):
134 response = json.dumps({'package': 'digikam', 'screenshots': [
135 {'version': '1.0', 'large_image_url': 'http://example.com/foo'},
136 {'version': '1.1', 'large_image_url': 'http://example.com/bar'},
137 ]})
138 mock_urlopen.return_value.getcode.return_value = 200
139 mock_urlopen.return_value.read.return_value = response
140 expected = ['http://example.com/foo', 'http://example.com/bar']
141 self.assertEqual(expected,
142 WebServices().get_screenshots_for_package('digikam'))
143
144 @patch('webcatalog.utilities.urllib.urlopen')
145 def test_not_found_response_returns_empty_list(self, mock_urlopen):
146 mock_urlopen.return_value.getcode.return_value = 404
147 self.assertEqual([], WebServices().get_screenshots_for_package('foo'))
148
149 @patch('webcatalog.utilities.urllib.urlopen')
150 def test_invalid_response_returns_empty_list(self, mock_urlopen):
151 mock_urlopen.return_value.getcode.return_value = 200
152 mock_urlopen.return_value.read.return_value = 'invalid json'
153 self.assertEqual([], WebServices().get_screenshots_for_package('foo'))
154
155 @patch('webcatalog.utilities.urllib.urlopen')
156 def test_unexpected_response_returns_empty_list(self, mock_urlopen):
157 response = json.dumps({'package': 'The Spanish Inquisition'})
158 mock_urlopen.return_value.getcode.return_value = 200
159 mock_urlopen.return_value.read.return_value = response
160 self.assertEqual([], WebServices().get_screenshots_for_package('foo'))
127161
=== modified file 'src/webcatalog/tests/test_views.py'
--- src/webcatalog/tests/test_views.py 2012-04-13 21:06:13 +0000
+++ src/webcatalog/tests/test_views.py 2012-04-18 22:40:24 +0000
@@ -22,12 +22,14 @@
22 with_statement,22 with_statement,
23 )23 )
2424
25import json
25import re26import re
2627
27from decimal import Decimal28from decimal import Decimal
2829
29from django.conf import settings30from django.conf import settings
30from django.core import mail31from django.core import mail
32from django.core.cache import cache
31from django.core.urlresolvers import reverse33from django.core.urlresolvers import reverse
32from django.test import TestCase34from django.test import TestCase
3335
@@ -43,6 +45,7 @@
43 'ApplicationDetailNoSeriesTestCase',45 'ApplicationDetailNoSeriesTestCase',
44 'ApplicationDetailTestCase',46 'ApplicationDetailTestCase',
45 'ApplicationReviewsTestCase',47 'ApplicationReviewsTestCase',
48 'ApplicationScreenshotsTestCase',
46 'IndexTestCase',49 'IndexTestCase',
47 'TermsOfServiceTestCase',50 'TermsOfServiceTestCase',
48 'OverviewTestCase',51 'OverviewTestCase',
@@ -59,6 +62,9 @@
5962
6063
61class ApplicationDetailTestCase(TestCaseWithFactory):64class ApplicationDetailTestCase(TestCaseWithFactory):
65 def setUp(self):
66 super(ApplicationDetailTestCase, self).setUp()
67 cache.clear()
6268
63 def get_app_details_url(self, app):69 def get_app_details_url(self, app):
64 return reverse('wc-package-detail',70 return reverse('wc-package-detail',
@@ -366,6 +372,10 @@
366372
367373
368class SearchTestCase(TestCaseWithFactory):374class SearchTestCase(TestCaseWithFactory):
375 def setUp(self):
376 super(SearchTestCase, self).setUp()
377 cache.clear()
378
369 def test_no_search_retrieves_no_apps(self):379 def test_no_search_retrieves_no_apps(self):
370 # Ensure there's some data in the DB380 # Ensure there's some data in the DB
371 distro = self.factory.make_distroseries(code_name='lucid')381 distro = self.factory.make_distroseries(code_name='lucid')
@@ -474,9 +484,9 @@
474484
475 def test_response_shows_number_of_apps_found(self):485 def test_response_shows_number_of_apps_found(self):
476 distro = self.factory.make_distroseries(code_name='lucid')486 distro = self.factory.make_distroseries(code_name='lucid')
477 app1 = self.factory.make_application(package_name='foo',487 self.factory.make_application(package_name='foo',
478 distroseries=distro)488 distroseries=distro)
479 app2 = self.factory.make_application(package_name='foobar',489 self.factory.make_application(package_name='foobar',
480 distroseries=distro)490 distroseries=distro)
481 url = reverse('wc-search', args=[distro.code_name])491 url = reverse('wc-search', args=[distro.code_name])
482492
@@ -536,6 +546,10 @@
536546
537547
538class IndexTestCase(TestCaseWithFactory):548class IndexTestCase(TestCaseWithFactory):
549 def setUp(self):
550 super(IndexTestCase, self).setUp()
551 cache.clear()
552
539 def test_index_contains_links_to_departments(self):553 def test_index_contains_links_to_departments(self):
540 dept = self.factory.make_department('foo')554 dept = self.factory.make_department('foo')
541555
@@ -671,6 +685,12 @@
671 self.assertContains(response, expected, 2)685 self.assertContains(response, expected, 2)
672 self.assertNotContains(response, app_not_included.package_name)686 self.assertNotContains(response, app_not_included.package_name)
673687
688 def test_includes_cache_headers(self):
689 with patch_settings(CACHE_MIDDLEWARE_SECONDS=600):
690 response = self.client.get(reverse('wc-index'))
691
692 self.assertEqual('max-age=600', response['cache-control'])
693
674694
675class TermsOfServiceTestCase(TestCase):695class TermsOfServiceTestCase(TestCase):
676696
@@ -686,6 +706,9 @@
686706
687707
688class OverviewTestCase(TestCaseWithFactory):708class OverviewTestCase(TestCaseWithFactory):
709 def setUp(self):
710 super(OverviewTestCase, self).setUp()
711 cache.clear()
689712
690 def test_department_contains_links_to_subdepartments(self):713 def test_department_contains_links_to_subdepartments(self):
691 dept = self.factory.make_department('foo')714 dept = self.factory.make_department('foo')
@@ -946,6 +969,70 @@
946 self.assertContains(response, 'review_summary2')969 self.assertContains(response, 'review_summary2')
947970
948971
972class ApplicationScreenshotsTestCase(TestCaseWithFactory):
973 def setUp(self):
974 super(ApplicationScreenshotsTestCase, self).setUp()
975 self.app = self.factory.make_application()
976 self.expected = ['http://example.com/foo', 'http://example.com/bar']
977
978 def test_only_valid_apps(self):
979 response = self.client.get(reverse('wc-package-screenshots',
980 args=['doesntexist']))
981
982 self.assertEqual(404, response.status_code)
983
984 @patch('webcatalog.utilities.urllib.urlopen')
985 def test_uncached_calls_to_rnr_api(self, mock_urlopen):
986 self.client.get(reverse('wc-package-screenshots',
987 args=[self.app.package_name]))
988
989 self.assertEqual(1, mock_urlopen.call_count)
990
991 @patch('webcatalog.utilities.urllib.urlopen')
992 def test_second_call_cached(self, mock_urlopen):
993 for count in range(2):
994 self.client.get(reverse('wc-package-screenshots',
995 args=[self.app.package_name]))
996
997 self.assertEqual(1, mock_urlopen.call_count)
998
999 @patch('webcatalog.utilities.WebServices.get_screenshots_for_package')
1000 def test_renders_screenshots(self, mock_get_screenshots):
1001 mock_get_screenshots.return_value = self.expected
1002
1003 response = self.client.get(reverse('wc-package-screenshots',
1004 args=[self.app.package_name]))
1005
1006 self.assertTemplateUsed(
1007 response, 'webcatalog/application_screenshots.html')
1008 for url in self.expected:
1009 self.assertContains(response, url)
1010
1011 @patch('webcatalog.utilities.WebServices.get_screenshots_for_package')
1012 def test_renders_json_for_json_request(self, mock_get_screenshots):
1013 mock_get_screenshots.return_value = self.expected
1014
1015 response = self.client.get(reverse('wc-package-screenshots',
1016 args=[self.app.package_name]),
1017 HTTP_X_REQUESTED_WITH='XMLHttpRequest')
1018
1019 self.assertTemplateNotUsed(
1020 response, 'webcatalog/application_review_list.html')
1021 self.assertEqual('application/json', response['content-type'])
1022 self.assertEqual(json.dumps(self.expected), response.content)
1023
1024 @patch('webcatalog.utilities.WebServices.get_screenshots_for_package')
1025 def test_has_caching_headers(self, mock_get_screenshots):
1026 mock_get_screenshots.return_value = self.expected
1027
1028 with patch_settings(CACHE_MIDDLEWARE_SECONDS=600):
1029 response = self.client.get(reverse('wc-package-screenshots',
1030 args=[self.app.package_name]),
1031 HTTP_X_REQUESTED_WITH='XMLHttpRequest')
1032
1033 self.assertEqual('max-age=600', response['cache-control'])
1034
1035
949class ComboViewTestCase(TestCase):1036class ComboViewTestCase(TestCase):
950 """Tests for ComboView."""1037 """Tests for ComboView."""
9511038
9521039
=== modified file 'src/webcatalog/urls.py'
--- src/webcatalog/urls.py 2012-03-20 11:12:02 +0000
+++ src/webcatalog/urls.py 2012-04-18 22:40:24 +0000
@@ -36,6 +36,8 @@
36 'department_overview', name='wc-department'),36 'department_overview', name='wc-department'),
37 url(r'^department/(?P<dept_slug_or_id>[\w\d-]+)/$', 'department_overview',37 url(r'^department/(?P<dept_slug_or_id>[\w\d-]+)/$', 'department_overview',
38 name='wc-department'),38 name='wc-department'),
39 url(r'^applications/screenshots/(?P<package_name>[-.+:\w]+)/$',
40 'application_screenshots', name="wc-package-screenshots"),
39 url(r'^applications/(?P<distro>[-.+\w]+)/(?P<package_name>[-.+:\w]+)/$',41 url(r'^applications/(?P<distro>[-.+\w]+)/(?P<package_name>[-.+:\w]+)/$',
40 'application_detail', name="wc-package-detail"),42 'application_detail', name="wc-package-detail"),
41 # The following URL redirects to the series-specific URL above.43 # The following URL redirects to the series-specific URL above.
4244
=== modified file 'src/webcatalog/utilities.py'
--- src/webcatalog/utilities.py 2012-03-19 20:43:17 +0000
+++ src/webcatalog/utilities.py 2012-04-18 22:40:24 +0000
@@ -28,10 +28,12 @@
28 'WebServiceError',28 'WebServiceError',
29 ]29 ]
3030
31import json
31import logging32import logging
32import math33import math
33import re34import re
34import urllib35import urllib
36from urlparse import urljoin
3537
36from django.conf import settings38from django.conf import settings
37from django.core.cache import cache39from django.core.cache import cache
@@ -77,6 +79,26 @@
77 cache.set(key, fresh_reviews, settings.REVIEWS_CACHE_TIMEOUT)79 cache.set(key, fresh_reviews, settings.REVIEWS_CACHE_TIMEOUT)
78 return fresh_reviews80 return fresh_reviews
7981
82 def get_screenshots_for_package(self, package_name):
83 key = 'get_screenshots_for_package-{0}'.format(
84 package_name)
85 cached_screenshots = cache.get(key)
86 if cached_screenshots is not None:
87 return cached_screenshots
88 url = urljoin(settings.SCREENSHOTS_BASE_URL,
89 '/json/package/{0}'.format(package_name))
90 fresh_screenshots = []
91 try:
92 response = urllib.urlopen(url)
93 if response.getcode() == 200:
94 data = json.loads(response.read())
95 fresh_screenshots = [entry['large_image_url']
96 for entry in data.get('screenshots', [])]
97 except (IOError, ValueError, TypeError):
98 pass
99 cache.set(key, fresh_screenshots, settings.REVIEWS_CACHE_TIMEOUT)
100 return fresh_screenshots
101
80 def _fake_validate_token(self, token, openid_identifier, signature):102 def _fake_validate_token(self, token, openid_identifier, signature):
81 """ This is a version of validate_token that gets the103 """ This is a version of validate_token that gets the
82 token_secret, consumer_secret from the plaintext signature.104 token_secret, consumer_secret from the plaintext signature.
83105
=== modified file 'src/webcatalog/views.py'
--- src/webcatalog/views.py 2012-04-13 21:17:09 +0000
+++ src/webcatalog/views.py 2012-04-18 22:40:24 +0000
@@ -210,6 +210,19 @@
210 return render_to_response(template, RequestContext(request, context))210 return render_to_response(template, RequestContext(request, context))
211211
212212
213def application_screenshots(request, package_name):
214 app = get_object_or_404(Application, package_name=package_name)
215 screenshots = WebServices().get_screenshots_for_package(package_name)
216 if request.is_ajax():
217 return HttpResponse(json.dumps(screenshots),
218 mimetype='application/json')
219 else:
220 context = dict(application=app, screenshots=screenshots,
221 breadcrumbs=app.crumbs())
222 return render_to_response('webcatalog/application_screenshots.html',
223 RequestContext(request, context))
224
225
213def combo_view(request):226def combo_view(request):
214 """Handle a request for combining a set of files."""227 """Handle a request for combining a set of files."""
215 fnames = parse_qs(request.META.get("QUERY_STRING", ""))228 fnames = parse_qs(request.META.get("QUERY_STRING", ""))

Subscribers

People subscribed via source and target branches