Merge lp:~elachuni/ubuntu-webcatalog/featured-apps into lp:ubuntu-webcatalog

Proposed by Anthony Lenton
Status: Merged
Approved by: Danny Tamez
Approved revision: 69
Merged at revision: 71
Proposed branch: lp:~elachuni/ubuntu-webcatalog/featured-apps
Merge into: lp:ubuntu-webcatalog
Diff against target: 568 lines (+312/-60)
12 files modified
src/webcatalog/managers.py (+38/-0)
src/webcatalog/models/applications.py (+3/-0)
src/webcatalog/schema.py (+2/-2)
src/webcatalog/static/css/carousel.css (+131/-51)
src/webcatalog/static/js/carousel.js (+13/-4)
src/webcatalog/templates/webcatalog/exhibits_widget.html (+4/-3)
src/webcatalog/templates/webcatalog/featured_apps_widget.html (+52/-0)
src/webcatalog/templates/webcatalog/index.html (+6/-0)
src/webcatalog/tests/__init__.py (+1/-0)
src/webcatalog/tests/test_managers.py (+50/-0)
src/webcatalog/tests/test_views.py (+8/-0)
src/webcatalog/views.py (+4/-0)
To merge this branch: bzr merge lp:~elachuni/ubuntu-webcatalog/featured-apps
Reviewer Review Type Date Requested Status
Danny Tamez (community) Approve
Review via email: mp+96640@code.launchpad.net

Commit message

Implemented a "featured apps" widget for the front page, similar to the one on https://developer.ubuntu.com/

Description of the change

Overview
========
This branch implements a "featured apps" widget for the front page, similar to the one on https://developer.ubuntu.com/

Details
=======
No javascript was taken from developer.u.c as we already have a YUI carousel widget in place for the exhibits widget. Instead, I generalized that a bit so that the controls could be placed outside of the main carousel container.

The list of featured apps was added via a setting. The data for each app is taken straight from the database. If anything in the app seems wrong this would need to be customized on the app itself, and currently would be overridden the next time app-install-data is imported.

The tidy solution for this would be to allow per-app customizations, but this was left for another branch.

A very short video showing off the featured apps widget: http://people.canonical.com/~anthony/featured_apps.ogv

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

Nice stuff!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'src/webcatalog/managers.py'
2--- src/webcatalog/managers.py 1970-01-01 00:00:00 +0000
3+++ src/webcatalog/managers.py 2012-03-08 18:33:37 +0000
4@@ -0,0 +1,38 @@
5+# -*- coding: utf-8 -*-
6+# This file is part of the Apps Directory
7+# Copyright (C) 2011 Canonical Ltd.
8+#
9+# This program is free software: you can redistribute it and/or modify
10+# it under the terms of the GNU Affero General Public License as
11+# published by the Free Software Foundation, either version 3 of the
12+# License, or (at your option) any later version.
13+#
14+# This program is distributed in the hope that it will be useful,
15+# but WITHOUT ANY WARRANTY; without even the implied warranty of
16+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+# GNU Affero General Public License for more details.
18+#
19+# You should have received a copy of the GNU Affero General Public License
20+# along with this program. If not, see <http://www.gnu.org/licenses/>.
21+
22+"""Django object managers."""
23+
24+from __future__ import (
25+ absolute_import,
26+ with_statement,
27+ )
28+
29+
30+__metaclass__ = type
31+__all__ = [
32+ 'ApplicationManager',
33+ ]
34+
35+from django.db import models
36+
37+
38+class ApplicationManager(models.Manager):
39+ def find_best(self, **kwargs):
40+ options = self.filter(**kwargs).order_by('-distroseries__version')
41+ if options.exists():
42+ return options[0]
43
44=== modified file 'src/webcatalog/models/applications.py'
45--- src/webcatalog/models/applications.py 2012-03-01 21:38:31 +0000
46+++ src/webcatalog/models/applications.py 2012-03-08 18:33:37 +0000
47@@ -31,6 +31,7 @@
48 from django.db import models
49
50 from webcatalog.department_filters import department_filters
51+from webcatalog.managers import ApplicationManager
52
53 __metaclass__ = type
54 __all__ = [
55@@ -103,6 +104,8 @@
56 # series etc.)
57 description = models.TextField(blank=True)
58
59+ objects = ApplicationManager()
60+
61 def __unicode__(self):
62 return u"{0} ({1})".format(self.name, self.package_name)
63
64
65=== modified file 'src/webcatalog/schema.py'
66--- src/webcatalog/schema.py 2012-03-06 16:47:42 +0000
67+++ src/webcatalog/schema.py 2012-03-08 18:33:37 +0000
68@@ -17,8 +17,6 @@
69
70 """configglue schema for the Apps Directory."""
71
72-import django
73-
74 from configglue.pyschema import ConfigSection
75 from configglue.pyschema.options import (
76 BoolConfigOption,
77@@ -63,6 +61,8 @@
78 webcatalog.oauth_data_store = StringConfigOption(
79 default='webcatalog.models.oauthtoken.DataStore')
80 webcatalog.convoy_root = StringConfigOption()
81+ webcatalog.featured_apps = LinesConfigOption(item=StringConfigOption(),
82+ default=[])
83
84 google = ConfigSection()
85 google.google_analytics_id = StringConfigOption()
86
87=== modified file 'src/webcatalog/static/css/carousel.css'
88--- src/webcatalog/static/css/carousel.css 2012-03-06 16:55:20 +0000
89+++ src/webcatalog/static/css/carousel.css 2012-03-08 18:33:37 +0000
90@@ -1,96 +1,176 @@
91+/* Base carousel */
92 .carousel-wrapper {
93 position: relative;
94- height: 200px;
95 overflow: hidden;
96 }
97-#carousel {
98+.carousel {
99 width: 100%;
100 height: 100%;
101 top: 0;
102 left: 0;
103 position: absolute;
104- background: #333;
105 display: block;
106 }
107-#carousel .next, #carousel .prev {
108- background: url(/assets/images/arrow-sprite.png) no-repeat 0 0;
109- position: absolute;
110- z-index: 20;
111- width: 33px;
112- height: 66px;
113- margin-top: -33px;
114-}
115-#carousel .next:active, #carousel .prev:active {
116- margin-top: -32px;
117- outline: none;
118-}
119-#carousel .next span, #carousel .prev span {
120- position:absolute;
121- left: -9999em;
122- height: 0;
123- width: 0;
124-}
125-#carousel .prev {
126- left: 0;
127- top: 50%;
128- background-position:0 0;
129-}
130-#carousel .prev:hover, #carousel .prev:focus {
131- outline: none;
132- background-position: 0 -116px;
133-}
134-#carousel .next {
135- right: 0;
136- top: 50%;
137- background-position: 0 -232px;
138-}
139-#carousel .next:hover, #carousel .next:focus {
140- outline: none;
141- background-position: 0 -348px;
142-}
143-#carousel .pagination {
144+.carousel .pagination {
145 position: absolute;
146 z-index: 20;
147 left: 50%;
148 top: 87%;
149 margin-left: -35px;
150 }
151-#carousel .pagination li {
152+.carousel .pagination li {
153 float: left;
154 margin-right: 10px;
155 }
156-#carousel .pagination li a {
157+.carousel .pagination li a {
158 display: block;
159 height: 10px;
160 width: 10px;
161 background: #aea79f;
162 border-radius: 20px;
163 }
164-#carousel .pagination li a:hover, #carousel .pagination li a:focus,
165-#carousel .pagination li a.active:hover, #carousel .pagination li a.active:focus {
166+.carousel .pagination li a:hover, .carousel .pagination li a:focus,
167+.carousel .pagination li a.active:hover, .carousel .pagination li a.active:focus {
168 background-color: #dd4814;
169 }
170-#carousel .pagination li a.active {
171+.carousel .pagination li a.active {
172 background-color: #fff;
173 }
174-#carousel .pagination li a span {
175+.carousel .pagination li a span {
176 position: absolute;
177 left: -999em;
178 }
179-#carousel .carousel {
180+.carousel .carouselol {
181 width: 10000px;
182 overflow: hidden;
183 position: relative;
184 }
185-#carousel .carousel li {
186+.carousel .carouselol li {
187 position: relative;
188 display: block;
189- background: #333;
190 float: left;
191 }
192-#carousel .carousel a {
193+.carousel .carouselol a {
194 cursor: hand;
195 }
196-#carousel .carousel .disabled {
197+.carousel .carouselol .disabled {
198 display: none;
199 }
200+/* End base carousel */
201+
202+/* Exhibits carousel */
203+.exhibits-widget {
204+ margin-bottom: 32px;
205+}
206+.exhibits-widget .carousel-wrapper{
207+ height: 200px;
208+}
209+.exhibits-widget .carousel .next, .exhibits-widget .carousel .prev {
210+ background: url(/assets/images/arrow-sprite.png) no-repeat 0 0;
211+ position: absolute;
212+ z-index: 20;
213+ width: 33px;
214+ height: 66px;
215+ margin-top: -33px;
216+}
217+.exhibits-widget .carousel .next:active, .exhibits-widget .carousel .prev:active {
218+ margin-top: -32px;
219+ outline: none;
220+}
221+.exhibits-widget .carousel .next span, .exhibits-widget .carousel .prev span {
222+ position:absolute;
223+ left: -9999em;
224+ height: 0;
225+ width: 0;
226+}
227+.exhibits-widget .carousel .prev {
228+ left: 0;
229+ top: 50%;
230+ background-position:0 0;
231+}
232+.exhibits-widget .carousel .prev:hover, .exhibits-widget .carousel .prev:focus {
233+ outline: none;
234+ background-position: 0 -116px;
235+}
236+.exhibits-widget .carousel .next {
237+ right: 0;
238+ top: 50%;
239+ background-position: 0 -232px;
240+}
241+.exhibits-widget .carousel .next:hover, .exhibits-widget .carousel .next:focus {
242+ outline: none;
243+ background-position: 0 -348px;
244+}
245+
246+/* End exhibits carousel */
247+
248+/* Featured apps carousel */
249+.featured-widget {
250+ background: url("/assets/images/pattern-featured.gif") repeat scroll 0 0 #ebe9e7;
251+ border: 2px solid #aea79f;
252+ border-radius: 4px 4px 4px 4px;
253+ margin-bottom: 30px;
254+ overflow: hidden;
255+ padding: 5px;
256+}
257+.featured-widget .carousel-wrapper{
258+ height: 140px;
259+}
260+.featured-widget div.carousel-container {
261+ background-color: #fff;
262+ border: 1px solid #aea79f;
263+ border-radius: 4px 4px 4px 4px;
264+ overflow: hidden;
265+ padding: 10px;
266+}
267+
268+#content .featured-widget table {
269+ width: 874px;
270+}
271+#content .featured-widget table td {
272+ border-right: 1px dotted #AEA79F;
273+ border-bottom: 0;
274+ padding: 0 10px;
275+ width: 202px;
276+}
277+#featured-carousel .pagination li a.active {
278+ background-color: #333;
279+}
280+
281+#featured-controls {
282+ width: 70px;
283+ height: 32px;
284+ float: right;
285+}
286+
287+#featured-controls .next, #featured-controls .prev {
288+ background: url(/assets/images/arrow-sliders.png) no-repeat 0 0;
289+ float: left;
290+ z-index: 20;
291+ width: 32px;
292+ height: 29px;
293+}
294+#featured-controls .next span, #featured-controls .prev span {
295+ position:absolute;
296+ left: -9999em;
297+ height: 0;
298+ width: 0;
299+}
300+#featured-controls .prev {
301+ background-position:0 0;
302+}
303+#featured-controls .next {
304+ background-position: -32px 0;
305+}
306+#featured-controls .prev:hover, #featured-controls .prev:focus,
307+#featured-controls .next:hover, #featured-controls .next:focus {
308+ outline: none;
309+}
310+
311+.featured-widget img.icon64 {
312+ margin: 0 8px 40px 0;
313+ float: left;
314+}
315+.featured-widget h4 {
316+ font-weight: bold;
317+}
318
319=== added file 'src/webcatalog/static/images/arrow-sliders.png'
320Binary files src/webcatalog/static/images/arrow-sliders.png 1970-01-01 00:00:00 +0000 and src/webcatalog/static/images/arrow-sliders.png 2012-03-08 18:33:37 +0000 differ
321=== added file 'src/webcatalog/static/images/pattern-featured.gif'
322Binary files src/webcatalog/static/images/pattern-featured.gif 1970-01-01 00:00:00 +0000 and src/webcatalog/static/images/pattern-featured.gif 2012-03-08 18:33:37 +0000 differ
323=== modified file 'src/webcatalog/static/js/carousel.js'
324--- src/webcatalog/static/js/carousel.js 2012-03-05 23:14:54 +0000
325+++ src/webcatalog/static/js/carousel.js 2012-03-08 18:33:37 +0000
326@@ -91,12 +91,12 @@
327 generateControls: function() {
328 var prev = Y.Node.create('<a href="#" class="prev"><span>Previous Slide</span></a>'),
329 next = Y.Node.create('<a href="#" class="next"><span>Next Slide</span></a>'),
330- nodeContainer = this.get("nodeContainer");
331+ controlsContainer = this.get("controlsContainer");
332
333 next.on("click", this.next, this);
334 prev.on("click", this.prev, this);
335- nodeContainer.appendChild(prev);
336- nodeContainer.appendChild(next);
337+ controlsContainer.appendChild(prev);
338+ controlsContainer.appendChild(next);
339 },
340 advance: function(e, val){
341 if (e) {
342@@ -157,7 +157,7 @@
343 }, {
344 NAME: "carousel",
345 ATTRS: {
346- carouselClassName: { value: "carousel"},
347+ carouselClassName: { value: "carouselol"},
348 containerHeight: { value: null },
349 containerWidth: { value: null },
350 nodeContainer: {
351@@ -169,6 +169,15 @@
352 return n;
353 }
354 },
355+ controlsContainer: {
356+ setter: function(sel) {
357+ var n = Y.one(sel);
358+ if (!n) {
359+ Y.log('UWC:Carousel - invalid selector provided: ' + sel);
360+ }
361+ return n;
362+ }
363+ },
364 slideAnimDuration: { value: 1 },
365 slideAnimInterval: { value: 5000 },
366 slideEasing: { value: Y.Easing.easeBoth },
367
368=== modified file 'src/webcatalog/templates/webcatalog/exhibits_widget.html'
369--- src/webcatalog/templates/webcatalog/exhibits_widget.html 2012-03-06 16:55:20 +0000
370+++ src/webcatalog/templates/webcatalog/exhibits_widget.html 2012-03-08 18:33:37 +0000
371@@ -1,6 +1,6 @@
372 <div class="carousel-wrapper">
373- <div id="carousel">
374- <ol class="carousel">
375+ <div id="exhibits-carousel" class="carousel">
376+ <ol class="carouselol">
377 {% autoescape off %}
378 {% comment %}
379 All slides except the first one start off with a "disabled" class, which will
380@@ -22,7 +22,8 @@
381 <script type="text/javascript">
382 YUI({combine: true, comboBase: '{% url wc-combo %}?', root: 'yui/3.4.0/build/'}).use('uwc-carousel', function(Y) {
383 var caro = new Y.uwc.Carousel({
384- nodeContainer: "#carousel",
385+ nodeContainer: "#exhibits-carousel",
386+ controlsContainer: "#exhibits-carousel",
387 containerHeight: 200,
388 containerWidth: 912,
389 autoPlay: true
390
391=== added file 'src/webcatalog/templates/webcatalog/featured_apps_widget.html'
392--- src/webcatalog/templates/webcatalog/featured_apps_widget.html 1970-01-01 00:00:00 +0000
393+++ src/webcatalog/templates/webcatalog/featured_apps_widget.html 2012-03-08 18:33:37 +0000
394@@ -0,0 +1,52 @@
395+<div id="featured-controls"></div>
396+ <h3>Featured apps on the Ubuntu Software Centre</h3>
397+<div class="carousel-container">
398+ <div class="carousel-wrapper">
399+ <div id="featured-carousel" class="carousel">
400+ <ol class="carouselol">
401+ {% autoescape off %}
402+ {% comment %}
403+All slides except the first one start off with a "disabled" class, which will
404+make them "display: none". This will avoid loading pointless remote resources
405+(banners) if Javascript is disabled. The "disabled" class is then removed via
406+Javascript.
407+ {% endcomment %}
408+ <li class="slide{% if forloop.counter > 1%} disabled{% endif %}">
409+ <table><tr>
410+ {% for app in featured_apps %}
411+ <td>
412+ {% if app.icon %}
413+ <img class="icon64" src="{{ app.icon.url }}"/>
414+ {% else %}
415+ <img class="icon64" src="{{ STATIC_URL }}images/applications-other-64.png"/>
416+ {% endif %}
417+ <h4><a href="{% url wc-package-detail distro=app.distroseries.code_name package_name=app.package_name %}">{{ app.name }}</a></h4>
418+ <p>{{ app.departments.all.0.name }} | <b>{% if app.price %}${{ app.price }}{% else %}FREE{% endif %}</b></p>
419+ <p>{{ app.comment }}</p>
420+ </td>
421+ {% if forloop.counter|divisibleby:4 %}
422+ </tr></table>
423+ </li>
424+ <li class="slide{% if forloop.counter > 1%} disabled{% endif %}">
425+ <table><tr>
426+ {% endif %}
427+ {% endfor %}
428+ </tr></table>
429+ </li>
430+ {% endautoescape %}
431+ </ol>
432+ </div>
433+ </div>
434+</div>
435+<script type="text/javascript">
436+YUI({combine: true, comboBase: '{% url wc-combo %}?', root: 'yui/3.4.0/build/'}).use('uwc-carousel', function(Y) {
437+ var caro = new Y.uwc.Carousel({
438+ nodeContainer: "#featured-carousel",
439+ controlsContainer: "#featured-controls",
440+ containerHeight: 200,
441+ containerWidth: 912,
442+ autoPlay: false
443+ });
444+ Y.all('.slide').removeClass('disabled');
445+});
446+</script>
447
448=== modified file 'src/webcatalog/templates/webcatalog/index.html'
449--- src/webcatalog/templates/webcatalog/index.html 2012-03-05 23:14:54 +0000
450+++ src/webcatalog/templates/webcatalog/index.html 2012-03-08 18:33:37 +0000
451@@ -19,6 +19,12 @@
452 </div>
453 {% endif %}
454
455+{% if featured_apps %}
456+<div class="featured-widget">
457+ {% include "webcatalog/featured_apps_widget.html" %}
458+</div>
459+{% endif %}
460+
461 <h3>{% trans "Browse application departments" %}:</h3>
462
463 {% for dept in depts %}
464
465=== modified file 'src/webcatalog/tests/__init__.py'
466--- src/webcatalog/tests/__init__.py 2012-01-06 17:54:47 +0000
467+++ src/webcatalog/tests/__init__.py 2012-03-08 18:33:37 +0000
468@@ -23,6 +23,7 @@
469 from .test_forms import *
470 from .test_handlers import *
471 from .test_models import *
472+from .test_managers import *
473 from .test_pep8 import *
474 from .test_templatetags import *
475 from .test_utilities import *
476
477=== added file 'src/webcatalog/tests/test_managers.py'
478--- src/webcatalog/tests/test_managers.py 1970-01-01 00:00:00 +0000
479+++ src/webcatalog/tests/test_managers.py 2012-03-08 18:33:37 +0000
480@@ -0,0 +1,50 @@
481+# -*- coding: utf-8 -*-
482+# This file is part of the Apps Directory
483+# Copyright (C) 2011 Canonical Ltd.
484+#
485+# This program is free software: you can redistribute it and/or modify
486+# it under the terms of the GNU Affero General Public License as
487+# published by the Free Software Foundation, either version 3 of the
488+# License, or (at your option) any later version.
489+#
490+# This program is distributed in the hope that it will be useful,
491+# but WITHOUT ANY WARRANTY; without even the implied warranty of
492+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
493+# GNU Affero General Public License for more details.
494+#
495+# You should have received a copy of the GNU Affero General Public License
496+# along with this program. If not, see <http://www.gnu.org/licenses/>.
497+
498+"""Test cases for object managers."""
499+
500+from __future__ import (
501+ absolute_import,
502+ with_statement,
503+ )
504+
505+
506+from webcatalog.tests.factory import TestCaseWithFactory
507+from webcatalog.models import Application
508+
509+__metaclass__ = type
510+__all__ = [
511+ 'ApplicationManagerTestCase',
512+ ]
513+
514+
515+class ApplicationManagerTestCase(TestCaseWithFactory):
516+ def test_find_best_returns_none(self):
517+ self.assertIsNone(Application.objects.find_best(package_name='foo'))
518+
519+ def test_find_best_returns_latest(self):
520+ latest = self.factory.make_distroseries(version='14.10')
521+ older = self.factory.make_distroseries(version='14.04')
522+
523+ expected = self.factory.make_application(distroseries=latest)
524+ self.factory.make_application(package_name=expected.package_name,
525+ distroseries=older)
526+
527+ retrieved = Application.objects.find_best(
528+ package_name=expected.package_name)
529+
530+ self.assertEqual(expected.id, retrieved.id)
531
532=== modified file 'src/webcatalog/tests/test_views.py'
533--- src/webcatalog/tests/test_views.py 2012-03-05 23:14:54 +0000
534+++ src/webcatalog/tests/test_views.py 2012-03-08 18:33:37 +0000
535@@ -421,6 +421,14 @@
536
537 self.assertContains(response, '<li class="slide', count=1)
538
539+ def test_featured_apps(self):
540+ app = self.factory.make_application(package_name='foobar')
541+
542+ with patch_settings(FEATURED_APPS=['foobar', 'baz']):
543+ response = self.client.get(reverse('wc-index'))
544+
545+ self.assertEqual([app], response.context[0]['featured_apps'])
546+
547
548 class OverviewTestCase(TestCaseWithFactory):
549 def test_department_contains_links_to_subdepartments(self):
550
551=== modified file 'src/webcatalog/views.py'
552--- src/webcatalog/views.py 2012-03-06 16:47:42 +0000
553+++ src/webcatalog/views.py 2012-03-08 18:33:37 +0000
554@@ -117,10 +117,14 @@
555 exhibits = list(Exhibit.objects.filter(Q(display=True) |
556 Q(display=None, published=True,)))
557 shuffle(exhibits)
558+ featured_apps = [Application.objects.find_best(package_name=app)
559+ for app in settings.FEATURED_APPS]
560+ featured_apps = [x for x in featured_apps if x]
561
562 context = RequestContext(request, dict={
563 'depts': depts,
564 'exhibits': exhibits,
565+ 'featured_apps': featured_apps,
566 })
567 return render_to_response('webcatalog/index.html',
568 context_instance=context)

Subscribers

People subscribed via source and target branches