# HG changeset patch # User Quentin Roquefort # Date 1304016104 -7200 # Node ID acd7f0e9f2762f55c9501ca769d263d6a69fb39d # Parent f9a68136eb878ccbb094efd3732fa54df3ebeb96 [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 diff -r f9a68136eb87 -r acd7f0e9f276 etwist/server.py --- 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/??' diff -r f9a68136eb87 -r acd7f0e9f276 test/unittest_utils.py --- 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""" + + + + + + + +""" + 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""" + + + + + + + + + + +""" + self.assertEqual(result, expected) if __name__ == '__main__': unittest_main() diff -r f9a68136eb87 -r acd7f0e9f276 utils.py --- 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'' - 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'\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' \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'\n' % xml_escape(jsfile)) # 5/ post inlined scripts (i.e. scripts depending on other JS files) diff -r f9a68136eb87 -r acd7f0e9f276 web/request.py --- 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'))