Merge lp:~michael.nelson/canonical-identity-provider/add-convoy-combo into lp:canonical-identity-provider/release

Proposed by Michael Nelson
Status: Rejected
Rejected by: Natalia Bidart
Proposed branch: lp:~michael.nelson/canonical-identity-provider/add-convoy-combo
Merge into: lp:canonical-identity-provider/release
Prerequisite: lp:~michael.nelson/canonical-identity-provider/fix_static_url
Diff against target: 374 lines (+209/-9)
11 files modified
.bzrignore (+2/-0)
django_project/config_dev/config/devel.cfg (+1/-0)
django_project/config_dev/config/main.cfg (+4/-1)
identityprovider/schema.py (+1/-0)
requirements/install.txt (+1/-0)
webui/templates/ubuntuone/base.html (+3/-7)
webui/templatetags/combo.py (+71/-0)
webui/tests/test_templatetags.py (+75/-0)
webui/tests/test_views_ui.py (+21/-0)
webui/urls.py (+1/-1)
webui/views/ui.py (+29/-0)
To merge this branch: bzr merge lp:~michael.nelson/canonical-identity-provider/add-convoy-combo
Reviewer Review Type Date Requested Status
Canonical ISD hackers Pending
Review via email: mp+158105@code.launchpad.net

Description of the change

WIP for discussion: An initial combo loader service which assumes a structure enabling multiple versions of various projects to be loaded.

Depending on the needs of the page, the loader can be use to load resources for a single project (ie. '?widget/widget-base.js&anim/anim-base.js' both from YUI), or across projects (ie. '?yui/3.3.0/widget-widget-base.js&ubuntuone/js/ie/html5shiv.js'). See the tests below.

The combo_url setting is added and used so that later we can switch to an external combo loader on a separate subdomain with a config change.

Questions
=========
 * Location? (in webui, but static media is in identityprovider?)

TODO
====
 * Enable revision in request for local sso media
 *

To post a comment you must log in.
776. By Michael Nelson

GREEN: combo template tag uses static_url.

777. By Michael Nelson

REFACTOR: move combo template tag to webui app.

778. By Michael Nelson

REFACTOR: combo template tag uses combo when debug is false.

779. By Michael Nelson

REFACTOR: add prefx option to combo tag.

780. By Michael Nelson

Convert to django 1.3 valid tag :/

781. By Michael Nelson

Use combo tag on ubuntuone base template.

782. By Michael Nelson

Use combo for js loading on login.

783. By Michael Nelson

Merged trunk from prev.

Revision history for this message
Natalia Bidart (nataliabidart) wrote :

Hi! I'm doing some cleanup on SSO MPs, I assume this is no longer valid, will set as Rejected.

Please shange the global status if this is still current. Thanks!

Unmerged revisions

783. By Michael Nelson

Merged trunk from prev.

782. By Michael Nelson

Use combo for js loading on login.

781. By Michael Nelson

Use combo tag on ubuntuone base template.

780. By Michael Nelson

Convert to django 1.3 valid tag :/

779. By Michael Nelson

REFACTOR: add prefx option to combo tag.

778. By Michael Nelson

REFACTOR: combo template tag uses combo when debug is false.

777. By Michael Nelson

REFACTOR: move combo template tag to webui app.

776. By Michael Nelson

GREEN: combo template tag uses static_url.

775. By Michael Nelson

Merged fix_static_url

774. By Michael Nelson

RED: combo template tag uses static_url

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2013-03-18 21:08:47 +0000
3+++ .bzrignore 2013-04-11 12:24:22 +0000
4@@ -30,3 +30,5 @@
5 scripts/local.cfg-
6 ./oopses/*
7
8+# Collected static files.
9+django_project/static/*
10
11=== modified file 'django_project/config_dev/config/devel.cfg'
12--- django_project/config_dev/config/devel.cfg 2013-04-03 20:34:24 +0000
13+++ django_project/config_dev/config/devel.cfg 2013-04-11 12:24:22 +0000
14@@ -22,6 +22,7 @@
15 django.contrib.messages
16 django.contrib.sessions
17 django.contrib.sites
18+ django.contrib.staticfiles
19 django.contrib.admin
20 django_openid_auth
21 django_configglue
22
23=== modified file 'django_project/config_dev/config/main.cfg'
24--- django_project/config_dev/config/main.cfg 2013-04-11 12:24:22 +0000
25+++ django_project/config_dev/config/main.cfg 2013-04-11 12:24:22 +0000
26@@ -37,6 +37,7 @@
27 django.contrib.messages
28 django.contrib.sessions
29 django.contrib.sites
30+ django.contrib.staticfiles
31 django.contrib.admin
32 django_openid_auth
33 django_configglue
34@@ -80,8 +81,10 @@
35 server_email = noreply@%(domain)s
36 session_cookie_secure = true
37 session_engine = django.contrib.sessions.backends.db
38-static_root = media
39+static_root = django_project/static
40 static_url = /assets/
41+staticfiles_dirs =
42+ django_project/external_staticfiles
43 template_context_processors =
44 django.contrib.messages.context_processors.messages
45 django.contrib.auth.context_processors.auth
46
47=== added directory 'django_project/external_staticfiles'
48=== added directory 'django_project/external_staticfiles/yui'
49=== added symlink 'django_project/external_staticfiles/yui/3.3.0'
50=== target is u'../../../identityprovider/media/src-js/lazrjs/yui'
51=== added directory 'django_project/static'
52=== added directory 'identityprovider/media/yui'
53=== added symlink 'identityprovider/media/yui/3.3.0'
54=== target is u'../src-js/lazrjs/yui/'
55=== modified file 'identityprovider/schema.py'
56--- identityprovider/schema.py 2013-03-28 13:08:29 +0000
57+++ identityprovider/schema.py 2013-04-11 12:24:22 +0000
58@@ -169,6 +169,7 @@
59 help="URL to POST when account data has changed")
60 embedded_trust_root = StringOption()
61 max_password_reset_tokens = IntOption(default=5)
62+ static_combo_url = StringOption(default="/combo/")
63
64 class twofactor(Section):
65 hotp_drift = IntOption()
66
67=== added directory 'identityprovider/static'
68=== added directory 'identityprovider/static/identityprovider'
69=== added symlink 'identityprovider/static/identityprovider/ubuntu'
70=== target is u'../../media/ubuntu'
71=== added symlink 'identityprovider/static/identityprovider/ubuntuone'
72=== target is u'../../media/ubuntuone'
73=== modified file 'requirements/install.txt'
74--- requirements/install.txt 2013-04-10 18:03:19 +0000
75+++ requirements/install.txt 2013-04-11 12:24:22 +0000
76@@ -1,6 +1,7 @@
77 argparse==1.2.1
78 BeautifulSoup==3.2.1
79 configglue==1.0.3
80+convoy==0.2.4
81 Django==1.3
82 django-adminaudit==0.3.3
83 django-configglue==0.6.1
84
85=== modified file 'webui/templates/ubuntuone/base.html'
86--- webui/templates/ubuntuone/base.html 2013-04-11 12:24:22 +0000
87+++ webui/templates/ubuntuone/base.html 2013-04-11 12:24:22 +0000
88@@ -1,4 +1,4 @@
89-{% load i18n static_url %}<!DOCTYPE html>
90+{% load i18n static_url combo %}<!DOCTYPE html>
91 <!--[if IE 7 ]><html class="ie7" lang="en" dir="ltr"><![endif]-->
92 <!--[if IE 8 ]><html class="ie8" lang="en" dir="ltr"><![endif]-->
93 <!--[if (gte IE 9)|!(IE)]><!--><html lang="en" dir="ltr" {% block html_extra %}{% endblock %}><!--<![endif]-->
94@@ -14,11 +14,7 @@
95 <link rel="icon" type="image/vnd.microsoft.icon" href="{{ STATIC_URL }}identityprovider/ubuntu/favicon.ico">
96
97 {% block corecss %}
98- <link href="{{STATIC_URL}}identityprovider/ubuntuone/css/grids-min.css" rel="stylesheet" type="text/css">
99- <link href="{{STATIC_URL}}identityprovider/ubuntuone/css/cssgrids-responsive-min.css" rel="stylesheet" type="text/css">
100- <link href="{{STATIC_URL}}identityprovider/ubuntuone/css/base.css" rel="stylesheet" type="text/css">
101- <link href="{{STATIC_URL}}identityprovider/ubuntuone/css/ubuntuone.css" rel="stylesheet" type="text/css">
102- <link href="{{STATIC_URL}}identityprovider/ubuntuone/css/footer-base.css" rel="stylesheet" type="text/css">
103+ {% combo prefix='identityprovider/ubuntuone/css' 'grids-min.css' 'cssgrids-responsive-min.css' 'base.css' 'ubuntuone.css' 'footer-base.css' %}
104
105 <link href='http://fonts.googleapis.com/css?family=Ubuntu:400,300,700' rel='stylesheet' type='text/css'>
106
107@@ -85,7 +81,7 @@
108 {% block general_js %}
109
110 {% comment %}To be replaced by u1 js{% endcomment %}
111- <script src="{{ STATIC_URL }}identityprovider/lazr-js/yui/yui-min.js"></script>
112+ {% combo prefix='yui/3.3.0' 'yui/yui-min.js' 'oop/oop-min.js' 'loader/loader-min.js' 'event-custom/event-custom-base-min.js' 'event/event-base-min.js' 'pluginhost/pluginhost-min.js' 'dom/dom-min.js' 'node/node-min.js' 'event/event-delegate-min.js' %}
113
114 {% endblock %}
115
116
117=== added directory 'webui/templatetags'
118=== added file 'webui/templatetags/__init__.py'
119=== added file 'webui/templatetags/combo.py'
120--- webui/templatetags/combo.py 1970-01-01 00:00:00 +0000
121+++ webui/templatetags/combo.py 2013-04-11 12:24:22 +0000
122@@ -0,0 +1,71 @@
123+# Copyright 2013 Canonical Ltd. This software is licensed under the
124+# GNU Affero General Public License version 3 (see the file LICENSE).
125+
126+from django import template
127+from django.conf import settings
128+from django.template import TemplateSyntaxError
129+from django.template.base import Node
130+from django.template.defaulttags import kwarg_re
131+
132+register = template.Library()
133+
134+
135+css_template = ('<link href="{0}" rel="stylesheet" type="text/css" '
136+ 'media="screen" />')
137+
138+js_template = '<script type="text/javascript" src="{0}"></script>'
139+
140+# XXX Once we move beyond django 1.3 we can just use a simple_tag
141+# which will handle *args and **kwargs with half the code.
142+
143+@register.tag
144+def combo(parser, token):
145+ """Parse the args and kwargs - based on django's url tag."""
146+ bits = token.split_contents()
147+ tagname = bits[0]
148+ if len(bits) < 2:
149+ raise TemplateSyntaxError(
150+ "'%s' takes at least one argument." % tagname)
151+
152+ filenames = []
153+ options = {}
154+ bits = bits[1:]
155+ for bit in bits:
156+ match = kwarg_re.match(bit)
157+ if not match:
158+ raise TemplateSyntaxError(
159+ "Malformed arguments to '{0}' tag.".format(tagname))
160+ name, value = match.groups()
161+ if name:
162+ options[name] = parser.compile_filter(value)
163+ else:
164+ filenames.append(parser.compile_filter(value))
165+
166+ return ComboNode(filenames, options)
167+
168+
169+class ComboNode(Node):
170+
171+ def __init__(self, filenames, options):
172+ self.filenames = filenames
173+ self.options = options
174+
175+ def render(self, context):
176+ filenames = [f.resolve(context) for f in self.filenames]
177+ options = dict([(k, v.resolve(context))
178+ for k, v in self.options.items()])
179+ prefix = options.get('prefix', '').strip('/')
180+ template = css_template
181+ if filenames[0].endswith('.js'):
182+ template = js_template
183+
184+ filenames = [f.strip('/') for f in filenames]
185+ if prefix:
186+ filenames = ['/'.join([prefix, filename]) for filename in filenames]
187+
188+ if settings.DEBUG or len(filenames) == 1:
189+ return "\n".join([
190+ template.format(settings.STATIC_URL + f) for f in filenames])
191+
192+ url = settings.STATIC_COMBO_URL + '?' + '&'.join(filenames)
193+ return template.format(url)
194
195=== added file 'webui/tests/test_templatetags.py'
196--- webui/tests/test_templatetags.py 1970-01-01 00:00:00 +0000
197+++ webui/tests/test_templatetags.py 2013-04-11 12:24:22 +0000
198@@ -0,0 +1,75 @@
199+# Copyright 2013 Canonical Ltd. This software is licensed under the
200+# GNU Affero General Public License version 3 (see the file LICENSE).
201+
202+from unittest import TestCase
203+
204+from django.conf import settings
205+from django.template import (
206+ Context,
207+ Template,
208+)
209+from mock import patch
210+
211+
212+class ComboTestCase(TestCase):
213+
214+ def test_debug(self):
215+ cases = ((
216+ "'a/b/foo.css' 'd/e/goo.css'",
217+ '<link href="/static/a/b/foo.css" rel="stylesheet"'
218+ ' type="text/css" media="screen" />\n'
219+ '<link href="/static/d/e/goo.css" rel="stylesheet"'
220+ ' type="text/css" media="screen" />',
221+ ), (
222+ "'a/b/foo.css'",
223+ '<link href="/static/a/b/foo.css" rel="stylesheet"'
224+ ' type="text/css" media="screen" />'
225+ ), (
226+ "'a/b/foo.js' 'd/e/goo.js'",
227+ '<script type="text/javascript" src="/static/a/b/foo.js">'
228+ '</script>\n'
229+ '<script type="text/javascript" src="/static/d/e/goo.js">'
230+ '</script>'
231+ ), (
232+ "'foo.js' 'goo.js' prefix='a/b'",
233+ '<script type="text/javascript" src="/static/a/b/foo.js">'
234+ '</script>\n'
235+ '<script type="text/javascript" src="/static/a/b/goo.js">'
236+ '</script>'
237+ ))
238+
239+ with patch.multiple(settings, DEBUG=True, STATIC_URL='/static/'):
240+ for (tag_input, expected) in cases:
241+ result = Template(
242+ "{% load combo %}"
243+ "{% combo " + tag_input + " %}").render(Context())
244+
245+ self.assertEqual(expected, result)
246+
247+ def test_non_debug(self):
248+ cases = ((
249+ "'a/b/foo.css' 'd/e/goo.css'",
250+ '<link href="/combo/?a/b/foo.css&d/e/goo.css" rel="stylesheet"'
251+ ' type="text/css" media="screen" />',
252+ ), (
253+ "'a/b/foo.css'",
254+ '<link href="/static/a/b/foo.css" rel="stylesheet"'
255+ ' type="text/css" media="screen" />'
256+ ), (
257+ "'a/b/foo.js' 'd/e/goo.js'",
258+ '<script type="text/javascript"'
259+ ' src="/combo/?a/b/foo.js&d/e/goo.js"></script>',
260+ ), (
261+ "'foo.js' 'goo.js' prefix='a/b'",
262+ '<script type="text/javascript"'
263+ ' src="/combo/?a/b/foo.js&a/b/goo.js"></script>',
264+ ))
265+
266+ with patch.multiple(settings, DEBUG=False, STATIC_COMBO_URL='/combo/',
267+ STATIC_URL='/static/'):
268+ for (tag_input, expected) in cases:
269+ result = Template(
270+ "{% load combo %}"
271+ "{% combo " + tag_input + " %}").render(Context())
272+
273+ self.assertEqual(expected, result)
274
275=== modified file 'webui/tests/test_views_ui.py'
276--- webui/tests/test_views_ui.py 2013-04-09 13:39:29 +0000
277+++ webui/tests/test_views_ui.py 2013-04-11 12:24:22 +0000
278@@ -16,6 +16,7 @@
279 from django.conf import settings
280 from django.contrib.sessions.models import Session
281 from django.core import mail
282+from django.core.management import call_command
283 from django.core.urlresolvers import reverse
284 from django.http import QueryDict, HttpResponse
285
286@@ -1528,3 +1529,23 @@
287 self.assertTrue(ui.authenticate_device(account, '123456'))
288 self.assertTrue(devices[0].authenticate.called)
289 self.assertTrue(devices[1].authenticate.called)
290+
291+
292+class ComboViewTestCase(SSOBaseTestCase):
293+
294+ @classmethod
295+ def setUpClass(cls):
296+ """Ensure the static files are ready for combo loading."""
297+ call_command('collectstatic', verbosity=0, interactive=False)
298+
299+ def test_combine_files_multiple_projects(self):
300+ response = self.client.get(settings.STATIC_COMBO_URL +
301+ '?yui/3.3.0/widget/widget-base.js'
302+ '&identityprovider/ubuntuone/js/ie/html5shiv.js')
303+
304+ self.assertEqual(response['content-type'], 'text/javascript')
305+ self.assertNotContains(response, '[missing]')
306+ self.assertContains(response, '/* yui/3.3.0/widget/widget-base.js */')
307+ self.assertContains(
308+ response,
309+ '/* identityprovider/ubuntuone/js/ie/html5shiv.js */')
310
311=== modified file 'webui/urls.py'
312--- webui/urls.py 2013-04-05 05:55:33 +0000
313+++ webui/urls.py 2013-04-11 12:24:22 +0000
314@@ -73,7 +73,7 @@
315 url(r'^\+faq$', 'static_page', {'page_name': 'faq'}, name='faq'),
316 url(r'^\+ubuntuone-account$', 'static_page',
317 {'page_name': 'ubuntuone-account'}, name='ubuntuone-account'),
318-
319+ url(r'^combo/$', 'combo_view', name='combo-view'),
320 )
321
322 urlpatterns += patterns(
323
324=== modified file 'webui/views/ui.py'
325--- webui/views/ui.py 2013-04-02 08:38:51 +0000
326+++ webui/views/ui.py 2013-04-11 12:24:22 +0000
327@@ -3,7 +3,12 @@
328 # LICENSE).
329
330 import logging
331+import os
332
333+from convoy.combo import (
334+ combine_files,
335+ parse_qs,
336+)
337 from django import forms
338 from django.conf import settings
339 from django.contrib import auth, messages
340@@ -12,6 +17,7 @@
341 from django.db.models import F
342 from django.http import (
343 Http404,
344+ HttpResponse,
345 HttpResponseNotAllowed,
346 HttpResponseRedirect,
347 urlencode,
348@@ -970,3 +976,26 @@
349
350 return render_to_response('static/%s.html' % page_name,
351 RequestContext(request))
352+
353+
354+def combo_view(request):
355+ """Handle a request for combining a set of files."""
356+ fnames = parse_qs(request.META.get("QUERY_STRING", ""))
357+ content_type = "text/plain"
358+
359+ if fnames:
360+ if fnames[0].endswith(".js"):
361+ content_type = "text/javascript"
362+ elif fnames[0].endswith(".css"):
363+ content_type = "text/css"
364+
365+ # XXX Check what resource_prefix is used for.
366+ content = combine_files(
367+ fnames, os.path.abspath(settings.STATIC_ROOT),
368+ resource_prefix=settings.STATIC_URL, rewrite_urls=True)
369+ # We're turning the generator returned by combine_files into a string
370+ # here since GZipMiddleware would consume it if not. See Bug #822888.
371+ return HttpResponse(
372+ content_type=content_type, status=200,
373+ content="".join(content))
374+ return HttpResponse(content_type=content_type, status=404)