Merge lp:~elachuni/ubuntu-webcatalog/carousel into lp:ubuntu-webcatalog

Proposed by Anthony Lenton
Status: Merged
Approved by: Anthony Lenton
Approved revision: 69
Merged at revision: 67
Proposed branch: lp:~elachuni/ubuntu-webcatalog/carousel
Merge into: lp:ubuntu-webcatalog
Prerequisite: lp:~elachuni/ubuntu-webcatalog/convoy
Diff against target: 491 lines (+323/-57)
9 files modified
django_project/config/main.cfg (+2/-1)
src/webcatalog/schema.py (+1/-0)
src/webcatalog/static/css/carousel.css (+96/-0)
src/webcatalog/static/js/carousel.js (+181/-0)
src/webcatalog/templates/webcatalog/application_detail.html (+1/-1)
src/webcatalog/templates/webcatalog/exhibits_widget.html (+30/-39)
src/webcatalog/templates/webcatalog/index.html (+6/-0)
src/webcatalog/tests/test_views.py (+1/-11)
src/webcatalog/views.py (+5/-5)
To merge this branch: bzr merge lp:~elachuni/ubuntu-webcatalog/carousel
Reviewer Review Type Date Requested Status
Michael Nelson (community) Approve
Review via email: mp+96023@code.launchpad.net

Commit message

Added a YUI carousel widget for the front page exhibits display.

Description of the change

Overview
========
This branch adds a YUI carousel widget for the front page exhibits display.

Details
=======
Instead of using YUI3's carousel control, I reused the controls and stripped down the code from the carousel widget on the Ubuntu One homepage.

If Javascript is disabled, the first exhibit in the list will just remain displayed, with no controls available. The list is shuffled before rendering on the template, so (ignoring cache expirations) a different exhibit will be rendered on the front page each time.

If Javascript is enabled, the exhibits will be displayed in a random order, and cycle every 5 seconds.

Thanks to the combo loader the front page uses only 20 requests, 12 of which are for the different department icons. Without the combo loader it would take up 41 extra requests.

To post a comment you must log in.
Revision history for this message
Anthony Lenton (elachuni) wrote :

Brief screencast of the exhibits widget: http://people.canonical.com/~anthony/exhibits_widget.ogv

Revision history for this message
Michael Nelson (michael.nelson) wrote :
Download full text (14.8 KiB)

On Tue, Mar 6, 2012 at 12:34 AM, Anthony Lenton
<email address hidden> wrote:
> Anthony Lenton has proposed merging lp:~elachuni/ubuntu-webcatalog/carousel into lp:ubuntu-webcatalog with lp:~elachuni/ubuntu-webcatalog/convoy as a prerequisite.
>
> Requested reviews:
>  Canonical Consumer Applications Hackers (canonical-ca-hackers)
>
> For more details, see:
> https://code.launchpad.net/~elachuni/ubuntu-webcatalog/carousel/+merge/96023
>
> Overview
> ========
> This branch adds a YUI carousel widget for the front page exhibits display.
>
> Details
> =======
> Instead of using YUI3's carousel control, I reused the controls and stripped down the code from the carousel widget on the Ubuntu One homepage.

Cool!

>
> If Javascript is disabled, the first exhibit in the list will just remain displayed, with no controls available.  The list is shuffled before rendering on the template, so (ignoring cache expirations) a different exhibit will be rendered on the front page each time.
>
> If Javascript is enabled, the exhibits will be displayed in a random order, and cycle every 5 seconds.
>
> Thanks to the combo loader the front page uses only 20 requests, 12 of which are for the different department icons.  Without the combo loader it would take up 41 extra requests.

That's excellent! Do we have a bug for doing the department icons as a
single sprite? Would it be worth doing that?

Thanks for the screencast too... looks very nice!

> === added file 'src/webcatalog/static/css/carousel.css'
> --- src/webcatalog/static/css/carousel.css      1970-01-01 00:00:00 +0000
> +++ src/webcatalog/static/css/carousel.css      2012-03-05 23:33:18 +0000
> @@ -0,0 +1,96 @@
> +.carousel-wrapper {
> +    position: relative;
> +    height: 200px;
> +    overflow: hidden;
> +}
> +.container  {

Should that class be more specific? carouselcontainer or even
ccontainer? I remember a time where every site had a <div
class="container"> wrapping the page. Ah, it's probably just inherited
from u1?

> === added file 'src/webcatalog/static/images/arrow-sprite.png'
> Binary files src/webcatalog/static/images/arrow-sprite.png      1970-01-01 00:00:00 +0000 and src/webcatalog/static/images/arrow-sprite.png     2012-03-05 23:33:18 +0000 differ
> === added directory 'src/webcatalog/static/js'
> === added file 'src/webcatalog/static/js/carousel.js'
> --- src/webcatalog/static/js/carousel.js        1970-01-01 00:00:00 +0000
> +++ src/webcatalog/static/js/carousel.js        2012-03-05 23:33:18 +0000
> @@ -0,0 +1,181 @@
> +YUI.add('uwc-carousel', function(Y) {
> +    Y.namespace('uwc');
> +    function Carousel(config){
> +        Carousel.superclass.constructor.apply(this, arguments);
> +    }
> +    Y.extend(Carousel, Y.Base, {

Why are we creating our own Carousel module here? Can we ask U1 to
publish theirs on the yui gallery? It'd be great to not be maintaining
this code (or, on the other side, improving code that others can reuse
too). Did you try even just pointing YUI at U1's carousel module, if
that's possible?

> +        initializer: function(cfg) {
> +            var nodeListSlides,
> +                nodeCarousel,
> +                nodeContainer = this.get("nodeContai...

Revision history for this message
Michael Nelson (michael.nelson) :
review: Approve
68. By Anthony Lenton

A couple of minor fixes per code review.

69. By Anthony Lenton

Removed unnecessary container class.

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-05 17:41:06 +0000
3+++ django_project/config/main.cfg 2012-03-06 17:02:19 +0000
4@@ -50,7 +50,7 @@
5 webcatalog.context_processors.google_analytics_id
6 webcatalog.context_processors.user_agent
7
8-static_root = ./src/webcatalog/static/
9+static_root = ./django_project/static/
10 static_url = /assets/
11 admin_media_prefix = /assets/admin/
12
13@@ -93,6 +93,7 @@
14 sca_api_url = https://sc.staging.ubuntu.com/api/2.0/
15 disk_apt_cache_location = /tmp/webcat_cache
16 default_distro = natty
17+convoy_root = ./src/webcatalog/static/
18
19 [google]
20 google_analytics_id = UA-1018242-36
21
22=== modified file 'src/webcatalog/schema.py'
23--- src/webcatalog/schema.py 2012-01-06 18:36:08 +0000
24+++ src/webcatalog/schema.py 2012-03-06 17:02:19 +0000
25@@ -62,6 +62,7 @@
26 webcatalog.preload_api_service_roots = BoolConfigOption()
27 webcatalog.oauth_data_store = StringConfigOption(
28 default='webcatalog.models.oauthtoken.DataStore')
29+ webcatalog.convoy_root = StringConfigOption()
30
31 google = ConfigSection()
32 google.google_analytics_id = StringConfigOption()
33
34=== added file 'src/webcatalog/static/css/carousel.css'
35--- src/webcatalog/static/css/carousel.css 1970-01-01 00:00:00 +0000
36+++ src/webcatalog/static/css/carousel.css 2012-03-06 17:02:19 +0000
37@@ -0,0 +1,96 @@
38+.carousel-wrapper {
39+ position: relative;
40+ height: 200px;
41+ overflow: hidden;
42+}
43+#carousel {
44+ width: 100%;
45+ height: 100%;
46+ top: 0;
47+ left: 0;
48+ position: absolute;
49+ background: #333;
50+ display: block;
51+}
52+#carousel .next, #carousel .prev {
53+ background: url(/assets/images/arrow-sprite.png) no-repeat 0 0;
54+ position: absolute;
55+ z-index: 20;
56+ width: 33px;
57+ height: 66px;
58+ margin-top: -33px;
59+}
60+#carousel .next:active, #carousel .prev:active {
61+ margin-top: -32px;
62+ outline: none;
63+}
64+#carousel .next span, #carousel .prev span {
65+ position:absolute;
66+ left: -9999em;
67+ height: 0;
68+ width: 0;
69+}
70+#carousel .prev {
71+ left: 0;
72+ top: 50%;
73+ background-position:0 0;
74+}
75+#carousel .prev:hover, #carousel .prev:focus {
76+ outline: none;
77+ background-position: 0 -116px;
78+}
79+#carousel .next {
80+ right: 0;
81+ top: 50%;
82+ background-position: 0 -232px;
83+}
84+#carousel .next:hover, #carousel .next:focus {
85+ outline: none;
86+ background-position: 0 -348px;
87+}
88+#carousel .pagination {
89+ position: absolute;
90+ z-index: 20;
91+ left: 50%;
92+ top: 87%;
93+ margin-left: -35px;
94+}
95+#carousel .pagination li {
96+ float: left;
97+ margin-right: 10px;
98+}
99+#carousel .pagination li a {
100+ display: block;
101+ height: 10px;
102+ width: 10px;
103+ background: #aea79f;
104+ border-radius: 20px;
105+}
106+#carousel .pagination li a:hover, #carousel .pagination li a:focus,
107+#carousel .pagination li a.active:hover, #carousel .pagination li a.active:focus {
108+ background-color: #dd4814;
109+}
110+#carousel .pagination li a.active {
111+ background-color: #fff;
112+}
113+#carousel .pagination li a span {
114+ position: absolute;
115+ left: -999em;
116+}
117+#carousel .carousel {
118+ width: 10000px;
119+ overflow: hidden;
120+ position: relative;
121+}
122+#carousel .carousel li {
123+ position: relative;
124+ display: block;
125+ background: #333;
126+ float: left;
127+}
128+#carousel .carousel a {
129+ cursor: hand;
130+}
131+#carousel .carousel .disabled {
132+ display: none;
133+}
134
135=== added file 'src/webcatalog/static/images/arrow-sprite.png'
136Binary files src/webcatalog/static/images/arrow-sprite.png 1970-01-01 00:00:00 +0000 and src/webcatalog/static/images/arrow-sprite.png 2012-03-06 17:02:19 +0000 differ
137=== added directory 'src/webcatalog/static/js'
138=== added file 'src/webcatalog/static/js/carousel.js'
139--- src/webcatalog/static/js/carousel.js 1970-01-01 00:00:00 +0000
140+++ src/webcatalog/static/js/carousel.js 2012-03-06 17:02:19 +0000
141@@ -0,0 +1,181 @@
142+YUI.add('uwc-carousel', function(Y) {
143+ Y.namespace('uwc');
144+ function Carousel(config){
145+ Carousel.superclass.constructor.apply(this, arguments);
146+ }
147+ Y.extend(Carousel, Y.Base, {
148+ initializer: function(cfg) {
149+ var nodeListSlides,
150+ nodeCarousel,
151+ nodeContainer = this.get("nodeContainer"),
152+ carouselClassName = this.get("carouselClassName");
153+ this.interval = null;
154+ this.curSlide = 0;
155+ nodeListSlides = this.getCarouselNodeList();
156+ this.nodeCarousel = nodeContainer.one("."+ carouselClassName);
157+ // Ensure Carousel is position: relative
158+ this.nodeCarousel.setStyle("position", "relative");
159+ this.numSlides = nodeListSlides.size();
160+ // init animation
161+ this.caroAnim = new Y.Anim({
162+ node: this.nodeCarousel
163+ });
164+ this.caroAnim.set("duration", this.get("slideAnimDuration"));
165+ this.caroAnim.set("easing", this.get("slideEasing"));
166+ this.caroAnim.on("end", function(){
167+ this.updatePagination();
168+ }, this);
169+
170+ // Create next/prev
171+ this.generateControls();
172+ // Create Paging
173+ this.generatePagination();
174+ this.updatePagination();
175+ // We are ready display everything.
176+ this.nodeCarousel.removeClass("hidden");
177+ if (this.get("autoPlay")) {
178+ this.autoPlay();
179+ }
180+ },
181+ getCarouselNodeList: function() {
182+ var nodeContainer = this.get("nodeContainer"),
183+ carouselClassName = this.get("carouselClassName");
184+ return nodeContainer.all("." + carouselClassName + " li");
185+ },
186+ updatePagination: function() {
187+ var nodeContainer = this.get("nodeContainer"),
188+ paginationListItems;
189+ paginationListItems = nodeContainer.all(".pagination li a");
190+ paginationListItems.each(function(a){
191+ var nodeClassName = a.get("className");
192+ if (parseInt(nodeClassName.replace("p-", ""), 10) === this.curSlide) {
193+ a.addClass("active");
194+ } else {
195+ a.removeClass("active");
196+ }
197+ }, this);
198+ },
199+ generatePagination: function() {
200+ var pageText = 'Page',
201+ nodeContainer = this.get("nodeContainer"),
202+ ol = document.createElement('ol'),
203+ li, a, sp, txt;
204+
205+ ol.className = "pagination";
206+ for (var i=0, j=this.numSlides; i < j; i++){
207+ li = document.createElement('li');
208+ a = document.createElement('a');
209+ sp = document.createElement('span');
210+ a.href = '#';
211+ txt = document.createTextNode(pageText+(i+1));
212+ sp.appendChild(txt);
213+ a.appendChild(sp);
214+ a.className = 'p-'+i;
215+ li.appendChild(a);
216+ ol.appendChild(li);
217+ }
218+ nodeContainer.append(ol);
219+ olNode = Y.one(ol);
220+ olNode.on("click", function(e){
221+ var targetClass;
222+ e.preventDefault();
223+ if (this.caroAnim.get("running") === false) {
224+ if (e && this.autoPlayTimer) {
225+ clearInterval(this.autoPlayTimer);
226+ }
227+ targetClass = e.target.get("className");
228+ this.gotoSlide(parseInt(targetClass.replace("p-", ""), 10));
229+ }
230+ }, this);
231+ },
232+ generateControls: function() {
233+ var prev = Y.Node.create('<a href="#" class="prev"><span>Previous Slide</span></a>'),
234+ next = Y.Node.create('<a href="#" class="next"><span>Next Slide</span></a>'),
235+ nodeContainer = this.get("nodeContainer");
236+
237+ next.on("click", this.next, this);
238+ prev.on("click", this.prev, this);
239+ nodeContainer.appendChild(prev);
240+ nodeContainer.appendChild(next);
241+ },
242+ advance: function(e, val){
243+ if (e) {
244+ e.preventDefault();
245+ }
246+ if (e && this.autoPlayTimer) {
247+ clearInterval(this.autoPlayTimer);
248+ }
249+ if (this.caroAnim.get("running") === true) {
250+ return;
251+ }
252+ this.gotoSlide(val);
253+ },
254+ next: function(e){
255+ this.advance(e, this.curSlide + 1);
256+ },
257+ prev: function(e){
258+ this.advance(e, this.curSlide - 1);
259+ },
260+ gotoSlide: function(nSlideIndex, duration) {
261+ var xOffSet, slideWidth;
262+ slideWidth = parseInt(this.nodeCarousel.one("li").getStyle("width"), 10);
263+ duration = (duration === 0) ? duration : (duration || this.get("slideAnimDuration"));
264+ this.nSlideIndex = nSlideIndex;
265+ if (nSlideIndex < 0) {
266+ this.animateTo(-(slideWidth * (this.numSlides-1)), duration);
267+ this.curSlide = this.numSlides - 1;
268+ } else if (nSlideIndex >= (this.numSlides)) {
269+ this.animateTo(0, duration);
270+ this.curSlide = 0;
271+ } else {
272+ xOffSet = -slideWidth * nSlideIndex;
273+ this.animateTo(xOffSet, duration);
274+ this.curSlide = nSlideIndex;
275+ }
276+
277+ },
278+ autoPlay: function() {
279+ var that = this;
280+ this.autoPlayTimer = setInterval(function(){
281+ that.next();
282+ }, that.get("slideAnimInterval"));
283+ },
284+ animateTo: function(value, duration) {
285+ var origDuration;
286+ if (duration === 0) {
287+ this.nodeCarousel.setStyle("left", value);
288+ } else {
289+ if (duration) {
290+ origDuration = this.caroAnim.get("duration");
291+ this.caroAnim.set("duration", duration);
292+ }
293+ this.caroAnim.set("to", { "left": value });
294+ this.caroAnim.run();
295+ this.caroAnim.set("duration", origDuration);
296+ }
297+ },
298+ }, {
299+ NAME: "carousel",
300+ ATTRS: {
301+ carouselClassName: { value: "carousel"},
302+ containerHeight: { value: null },
303+ containerWidth: { value: null },
304+ nodeContainer: {
305+ setter: function(sel) {
306+ var n = Y.one(sel);
307+ if (!n) {
308+ Y.log('UWC:Carousel - invalid selector provided: ' + sel);
309+ }
310+ return n;
311+ }
312+ },
313+ slideAnimDuration: { value: 1 },
314+ slideAnimInterval: { value: 5000 },
315+ slideEasing: { value: Y.Easing.easeBoth },
316+ autoPlay: { value: true }
317+ }
318+ });
319+ Y.uwc.Carousel = Carousel;
320+}, '0.0.1', {
321+ requires: ['base', 'node', 'anim', 'event']
322+});
323
324=== modified file 'src/webcatalog/templates/webcatalog/application_detail.html'
325--- src/webcatalog/templates/webcatalog/application_detail.html 2012-03-05 17:41:06 +0000
326+++ src/webcatalog/templates/webcatalog/application_detail.html 2012-03-06 17:02:19 +0000
327@@ -8,7 +8,7 @@
328 {{ block.super }}
329 <script src="{{ STATIC_URL }}yui/3.4.0/build/yui/yui-min.js"></script>
330 <script>
331-YUI({debug: false, combine: true, comboBase: '{% url wc-combo %}?', root: 'yui/3.4.0/build/'}).use('io-base', 'node-base', function (Y) {
332+YUI({combine: true, comboBase: '{% url wc-combo %}?', root: 'yui/3.4.0/build/'}).use('io-base', 'node-base', function (Y) {
333 function complete(id, obj){
334 var reviews_html = obj.responseText;
335 var reviews_div = Y.one('#reviews_placeholder');
336
337=== modified file 'src/webcatalog/templates/webcatalog/exhibits_widget.html'
338--- src/webcatalog/templates/webcatalog/exhibits_widget.html 2012-03-01 22:42:00 +0000
339+++ src/webcatalog/templates/webcatalog/exhibits_widget.html 2012-03-06 17:02:19 +0000
340@@ -1,41 +1,32 @@
341+<div class="carousel-wrapper">
342+ <div id="carousel">
343+ <ol class="carousel">
344+ {% autoescape off %}
345+ {% comment %}
346+All slides except the first one start off with a "disabled" class, which will
347+make them "display: none". This will avoid loading pointless remote resources
348+(banners) if Javascript is disabled. The "disabled" class is then removed via
349+Javascript.
350+ {% endcomment %}
351+ {% for exhibit in exhibits %}
352+ <li class="slide{% if forloop.counter > 1%} disabled{% endif %}">
353+ <a href="{{ exhibit.destination_url }}">
354+ {{ exhibit.html }}
355+ </a>
356+ </li>
357+ {% endfor %}
358+ {% endautoescape %}
359+ </ol>
360+ </div>
361+</div>
362 <script type="text/javascript">
363- var activeExhibit = {{ active }};
364- var images = new Array();
365-
366- function switchExhibit() {
367- var tags = document.getElementsByClassName('exhibit-container');
368- var nExhibits = tags.length;
369- activeExhibit = (activeExhibit + 1) % nExhibits;
370- for (var i in tags) {
371- if (i == activeExhibit) {
372- tags[i].classList.add('enabled');
373- }
374- else {
375- tags[i].classList.remove('enabled');
376- }
377- }
378- setTimeout(switchExhibit, 10000);
379- };
380-
381- function preLoadBanners() {
382- for (var i = 0; i < preLoadBanners.arguments.length; i++) {
383- images[i] = new Image()
384- images[i].src = preLoadBanners.arguments[i]
385- }
386- }
387-
388- window.onload = function() {
389- preLoadBanners({% for exhibit in exhibits %}"{{ exhibit.banner_url }}"{% if not forloop.last %}, {% endif %}{% endfor %});
390- setTimeout(switchExhibit, 10000);
391- }
392+YUI({combine: true, comboBase: '{% url wc-combo %}?', root: 'yui/3.4.0/build/'}).use('uwc-carousel', function(Y) {
393+ var caro = new Y.uwc.Carousel({
394+ nodeContainer: "#carousel",
395+ containerHeight: 200,
396+ containerWidth: 912,
397+ autoPlay: true
398+ });
399+ Y.all('.slide').removeClass('disabled');
400+});
401 </script>
402-
403-{% autoescape off %}
404-{% for exhibit in exhibits %}
405-<div class="exhibit-container{% ifequal forloop.counter0 active %} enabled{% endifequal %}">
406- <a href="{{ exhibit.destination_url }}">
407-{{ exhibit.html }}
408- </a>
409-</div>
410-{% endfor %}
411-{% endautoescape %}
412\ No newline at end of file
413
414=== modified file 'src/webcatalog/templates/webcatalog/index.html'
415--- src/webcatalog/templates/webcatalog/index.html 2012-03-01 22:41:05 +0000
416+++ src/webcatalog/templates/webcatalog/index.html 2012-03-06 17:02:19 +0000
417@@ -5,6 +5,12 @@
418 {% block header %}{% trans "Ubuntu Apps Directory" %}{% endblock %}
419 {% block search %}{% endblock %}
420
421+{% block head_extra %}
422+ <link rel="stylesheet" type="text/css" href="{% url wc-combo %}?light/css/reset.css&light/css/styles.css&css/webcatalog.css&light/css/forms.css&css/carousel.css"/>
423+ <script src="{% url wc-combo %}?yui/3.4.0/build/yui/yui-min.js&js/carousel.js"></script>
424+{% endblock %}
425+
426+
427 {% block content %}
428
429 {% if exhibits %}
430
431=== modified file 'src/webcatalog/tests/test_views.py'
432--- src/webcatalog/tests/test_views.py 2012-03-05 17:41:06 +0000
433+++ src/webcatalog/tests/test_views.py 2012-03-06 17:02:19 +0000
434@@ -419,17 +419,7 @@
435
436 response = self.client.get(reverse('wc-index'))
437
438- self.assertContains(response, '<div class="exhibit-container',
439- count=1)
440-
441- def test_only_one_exhibit_enabled(self):
442- for i in range(3):
443- self.factory.make_exhibit(published=True)
444-
445- response = self.client.get(reverse('wc-index'))
446-
447- self.assertContains(response,
448- '<div class="exhibit-container enabled">', count=1)
449+ self.assertContains(response, '<li class="slide', count=1)
450
451
452 class OverviewTestCase(TestCaseWithFactory):
453
454=== modified file 'src/webcatalog/views.py'
455--- src/webcatalog/views.py 2012-03-05 17:41:06 +0000
456+++ src/webcatalog/views.py 2012-03-06 17:02:19 +0000
457@@ -24,7 +24,7 @@
458
459 import operator
460 import os
461-from random import randint
462+from random import shuffle
463 from urllib import urlencode
464
465 from convoy.combo import combine_files, parse_qs
466@@ -114,13 +114,13 @@
467 def index(request):
468 depts = Department.objects.filter(parent=None).order_by('name')
469 depts = depts.order_by('name')
470- exhibits = Exhibit.objects.filter(Q(display=True) |
471- Q(display=None, published=True,))
472+ exhibits = list(Exhibit.objects.filter(Q(display=True) |
473+ Q(display=None, published=True,)))
474+ shuffle(exhibits)
475
476 context = RequestContext(request, dict={
477 'depts': depts,
478 'exhibits': exhibits,
479- 'active': randint(0, max(exhibits.count() - 1, 1)),
480 })
481 return render_to_response('webcatalog/index.html',
482 context_instance=context)
483@@ -217,7 +217,7 @@
484 content_type = "text/javascript"
485 elif fnames[0].endswith(".css"):
486 content_type = "text/css"
487- content = combine_files(fnames, os.path.abspath(settings.STATIC_ROOT),
488+ content = combine_files(fnames, os.path.abspath(settings.CONVOY_ROOT),
489 resource_prefix=settings.MEDIA_URL, rewrite_urls=True)
490 # We're turning the generator returned by combine_files into a string
491 # here since GZipMiddleware would consume it if not. See Bug #822888.

Subscribers

People subscribed via source and target branches