[etwist] Generates apache's mod_concat-like urls for js and css
Uses the lists of js, css and ie_css included in order to generate
urls compatible with Apache mod_concat (ie: http://test.fr/??file1.js,file2.js).
Css files may use `media` option or may be for IE only ; this patch
does not concatenate css filenames for different media types or
different IE specs.
It concatenates only files served under cubicweb's `data` urls
and keeps original include order.
The generated urls are added to the HTML header as usual.
HOW-TO:
- calling HTMLHead(datadir_url) will enable the url concatenation
- calling HTMLHead() will not benefit of url concatenation
IMPORTANT NOTE:
- fckeditor can no more be served under `/data/` segment
--- a/etwist/server.py Thu Apr 28 20:24:27 2011 +0200
+++ b/etwist/server.py Thu Apr 28 20:41:44 2011 +0200
@@ -88,10 +88,6 @@
NoListingFile.__init__(self, path)
self.config = config
self.here = path
- # backward-compatiblity: take care fckeditor may appears as
- # root directory or as a data subdirectory. XXX (adim) : why
- # that ?
- self.putChild('fckeditor', FCKEditorResource(self.config, ''))
self._defineChildResources()
if self.config.debugmode:
self.data_modconcat_basepath = '/data/??'
--- a/test/unittest_utils.py Thu Apr 28 20:24:27 2011 +0200
+++ b/test/unittest_utils.py Thu Apr 28 20:41:44 2011 +0200
@@ -22,8 +22,8 @@
import datetime
from logilab.common.testlib import TestCase, unittest_main
-
-from cubicweb.utils import make_uid, UStringIO, SizeConstrainedList, RepeatList
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.utils import make_uid, UStringIO, SizeConstrainedList, RepeatList, HTMLHead
from cubicweb.entity import Entity
try:
@@ -155,6 +155,102 @@
def test_encoding_unknown_stuff(self):
self.assertEqual(self.encode(TestCase), 'null')
+class HTMLHeadTC(CubicWebTC):
+ def test_concat_urls(self):
+ base_url = u'http://test.fr/data/'
+ head = HTMLHead(base_url)
+ urls = [base_url + u'bob1.js',
+ base_url + u'bob2.js',
+ base_url + u'bob3.js']
+ result = head.concat_urls(urls)
+ expected = u'http://test.fr/data/??bob1.js,bob2.js,bob3.js'
+ self.assertEqual(result, expected)
+
+ def test_group_urls(self):
+ base_url = u'http://test.fr/data/'
+ head = HTMLHead(base_url)
+ urls_spec = [(base_url + u'bob0.js', None),
+ (base_url + u'bob1.js', None),
+ (u'http://ext.com/bob2.js', None),
+ (u'http://ext.com/bob3.js', None),
+ (base_url + u'bob4.css', 'all'),
+ (base_url + u'bob5.css', 'all'),
+ (base_url + u'bob6.css', 'print'),
+ (base_url + u'bob7.css', 'print'),
+ (base_url + u'bob8.css', ('all', u'[if IE 8]')),
+ (base_url + u'bob9.css', ('print', u'[if IE 8]'))
+ ]
+ result = head.group_urls(urls_spec)
+ expected = [(base_url + u'??bob0.js,bob1.js', None),
+ (u'http://ext.com/bob2.js', None),
+ (u'http://ext.com/bob3.js', None),
+ (base_url + u'??bob4.css,bob5.css', 'all'),
+ (base_url + u'??bob6.css,bob7.css', 'print'),
+ (base_url + u'bob8.css', ('all', u'[if IE 8]')),
+ (base_url + u'bob9.css', ('print', u'[if IE 8]'))
+ ]
+ self.assertEqual(list(result), expected)
+
+ def test_getvalue_with_concat(self):
+ base_url = u'http://test.fr/data/'
+ head = HTMLHead(base_url)
+ head.add_js(base_url + u'bob0.js')
+ head.add_js(base_url + u'bob1.js')
+ head.add_js(u'http://ext.com/bob2.js')
+ head.add_js(u'http://ext.com/bob3.js')
+ head.add_css(base_url + u'bob4.css')
+ head.add_css(base_url + u'bob5.css')
+ head.add_css(base_url + u'bob6.css', 'print')
+ head.add_css(base_url + u'bob7.css', 'print')
+ head.add_ie_css(base_url + u'bob8.css')
+ head.add_ie_css(base_url + u'bob9.css', 'print', u'[if lt IE 7]')
+ result = head.getvalue()
+ expected = u"""<head>
+<link rel="stylesheet" type="text/css" media="all" href="http://test.fr/data/??bob4.css,bob5.css"/>
+<link rel="stylesheet" type="text/css" media="print" href="http://test.fr/data/??bob6.css,bob7.css"/>
+<!--[if lt IE 8]>
+<link rel="stylesheet" type="text/css" media="all" href="http://test.fr/data/bob8.css"/>
+<!--[if lt IE 7]>
+<link rel="stylesheet" type="text/css" media="print" href="http://test.fr/data/bob9.css"/>
+<![endif]-->
+<script type="text/javascript" src="http://test.fr/data/??bob0.js,bob1.js"></script>
+<script type="text/javascript" src="http://ext.com/bob2.js"></script>
+<script type="text/javascript" src="http://ext.com/bob3.js"></script>
+</head>
+"""
+ self.assertEqual(result, expected)
+
+ def test_getvalue_without_concat(self):
+ base_url = u'http://test.fr/data/'
+ head = HTMLHead()
+ head.add_js(base_url + u'bob0.js')
+ head.add_js(base_url + u'bob1.js')
+ head.add_js(u'http://ext.com/bob2.js')
+ head.add_js(u'http://ext.com/bob3.js')
+ head.add_css(base_url + u'bob4.css')
+ head.add_css(base_url + u'bob5.css')
+ head.add_css(base_url + u'bob6.css', 'print')
+ head.add_css(base_url + u'bob7.css', 'print')
+ head.add_ie_css(base_url + u'bob8.css')
+ head.add_ie_css(base_url + u'bob9.css', 'print', u'[if lt IE 7]')
+ result = head.getvalue()
+ expected = u"""<head>
+<link rel="stylesheet" type="text/css" media="all" href="http://test.fr/data/bob4.css"/>
+<link rel="stylesheet" type="text/css" media="all" href="http://test.fr/data/bob5.css"/>
+<link rel="stylesheet" type="text/css" media="print" href="http://test.fr/data/bob6.css"/>
+<link rel="stylesheet" type="text/css" media="print" href="http://test.fr/data/bob7.css"/>
+<!--[if lt IE 8]>
+<link rel="stylesheet" type="text/css" media="all" href="http://test.fr/data/bob8.css"/>
+<!--[if lt IE 7]>
+<link rel="stylesheet" type="text/css" media="print" href="http://test.fr/data/bob9.css"/>
+<![endif]-->
+<script type="text/javascript" src="http://test.fr/data/bob0.js"></script>
+<script type="text/javascript" src="http://test.fr/data/bob1.js"></script>
+<script type="text/javascript" src="http://ext.com/bob2.js"></script>
+<script type="text/javascript" src="http://ext.com/bob3.js"></script>
+</head>
+"""
+ self.assertEqual(result, expected)
if __name__ == '__main__':
unittest_main()
--- a/utils.py Thu Apr 28 20:24:27 2011 +0200
+++ b/utils.py Thu Apr 28 20:41:44 2011 +0200
@@ -237,7 +237,7 @@
xhtml_safe_script_opening = u'<script type="text/javascript"><!--//--><![CDATA[//><!--\n'
xhtml_safe_script_closing = u'\n//--><!]]></script>'
- def __init__(self):
+ def __init__(self, datadir_url=None):
super(HTMLHead, self).__init__()
self.jsvars = []
self.jsfiles = []
@@ -245,6 +245,7 @@
self.ie_cssfiles = []
self.post_inlined_scripts = []
self.pagedata_unload = False
+ self.datadir_url = datadir_url
def add_raw(self, rawheader):
@@ -281,7 +282,7 @@
if jsfile not in self.jsfiles:
self.jsfiles.append(jsfile)
- def add_css(self, cssfile, media):
+ def add_css(self, cssfile, media='all'):
"""adds `cssfile` to the list of javascripts used in the webpage
This function checks if the file has already been added
@@ -301,6 +302,45 @@
self.post_inlined_scripts.append(self.js_unload_code)
self.pagedata_unload = True
+ def concat_urls(self, urls):
+ """concatenates urls into one url usable by Apache mod_concat
+
+ This method returns the url without modifying it if there is only
+ one element in the list
+ :param urls: list of local urls/filenames to concatenate
+ """
+ if len(urls) == 1:
+ return urls[0]
+ len_prefix = len(self.datadir_url)
+ concated = u','.join(url[len_prefix:] for url in urls)
+ return (u'%s??%s' % (self.datadir_url, concated))
+
+ def group_urls(self, urls_spec):
+ """parses urls_spec in order to generate concatenated urls
+ for js and css includes
+
+ This method checks if the file is local and if it shares options
+ with direct neighbors
+ :param urls_spec: entire list of urls/filenames to inspect
+ """
+ concatable = []
+ prev_islocal = False
+ prev_key = None
+ for url, key in urls_spec:
+ islocal = url.startswith(self.datadir_url)
+ if concatable and (islocal != prev_islocal or key != prev_key):
+ yield (self.concat_urls(concatable), prev_key)
+ del concatable[:]
+ if not islocal:
+ yield (url, key)
+ else:
+ concatable.append(url)
+ prev_islocal = islocal
+ prev_key = key
+ if concatable:
+ yield (self.concat_urls(concatable), prev_key)
+
+
def getvalue(self, skiphead=False):
"""reimplement getvalue to provide a consistent (and somewhat browser
optimzed cf. http://stevesouders.com/cuzillion) order in external
@@ -318,18 +358,20 @@
w(vardecl + u'\n')
w(self.xhtml_safe_script_closing)
# 2/ css files
- for cssfile, media in self.cssfiles:
+ for cssfile, media in (self.group_urls(self.cssfiles) if self.datadir_url else self.cssfiles):
w(u'<link rel="stylesheet" type="text/css" media="%s" href="%s"/>\n' %
(media, xml_escape(cssfile)))
# 3/ ie css if necessary
if self.ie_cssfiles:
- for cssfile, media, iespec in self.ie_cssfiles:
+ ie_cssfiles = ((x, (y, z)) for x, y, z in self.ie_cssfiles)
+ for cssfile, (media, iespec) in (self.group_urls(ie_cssfiles) if self.datadir_url else ie_cssfiles):
w(u'<!--%s>\n' % iespec)
w(u'<link rel="stylesheet" type="text/css" media="%s" href="%s"/>\n' %
(media, xml_escape(cssfile)))
w(u'<![endif]--> \n')
# 4/ js files
- for jsfile in self.jsfiles:
+ jsfiles = ((x, None) for x in self.jsfiles)
+ for jsfile, media in self.group_urls(jsfiles) if self.datadir_url else jsfiles:
w(u'<script type="text/javascript" src="%s"></script>\n' %
xml_escape(jsfile))
# 5/ post inlined scripts (i.e. scripts depending on other JS files)
--- a/web/request.py Thu Apr 28 20:24:27 2011 +0200
+++ b/web/request.py Thu Apr 28 20:41:44 2011 +0200
@@ -92,7 +92,7 @@
self.uiprops = vreg.config.uiprops
self.datadir_url = vreg.config.datadir_url
# raw html headers that can be added from any view
- self.html_headers = HTMLHead()
+ self.html_headers = HTMLHead(self.datadir_url)
# form parameters
self.setup_params(form)
# dictionnary that may be used to store request data that has to be
@@ -258,7 +258,7 @@
"""used by AutomaticWebTest to clear html headers between tests on
the same resultset
"""
- self.html_headers = HTMLHead()
+ self.html_headers = HTMLHead(self.datadir_url)
return self
# web state helpers #######################################################
@@ -417,7 +417,8 @@
@cached # so it's writed only once
def fckeditor_config(self):
- self.add_js('fckeditor/fckeditor.js')
+ fckeditor_url = self.build_url('fckeditor/fckeditor.js')
+ self.add_js(fckeditor_url, localfile=False)
self.html_headers.define_var('fcklang', self.lang)
self.html_headers.define_var('fckconfigpath',
self.data_url('cubicweb.fckcwconfig.js'))