[webconfig] introduce property sheets. Use them to replace external_resources
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 30 Apr 2010 12:14:15 +0200
changeset 5444 f7fdb5dd82f6
parent 5443 f299ee54d7e0
child 5445 4467ed43d97d
[webconfig] introduce property sheets. Use them to replace external_resources
cwconfig.py
cwvreg.py
devtools/fake.py
server/session.py
web/data/external_resources
web/data/uiprops.py
web/formwidgets.py
web/propertysheet.py
web/request.py
web/test/data/sheet1.py
web/test/data/sheet2.py
web/test/unittest_propertysheet.py
web/test/unittest_views_basecontrollers.py
web/test/unittest_webconfig.py
web/views/basecomponents.py
web/views/basecontrollers.py
web/views/basetemplates.py
web/views/idownloadable.py
web/views/igeocodable.py
web/views/schema.py
web/views/tableview.py
web/views/xmlrss.py
web/webconfig.py
--- a/cwconfig.py	Thu Apr 29 14:21:59 2010 +0200
+++ b/cwconfig.py	Fri Apr 30 12:14:15 2010 +0200
@@ -664,7 +664,8 @@
         self.debugmode = debugmode
         self.adjust_sys_path()
         self.load_defaults()
-        self.translations = {}
+        # will be properly initialized later by _gettext_init
+        self.translations = {'en': (unicode, lambda ctx, msgid: unicode(msgid) )}
         # don't register ReStructured Text directives by simple import, avoid pb
         # with eg sphinx.
         # XXX should be done properly with a function from cw.uicfg
@@ -985,7 +986,7 @@
         super(CubicWebConfiguration, self).load_configuration()
         if self.apphome and self.set_language:
             # init gettext
-            self._set_language()
+            self._gettext_init()
 
     def init_log(self, logthreshold=None, force=False):
         """init the log service"""
@@ -1013,7 +1014,7 @@
             if lang != 'en':
                 yield lang
 
-    def _set_language(self):
+    def _gettext_init(self):
         """set language for gettext"""
         from gettext import translation
         path = join(self.apphome, 'i18n')
--- a/cwvreg.py	Thu Apr 29 14:21:59 2010 +0200
+++ b/cwvreg.py	Fri Apr 30 12:14:15 2010 +0200
@@ -449,7 +449,6 @@
         super(CubicWebVRegistry, self).__init__(config)
         self.schema = None
         self.initialized = False
-        self.reset()
         # XXX give force_reload (or refactor [re]loading...)
         if self.config.mode != 'test':
             # don't clear rtags during test, this may cause breakage with
--- a/devtools/fake.py	Thu Apr 29 14:21:59 2010 +0200
+++ b/devtools/fake.py	Fri Apr 30 12:14:15 2010 +0200
@@ -39,6 +39,7 @@
         self['uid'] = None
         self['base-url'] = BASE_URL
         self['rql-cache-size'] = 100
+        self.datadir_url = BASE_URL + 'data/'
 
     def cubes(self, expand=False):
         return self._cubes
--- a/server/session.py	Thu Apr 29 14:21:59 2010 +0200
+++ b/server/session.py	Fri Apr 30 12:14:15 2010 +0200
@@ -302,16 +302,15 @@
 
     def set_language(self, language):
         """i18n configuration for translation"""
-        vreg = self.vreg
         language = language or self.user.property_value('ui.language')
         try:
-            gettext, pgettext = vreg.config.translations[language]
+            gettext, pgettext = self.vreg.config.translations[language]
             self._ = self.__ = gettext
             self.pgettext = pgettext
         except KeyError:
-            language = vreg.property_value('ui.language')
+            language = self.vreg.property_value('ui.language')
             try:
-                gettext, pgettext = vreg.config.translations[language]
+                gettext, pgettext = self.vreg.config.translations[language]
                 self._ = self.__ = gettext
                 self.pgettext = pgettext
             except KeyError:
--- a/web/data/external_resources	Thu Apr 29 14:21:59 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,62 +0,0 @@
-# -*- shell-script -*-
-###############################################################################
-#
-# external resources file for core library resources
-#
-# Commented values are default values used by the application.
-#
-###############################################################################
-
-
-# CSS stylesheets to include in HTML headers
-STYLESHEETS = DATADIR/cubicweb.reset.css, DATADIR/cubicweb.css
-
-# CSS stylesheets for print
-#STYLESHEETS_PRINT = DATADIR/cubicweb.print.css
-
-#CSS stylesheets for IE
-#IE_STYLESHEETS = DATADIR/cubicweb.ie.css
-
-# Javascripts files to include in HTML headers
-#JAVASCRIPTS = DATADIR/jquery.js, DATADIR/cubicweb.python.js, DATADIR/jquery.json.js, DATADIR/cubicweb.compat.js, DATADIR/cubicweb.htmlhelpers.js
-
-# path to favicon (relative to the application main script, seen as a
-# directory, hence .. when you are not using an absolute path)
-#FAVICON = DATADIR/favicon.ico
-
-# path to the logo (relative to the application main script, seen as a
-# directory, hence .. when you are not using an absolute path)
-LOGO = DATADIR/logo.png
-
-# rss logo (link to get the rss view of a selection)
-RSS_LOGO = DATADIR/rss.png
-RSS_LOGO_16 = DATADIR/feed-icon16x16.png
-RSS_LOGO_32 = DATADIR/feed-icon32x32.png
-
-# path to search image
-SEARCH_GO =  DATADIR/go.png
-
-#FCKEDITOR_PATH = /usr/share/fckeditor/
-
-PUCE_UP = DATADIR/puce_up.png
-PUCE_DOWN = DATADIR/puce_down.png
-
-# icons for entity types
-BOOKMARK_ICON = DATADIR/icon_bookmark.gif
-EMAILADDRESS_ICON = DATADIR/icon_emailaddress.gif
-EUSER_ICON = DATADIR/icon_euser.gif
-STATE_ICON = DATADIR/icon_state.gif
-
-# other icons
-CALENDAR_ICON = DATADIR/calendar.gif
-CANCEL_EMAIL_ICON = DATADIR/sendcancel.png
-SEND_EMAIL_ICON = DATADIR/sendok.png
-DOWNLOAD_ICON = DATADIR/download.gif
-UPLOAD_ICON = DATADIR/upload.gif
-GMARKER_ICON = DATADIR/gmap_blue_marker.png
-UP_ICON = DATADIR/up.gif
-
-OK_ICON = DATADIR/ok.png
-CANCEL_ICON = DATADIR/cancel.png
-APPLY_ICON = DATADIR/plus.png
-TRASH_ICON = DATADIR/trash_can_small.png
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/uiprops.py	Fri Apr 30 12:14:15 2010 +0200
@@ -0,0 +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]
+
+# 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]
+
+# 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
+
+# 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
+
+# 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
+
+# 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
+
+# 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
+
+# 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
--- a/web/formwidgets.py	Thu Apr 29 14:21:59 2010 +0200
+++ b/web/formwidgets.py	Fri Apr 30 12:14:15 2010 +0200
@@ -550,7 +550,7 @@
         return (u"""<a onclick="toggleCalendar('%s', '%s', %s, %s);" class="calhelper">
 <img src="%s" title="%s" alt="" /></a><div class="calpopup hidden" id="%s"></div>"""
                 % (helperid, inputid, year, month,
-                   form._cw.external_resource('CALENDAR_ICON'),
+                   form._cw.vreg.config.uiprops['CALENDAR_ICON'],
                    form._cw._('calendar'), helperid) )
 
 
@@ -574,7 +574,7 @@
         req.add_onload(u'jqNode("%s").datepicker('
                        '{buttonImage: "%s", dateFormat: "%s", firstDay: 1,'
                        ' showOn: "button", buttonImageOnly: true})' % (
-                           domid, req.external_resource('CALENDAR_ICON'), fmt))
+                           domid, req.vreg.config.uiprops['CALENDAR_ICON'], fmt))
         if self.datestr is None:
             value = self.values(form, field)[0]
         else:
@@ -954,7 +954,7 @@
         if self.settabindex and not 'tabindex' in attrs:
             attrs['tabindex'] = form._cw.next_tabindex()
         if self.icon:
-            img = tags.img(src=form._cw.external_resource(self.icon), alt=self.icon)
+            img = tags.img(src=form._cw.vreg.config.uiprops[self.icon], alt=self.icon)
         else:
             img = u''
         return tags.button(img + xml_escape(label), escapecontent=False,
@@ -985,7 +985,7 @@
 
     def render(self, form, field=None, renderer=None):
         label = form._cw._(self.label)
-        imgsrc = form._cw.external_resource(self.imgressource)
+        imgsrc = form._cw.vreg.config.uiprops[self.imgressource]
         return '<a id="%(domid)s" href="%(href)s">'\
                '<img src="%(imgsrc)s" alt="%(label)s"/>%(label)s</a>' % {
             'label': label, 'imgsrc': imgsrc,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/propertysheet.py	Fri Apr 30 12:14:15 2010 +0200
@@ -0,0 +1,31 @@
+# copyright 2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""property sheets allowing configuration of the web ui"""
+
+__docformat__ = "restructuredtext en"
+
+class PropertySheet(dict):
+    def __init__(self, **context):
+        self._context = context
+        context['sheet'] = self
+
+    def load(self, fpath):
+        scriptglobals = self._context.copy()
+        scriptglobals['__file__'] = fpath
+        execfile(fpath, scriptglobals, self)
+
--- a/web/request.py	Thu Apr 29 14:21:59 2010 +0200
+++ b/web/request.py	Fri Apr 30 12:14:15 2010 +0200
@@ -99,7 +99,7 @@
         self.next_tabindex = self.tabindexgen.next
         # page id, set by htmlheader template
         self.pageid = None
-        self.datadir_url = self._datadir_url()
+        self.datadir_url = vreg.config.datadir_url
         self._set_pageid()
         # prepare output header
         self.headers_out = Headers()
@@ -589,10 +589,6 @@
         """return currently accessed url"""
         return self.base_url() + self.relative_path(includeparams)
 
-    def _datadir_url(self):
-        """return url of the instance's data directory"""
-        return self.base_url() + 'data%s/' % self.vreg.config.instance_md5_version()
-
     def selected(self, url):
         """return True if the url is equivalent to currently accessed url"""
         reqpath = self.relative_path().lower()
@@ -618,25 +614,6 @@
             return controller
         return 'view'
 
-    def external_resource(self, rid, default=_MARKER):
-        """return a path to an external resource, using its identifier
-
-        raise KeyError  if the resource is not defined
-        """
-        try:
-            value = self.vreg.config.ext_resources[rid]
-        except KeyError:
-            if default is _MARKER:
-                raise
-            return default
-        if value is None:
-            return None
-        baseurl = self.datadir_url[:-1] # remove trailing /
-        if isinstance(value, list):
-            return [v.replace('DATADIR', baseurl) for v in value]
-        return value.replace('DATADIR', baseurl)
-    external_resource = cached(external_resource, keyarg=1)
-
     def validate_cache(self):
         """raise a `DirectResponse` exception if a cached page along the way
         exists and is still usable.
@@ -712,12 +689,6 @@
                            auth, ex.__class__.__name__, ex)
         return None, None
 
-    @deprecated("[3.4] use parse_accept_header('Accept-Language')")
-    def header_accept_language(self):
-        """returns an ordered list of preferred languages"""
-        return [value.split('-')[0] for value in
-                self.parse_accept_header('Accept-Language')]
-
     def parse_accept_header(self, header):
         """returns an ordered list of preferred languages"""
         accepteds = self.get_header(header, '')
@@ -823,5 +794,25 @@
                     u'<div xmlns="http://www.w3.org/1999/xhtml" xmlns:cubicweb="http://www.logilab.org/2008/cubicweb">')
         return u'<div>'
 
+    @deprecated('[3.9] use req.vreg.config.uiprops[rid]')
+    def external_resource(self, rid, default=_MARKER):
+        """return a path to an external resource, using its identifier
+
+        raise `KeyError` if the resource is not defined
+        """
+        try:
+            return self.vreg.config.uiprops[rid]
+        except KeyError:
+            if default is _MARKER:
+                raise
+            return default
+
+    @deprecated("[3.4] use parse_accept_header('Accept-Language')")
+    def header_accept_language(self):
+        """returns an ordered list of preferred languages"""
+        return [value.split('-')[0] for value in
+                self.parse_accept_header('Accept-Language')]
+
+
 from cubicweb import set_log_methods
 set_log_methods(CubicWebRequestBase, LOGGER)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/data/sheet1.py	Fri Apr 30 12:14:15 2010 +0200
@@ -0,0 +1,3 @@
+bgcolor = '#000000'
+stylesheets = ['%s/cubicweb.css' % datadir_url]
+logo = '%s/logo.png' % datadir_url
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/data/sheet2.py	Fri Apr 30 12:14:15 2010 +0200
@@ -0,0 +1,3 @@
+fontcolor = 'black'
+bgcolor = '#FFFFFF'
+stylesheets = sheet['stylesheets'] + ['%s/mycube.css' % datadir_url]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_propertysheet.py	Fri Apr 30 12:14:15 2010 +0200
@@ -0,0 +1,24 @@
+from os.path import join, dirname
+from logilab.common.testlib import TestCase, unittest_main
+from cubicweb.web.propertysheet import *
+
+DATADIR = join(dirname(__file__), 'data')
+class PropertySheetTC(TestCase):
+
+    def test(self):
+        ps = PropertySheet(datadir_url='http://cwtest.com')
+        ps.load(join(DATADIR, 'sheet1.py'))
+        ps.load(join(DATADIR, 'sheet2.py'))
+        # defined by sheet1
+        self.assertEquals(ps['logo'], 'http://cwtest.com/logo.png')
+        # defined by sheet1, overriden by sheet2
+        self.assertEquals(ps['bgcolor'], '#FFFFFF')
+        # defined by sheet2
+        self.assertEquals(ps['fontcolor'], 'black')
+        # defined by sheet1, extended by sheet2
+        self.assertEquals(ps['stylesheets'], ['http://cwtest.com/cubicweb.css',
+                                              'http://cwtest.com/mycube.css'])
+
+
+if __name__ == '__main__':
+    unittest_main()
--- a/web/test/unittest_views_basecontrollers.py	Thu Apr 29 14:21:59 2010 +0200
+++ b/web/test/unittest_views_basecontrollers.py	Fri Apr 30 12:14:15 2010 +0200
@@ -643,7 +643,7 @@
     # silly tests
     def test_external_resource(self):
         self.assertEquals(self.remote_call('external_resource', 'RSS_LOGO')[0],
-                          json.dumps(self.request().external_resource('RSS_LOGO')))
+                          json.dumps(self.config.uiprops['RSS_LOGO']))
     def test_i18n(self):
         self.assertEquals(self.remote_call('i18n', ['bimboom'])[0],
                           json.dumps(['bimboom']))
--- a/web/test/unittest_webconfig.py	Thu Apr 29 14:21:59 2010 +0200
+++ b/web/test/unittest_webconfig.py	Fri Apr 30 12:14:15 2010 +0200
@@ -33,15 +33,14 @@
     def test_nonregr_print_css_as_list(self):
         """make sure PRINT_CSS *must* is a list"""
         config = self.config
-        req = fake.FakeRequest()
-        print_css = req.external_resource('STYLESHEETS_PRINT')
+        print_css = config.uiprops['STYLESHEETS_PRINT']
         self.failUnless(isinstance(print_css, list))
-        ie_css = req.external_resource('IE_STYLESHEETS')
+        ie_css = config.uiprops['STYLESHEETS_IE']
         self.failUnless(isinstance(ie_css, list))
 
     def test_locate_resource(self):
-        self.failUnless('FILE_ICON' in self.config.ext_resources)
-        rname = self.config.ext_resources['FILE_ICON'].replace('DATADIR/', '')
+        self.failUnless('FILE_ICON' in self.config.uiprops)
+        rname = self.config.uiprops['FILE_ICON'].replace(self.config.datadir_url, '')
         self.failUnless('file' in self.config.locate_resource(rname).split(os.sep))
         cubicwebcsspath = self.config.locate_resource('cubicweb.css').split(os.sep)
         self.failUnless('web' in cubicwebcsspath or 'shared' in cubicwebcsspath) # 'shared' if tests under apycot
--- a/web/views/basecomponents.py	Thu Apr 29 14:21:59 2010 +0200
+++ b/web/views/basecomponents.py	Fri Apr 30 12:14:15 2010 +0200
@@ -79,7 +79,7 @@
 
     def call(self):
         self.w(u'<a href="%s"><img class="logo" src="%s" alt="logo"/></a>'
-               % (self._cw.base_url(), self._cw.external_resource('LOGO')))
+               % (self._cw.base_url(), self._cw.vreg.config.uiprops['LOGO']))
 
 
 class ApplHelp(component.Component):
--- a/web/views/basecontrollers.py	Thu Apr 29 14:21:59 2010 +0200
+++ b/web/views/basecontrollers.py	Fri Apr 30 12:14:15 2010 +0200
@@ -481,7 +481,7 @@
     @jsonize
     def js_external_resource(self, resource):
         """returns the URL of the external resource named `resource`"""
-        return self._cw.external_resource(resource)
+        return self._cw.vreg.config.uiprops[resource]
 
     @check_pageid
     @jsonize
--- a/web/views/basetemplates.py	Thu Apr 29 14:21:59 2010 +0200
+++ b/web/views/basetemplates.py	Fri Apr 30 12:14:15 2010 +0200
@@ -294,22 +294,22 @@
         self.alternates()
 
     def favicon(self):
-        favicon = self._cw.external_resource('FAVICON', None)
+        favicon = self._cw.vreg.config.uiprops.get('FAVICON', None)
         if favicon:
             self.whead(u'<link rel="shortcut icon" href="%s"/>\n' % favicon)
 
     def stylesheets(self):
         req = self._cw
         add_css = req.add_css
-        for css in req.external_resource('STYLESHEETS'):
+        for css in req.vreg.config.uiprops['STYLESHEETS']:
             add_css(css, localfile=False)
-        for css in req.external_resource('STYLESHEETS_PRINT'):
+        for css in req.vreg.config.uiprops['STYLESHEETS_PRINT']:
             add_css(css, u'print', localfile=False)
-        for css in req.external_resource('IE_STYLESHEETS'):
+        for css in req.vreg.config.uiprops['STYLESHEETS_IE']:
             add_css(css, localfile=False, ieonly=True)
 
     def javascripts(self):
-        for jscript in self._cw.external_resource('JAVASCRIPTS'):
+        for jscript in self._cw.vreg.config.uiprops['JAVASCRIPTS']:
             self._cw.add_js(jscript, localfile=False)
 
     def alternates(self):
--- a/web/views/idownloadable.py	Thu Apr 29 14:21:59 2010 +0200
+++ b/web/views/idownloadable.py	Fri Apr 30 12:14:15 2010 +0200
@@ -48,7 +48,7 @@
     w(u'<div class="sideBox downloadBox"><div class="sideBoxBody">')
     w(u'<a href="%s"><img src="%s" alt="%s"/> %s</a>'
       % (xml_escape(entity.download_url()),
-         req.external_resource('DOWNLOAD_ICON'),
+         req.vreg.config.uiprops['DOWNLOAD_ICON'],
          _('download icon'), xml_escape(label or entity.dc_title())))
     w(u'%s</div>' % footer)
     w(u'</div></div>\n')
--- a/web/views/igeocodable.py	Thu Apr 29 14:21:59 2010 +0200
+++ b/web/views/igeocodable.py	Fri Apr 30 12:14:15 2010 +0200
@@ -59,7 +59,7 @@
         if hasattr(entity, 'marker_icon'):
             icon = entity.marker_icon()
         else:
-            icon = (self._cw.external_resource('GMARKER_ICON'), (20, 34), (4, 34), None)
+            icon = (self._cw.vreg.config.uiprops['GMARKER_ICON'], (20, 34), (4, 34), None)
         return {'latitude': entity.latitude, 'longitude': entity.longitude,
                 'title': entity.dc_long_title(),
                 #icon defines : (icon._url, icon.size,  icon.iconAncho', icon.shadow)
--- a/web/views/schema.py	Thu Apr 29 14:21:59 2010 +0200
+++ b/web/views/schema.py	Fri Apr 30 12:14:15 2010 +0200
@@ -248,7 +248,7 @@
                 eschema.type, self._cw.build_url('cwetype/%s' % eschema.type),
                 eschema.type, _(eschema.type)))
             self.w(u'<a href="%s#schema_security"><img src="%s" alt="%s"/></a>' % (
-                url,  self._cw.external_resource('UP_ICON'), _('up')))
+                url,  self._cw.vreg.config.uiprops['UP_ICON'], _('up')))
             self.w(u'</h3>')
             self.w(u'<div style="margin: 0px 1.5em">')
             self.permissions_table(eschema)
@@ -277,7 +277,7 @@
                 rschema.type, self._cw.build_url('cwrtype/%s' % rschema.type),
                 rschema.type, _(rschema.type)))
             self.w(u'<a href="%s#schema_security"><img src="%s" alt="%s"/></a>' % (
-                url,  self._cw.external_resource('UP_ICON'), _('up')))
+                url,  self._cw.vreg.config.uiprops['UP_ICON'], _('up')))
             self.w(u'</h3>')
             self.grouped_permissions_table(rschema)
 
--- a/web/views/tableview.py	Thu Apr 29 14:21:59 2010 +0200
+++ b/web/views/tableview.py	Fri Apr 30 12:14:15 2010 +0200
@@ -204,7 +204,7 @@
 
     def render_actions(self, divid, actions):
         box = MenuWidget('', 'tableActionsBox', _class='', islist=False)
-        label = tags.img(src=self._cw.external_resource('PUCE_DOWN'),
+        label = tags.img(src=self._cw.vreg.config.uiprops['PUCE_DOWN'],
                          alt=xml_escape(self._cw._('action(s) on this selection')))
         menu = PopupBoxMenu(label, isitem=False, link_class='actionsBox',
                             ident='%sActions' % divid)
--- a/web/views/xmlrss.py	Thu Apr 29 14:21:59 2010 +0200
+++ b/web/views/xmlrss.py	Fri Apr 30 12:14:15 2010 +0200
@@ -147,7 +147,7 @@
 
     def call(self, **kwargs):
         try:
-            rss = self._cw.external_resource('RSS_LOGO')
+            rss = self._cw.vreg.config.uiprops['RSS_LOGO']
         except KeyError:
             self.error('missing RSS_LOGO external resource')
             return
--- a/web/webconfig.py	Thu Apr 29 14:21:59 2010 +0200
+++ b/web/webconfig.py	Fri Apr 30 12:14:15 2010 +0200
@@ -23,6 +23,7 @@
 
 import os
 from os.path import join, exists, split
+from warnings import warn
 
 from logilab.common.decorators import cached
 
@@ -208,7 +209,7 @@
         ))
 
     def fckeditor_installed(self):
-        return exists(self.ext_resources['FCKEDITOR_PATH'])
+        return exists(self.uiprops['FCKEDITOR_PATH'])
 
     def eproperty_definitions(self):
         for key, pdef in super(WebConfiguration, self).eproperty_definitions():
@@ -239,30 +240,6 @@
     def vc_config(self):
         return self.repository().get_versions()
 
-    # mapping to external resources (id -> path) (`external_resources` file) ##
-    ext_resources = {
-        'FAVICON':  'DATADIR/favicon.ico',
-        'LOGO':     'DATADIR/logo.png',
-        'RSS_LOGO': 'DATADIR/rss.png',
-        'HELP':     'DATADIR/help.png',
-        'CALENDAR_ICON': 'DATADIR/calendar.gif',
-        'SEARCH_GO':'DATADIR/go.png',
-
-        'FCKEDITOR_PATH':  '/usr/share/fckeditor/',
-
-        'IE_STYLESHEETS':    ['DATADIR/cubicweb.ie.css'],
-        'STYLESHEETS':       ['DATADIR/cubicweb.css'],
-        'STYLESHEETS_PRINT': ['DATADIR/cubicweb.print.css'],
-
-        'JAVASCRIPTS':       ['DATADIR/jquery.js',
-                              'DATADIR/jquery.corner.js',
-                              'DATADIR/jquery.json.js',
-                              'DATADIR/cubicweb.compat.js',
-                              'DATADIR/cubicweb.python.js',
-                              'DATADIR/cubicweb.htmlhelpers.js'],
-        }
-
-
     def anonymous_user(self):
         """return a login and password to use for anonymous users. None
         may be returned for both if anonymous connections are not allowed
@@ -278,7 +255,7 @@
 
     def has_resource(self, rid):
         """return true if an external resource is defined"""
-        return bool(self.ext_resources.get(rid))
+        return bool(self.uiprops.get(rid))
 
     @cached
     def locate_resource(self, rid):
@@ -310,7 +287,7 @@
         super(WebConfiguration, self).load_configuration()
         # load external resources definition
         self._init_base_url()
-        self._build_ext_resources()
+        self._build_ui_properties()
 
     def _init_base_url(self):
         # normalize base url(s)
@@ -324,25 +301,40 @@
             httpsurl += '/'
             if not self.repairing:
                 self.global_set_option('https-url', httpsurl)
+        if self.debugmode:
+            self.datadir_url = baseurl + 'data/'
+        else:
+            self.datadir_url = baseurl + 'data%s/' % self.instance_md5_version()
 
-    def _build_ext_resources(self):
-        libresourcesfile = join(self.shared_dir(), 'data', 'external_resources')
-        self.ext_resources.update(read_config(libresourcesfile))
+    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])
+        libuiprops = join(self.shared_dir(), 'data', 'uiprops.py')
+        self.uiprops.load(libuiprops)
         for path in reversed([self.apphome] + self.cubes_path()):
-            resourcesfile = join(path, 'data', 'external_resources')
-            if exists(resourcesfile):
-                self.debug('loading %s', resourcesfile)
-                self.ext_resources.update(read_config(resourcesfile))
-        resourcesfile = join(self.apphome, 'external_resources')
+            self._load_ui_properties(join(path, 'data'))
+        self._load_ui_properties(self.apphome)
+
+    def _load_ui_properties(self, path):
+        resourcesfile = join(path, 'external_resources')
         if exists(resourcesfile):
-            self.debug('loading %s', resourcesfile)
-            self.ext_resources.update(read_config(resourcesfile))
-        for resource in ('STYLESHEETS', 'STYLESHEETS_PRINT',
-                         'IE_STYLESHEETS', 'JAVASCRIPTS'):
-            val = self.ext_resources[resource]
-            if isinstance(val, str):
-                files = [w.strip() for w in val.split(',') if w.strip()]
-                self.ext_resources[resource] = files
+            warn('[3.9] %s file is deprecated, use an uiprops.py file'
+                 % resourcesfile, DeprecationWarning)
+            for rid, val in read_config(resourcesfile).iteritems():
+                if rid in ('STYLESHEETS', 'STYLESHEETS_PRINT',
+                           'IE_STYLESHEETS', 'JAVASCRIPTS'):
+                    val = [w.strip().replace('DATADIR/', self.datadir_url)
+                           for w in val.split(',') if w.strip()]
+                    if rid == 'IE_STYLESHEETS':
+                        rid = 'STYLESHEETS_IE'
+                else:
+                    val = val.strip().replace('DATADIR/', self.datadir_url)
+                self.uiprops[rid] = val
+        uipropsfile = join(path, 'uiprops.py')
+        if exists(uipropsfile):
+            self.debug('loading %s', uipropsfile)
+            self.uiprops.load(uipropsfile)
 
     # static files handling ###################################################