[web] use uiprops value to compile css transparently, handlig cache and reloading in debug mode
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 30 Apr 2010 12:15:37 +0200
changeset 5445 4467ed43d97d
parent 5444 f7fdb5dd82f6
child 5446 b018324198d4
[web] use uiprops value to compile css transparently, handlig cache and reloading in debug mode
etwist/server.py
web/data/uiprops.py
web/propertysheet.py
web/test/unittest_propertysheet.py
web/webconfig.py
--- a/etwist/server.py	Fri Apr 30 12:14:15 2010 +0200
+++ b/etwist/server.py	Fri Apr 30 12:15:37 2010 +0200
@@ -210,6 +210,7 @@
         """Render a page from the root resource"""
         # reload modified files in debug mode
         if self.config.debugmode:
+            self.config.uiprops.reload_if_needed()
             self.appli.vreg.reload_if_needed()
         if self.config['profile']: # default profiler don't trace threads
             return self.render_request(request)
--- a/web/data/uiprops.py	Fri Apr 30 12:14:15 2010 +0200
+++ b/web/data/uiprops.py	Fri Apr 30 12:15:37 2010 +0200
@@ -1,57 +1,57 @@
 """define default ui properties"""
 
 # CSS stylesheets to include systematically in HTML headers
-STYLESHEETS =       ['%s/cubicweb.reset.css' % datadir_url,
-                     '%s/cubicweb.css' % datadir_url]
-STYLESHEETS_IE =    ['%s/cubicweb.ie.css' % datadir_url]
-STYLESHEETS_PRINT = ['%s/cubicweb.print.css' % datadir_url]
+STYLESHEETS =       [data('cubicweb.reset.css'),
+                     data('cubicweb.css')]
+STYLESHEETS_IE =    [data('cubicweb.ie.css')]
+STYLESHEETS_PRINT = [data('cubicweb.print.css')]
 
 # Javascripts files to include systematically in HTML headers
-JAVASCRIPTS = ['%s/jquery.js' % datadir_url,
-               '%s/jquery.corner.js' % datadir_url,
-               '%s/jquery.json.js' % datadir_url,
-               '%s/cubicweb.compat.js' % datadir_url,
-               '%s/cubicweb.python.js' % datadir_url,
-               '%s/cubicweb.htmlhelpers.js' % datadir_url]
+JAVASCRIPTS = [data('jquery.js'),
+               data('jquery.corner.js'),
+               data('jquery.json.js'),
+               data('cubicweb.compat.js'),
+               data('cubicweb.python.js'),
+               data('cubicweb.htmlhelpers.js')]
 
 # where is installed fckeditor
 FCKEDITOR_PATH = '/usr/share/fckeditor/'
 
 # favicon and logo for the instance
-FAVICON = '%s/favicon.ico' % datadir_url
-LOGO = '%s/logo.png' % datadir_url
+FAVICON = data('favicon.ico')
+LOGO = data('logo.png')
 
 # rss logo (link to get the rss view of a selection)
-RSS_LOGO = '%s/rss.png' % datadir_url
-RSS_LOGO_16 = '%s/feed-icon16x16.png' % datadir_url
-RSS_LOGO_32 = '%s/feed-icon32x32.png' % datadir_url
+RSS_LOGO = data('rss.png')
+RSS_LOGO_16 = data('feed-icon16x16.png')
+RSS_LOGO_32 = data('feed-icon32x32.png')
 
 # XXX cleanup resources below, some of them are probably not used
 # (at least entity types icons...)
 
 # images
-HELP = '%s/help.png' % datadir_url
-SEARCH_GO = '%s/go.png' % datadir_url
-PUCE_UP = '%s/puce_up.png' % datadir_url
-PUCE_DOWN = '%s/puce_down.png' % datadir_url
+HELP = data('help.png')
+SEARCH_GO = data('go.png')
+PUCE_UP = data('puce_up.png')
+PUCE_DOWN = data('puce_down.png')
 
 # button icons
-OK_ICON = '%s/ok.png' % datadir_url
-CANCEL_ICON = '%s/cancel.png' % datadir_url
-APPLY_ICON = '%s/plus.png' % datadir_url
-TRASH_ICON = '%s/trash_can_small.png' % datadir_url
+OK_ICON = data('ok.png')
+CANCEL_ICON = data('cancel.png')
+APPLY_ICON = data('plus.png')
+TRASH_ICON = data('trash_can_small.png')
 
 # icons for entity types
-BOOKMARK_ICON = '%s/icon_bookmark.gif' % datadir_url
-EMAILADDRESS_ICON = '%s/icon_emailaddress.gif' % datadir_url
-EUSER_ICON = '%s/icon_euser.gif' % datadir_url
-STATE_ICON = '%s/icon_state.gif' % datadir_url
+BOOKMARK_ICON = data('icon_bookmark.gif')
+EMAILADDRESS_ICON = data('icon_emailaddress.gif')
+EUSER_ICON = data('icon_euser.gif')
+STATE_ICON = data('icon_state.gif')
 
 # other icons
-CALENDAR_ICON = '%s/calendar.gif' % datadir_url
-CANCEL_EMAIL_ICON = '%s/sendcancel.png' % datadir_url
-SEND_EMAIL_ICON = '%s/sendok.png' % datadir_url
-DOWNLOAD_ICON = '%s/download.gif' % datadir_url
-UPLOAD_ICON = '%s/upload.gif' % datadir_url
-GMARKER_ICON = '%s/gmap_blue_marker.png' % datadir_url
-UP_ICON = '%s/up.gif' % datadir_url
+CALENDAR_ICON = data('calendar.gif')
+CANCEL_EMAIL_ICON = data('sendcancel.png')
+SEND_EMAIL_ICON = data('sendok.png')
+DOWNLOAD_ICON = data('download.gif')
+UPLOAD_ICON = data('upload.gif')
+GMARKER_ICON = data('gmap_blue_marker.png')
+UP_ICON = data('up.gif')
--- a/web/propertysheet.py	Fri Apr 30 12:14:15 2010 +0200
+++ b/web/propertysheet.py	Fri Apr 30 12:15:37 2010 +0200
@@ -19,13 +19,81 @@
 
 __docformat__ = "restructuredtext en"
 
+import re
+import os
+import os.path as osp
+
+
 class PropertySheet(dict):
-    def __init__(self, **context):
+    def __init__(self, cache_directory, **context):
+        self._cache_directory = cache_directory
         self._context = context
+        self.reset()
         context['sheet'] = self
+        self._percent_rgx = re.compile('%(?!\()')
+
+    def reset(self):
+        self.clear()
+        self._ordered_propfiles = []
+        self._propfile_mtime = {}
+        self._sourcefile_mtime = {}
+        self._cache = {}
 
     def load(self, fpath):
         scriptglobals = self._context.copy()
         scriptglobals['__file__'] = fpath
         execfile(fpath, scriptglobals, self)
+        self._propfile_mtime[fpath] = os.stat(fpath)[-2]
+        self._ordered_propfiles.append(fpath)
 
+    def need_reload(self):
+        for fpath, mtime in self._propfile_mtime.iteritems():
+            if os.stat(fpath)[-2] > mtime:
+                return True
+        for rid, (directory, mtime) in self._cache.items():
+            if os.stat(osp.join(directory, rid))[-2] > mtime:
+                del self._cache[rid]
+        return False
+
+    def reload(self):
+        ordered_files = self._ordered_propfiles
+        self.reset()
+        for fpath in ordered_files:
+            self.load(fpath)
+
+    def reload_if_needed(self):
+        if self.need_reload():
+            self.reload()
+
+    def process_resource(self, rdirectory, rid):
+        try:
+            return self._cache[rid][0]
+        except KeyError:
+            cachefile = osp.join(self._cache_directory, rid)
+            self.debug('caching processed css %s/%s into %s',
+                       rdirectory, rid, cachefile)
+            rcachedir = osp.dirname(cachefile)
+            if not osp.exists(rcachedir):
+                os.makedirs(rcachedir)
+            sourcefile = osp.join(rdirectory, rid)
+            content = file(sourcefile).read()
+            # XXX replace % not followed by a paren by %% to avoid having to do
+            # this in the source css file ?
+            try:
+                content = self.compile(content)
+            except ValueError, ex:
+                self.error("can't process %s/%s: %s", rdirectory, rid, ex)
+            else:
+                stream = file(cachefile, 'w')
+                stream.write(content)
+                stream.close()
+                rdirectory = self._cache_directory
+            self._cache[rid] = (rdirectory, os.stat(sourcefile)[-2])
+            return rdirectory
+
+    def compile(self, content):
+        return self._percent_rgx.sub('%%', content) % self
+
+from cubicweb.web import LOGGER
+from logilab.common.logging_ext import set_log_methods
+set_log_methods(PropertySheet, LOGGER)
--- a/web/test/unittest_propertysheet.py	Fri Apr 30 12:14:15 2010 +0200
+++ b/web/test/unittest_propertysheet.py	Fri Apr 30 12:15:37 2010 +0200
@@ -6,7 +6,7 @@
 class PropertySheetTC(TestCase):
 
     def test(self):
-        ps = PropertySheet(datadir_url='http://cwtest.com')
+        ps = PropertySheet(None, datadir_url='http://cwtest.com')
         ps.load(join(DATADIR, 'sheet1.py'))
         ps.load(join(DATADIR, 'sheet2.py'))
         # defined by sheet1
@@ -18,7 +18,8 @@
         # defined by sheet1, extended by sheet2
         self.assertEquals(ps['stylesheets'], ['http://cwtest.com/cubicweb.css',
                                               'http://cwtest.com/mycube.css'])
-
+        self.assertEquals(ps.compile('a {bgcolor: %(bgcolor)s; size: 1%;}'),
+                          'a {bgcolor: #FFFFFF; size: 1%;}')
 
 if __name__ == '__main__':
     unittest_main()
--- a/web/webconfig.py	Fri Apr 30 12:14:15 2010 +0200
+++ b/web/webconfig.py	Fri Apr 30 12:15:37 2010 +0200
@@ -272,6 +272,9 @@
         path = [self.apphome] + self.cubes_path() + [join(self.shared_dir())]
         for directory in path:
             if exists(join(directory, rdirectory, rid)):
+                if rdirectory == 'data' and rid.endswith('.css'):
+                    return self.uiprops.process_resource(join(directory, rdirectory),
+                                                         rid)
                 return join(directory, rdirectory)
 
     def locate_all_files(self, rid, rdirectory='wdoc'):
@@ -309,7 +312,10 @@
     def _build_ui_properties(self):
         # self.datadir_url[:-1] to remove trailing /
         from cubicweb.web.propertysheet import PropertySheet
-        self.uiprops = PropertySheet(datadir_url=self.datadir_url[:-1])
+        self.uiprops = PropertySheet(
+            join(self.appdatahome, 'uicache'),
+            data=lambda x: self.datadir_url + x,
+            datadir_url=self.datadir_url[:-1])
         libuiprops = join(self.shared_dir(), 'data', 'uiprops.py')
         self.uiprops.load(libuiprops)
         for path in reversed([self.apphome] + self.cubes_path()):