[etwist] Generates apache's mod_concat-like urls for js and css
authorQuentin Roquefort <roquefort.quentin@gmail.com>
Thu, 28 Apr 2011 20:41:44 +0200
changeset 7277 acd7f0e9f276
parent 7276 f9a68136eb87
child 7278 1144a2d90314
[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
etwist/server.py
test/unittest_utils.py
utils.py
web/request.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/??'
--- 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'))