diff --git a/openerp/addons/base/__openerp__.py b/openerp/addons/base/__openerp__.py
index 3d0d3e4dda4..d38ac7ceed1 100644
--- a/openerp/addons/base/__openerp__.py
+++ b/openerp/addons/base/__openerp__.py
@@ -99,9 +99,5 @@ The kernel of OpenERP, needed for all installation.
],
'installable': True,
'auto_install': True,
- 'css': ['static/src/css/modules.css'],
- 'js': [
- 'static/src/js/apps.js',
- ],
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py
index 4608f63bd49..798d0e5fe24 100644
--- a/openerp/addons/base/ir/ir_qweb.py
+++ b/openerp/addons/base/ir/ir_qweb.py
@@ -2,19 +2,27 @@
import collections
import cStringIO
import datetime
+import hashlib
import json
import logging
import math
+import os
import re
import sys
import xml # FIXME use lxml and etree
+import itertools
+import lxml.html
+from urlparse import urlparse
import babel
import babel.dates
-import werkzeug.utils
+import werkzeug
from PIL import Image
+import openerp.http
import openerp.tools
+import openerp.tools.func
+import openerp.tools.lru
from openerp.tools.safe_eval import safe_eval as eval
from openerp.osv import osv, orm, fields
from openerp.tools.translate import _
@@ -396,6 +404,22 @@ class QWeb(orm.AbstractModel):
pass
return self.render(cr, uid, template, d)
+ def render_tag_call_assets(self, element, template_attributes, generated_attributes, qwebcontext):
+ """ This special 't-call' tag can be used in order to aggregate/minify javascript and css assets"""
+ name = template_attributes['call-assets']
+
+ # Backward compatibility hack for manifest usage
+ qwebcontext['manifest_list'] = openerp.addons.web.controllers.main.manifest_list
+
+ d = qwebcontext.copy()
+ d.context['inherit_branding'] = False
+ content = self.render_tag_call(
+ element, {'call': name}, generated_attributes, d)
+ if qwebcontext.get('debug'):
+ return content
+ bundle = AssetsBundle(name, html=content)
+ return bundle.to_html()
+
def render_tag_set(self, element, template_attributes, generated_attributes, qwebcontext):
if "value" in template_attributes:
qwebcontext[template_attributes["set"]] = self.eval_object(template_attributes["value"], qwebcontext)
@@ -821,7 +845,6 @@ class RelativeDatetimeConverter(osv.AbstractModel):
return babel.dates.format_timedelta(
value - reference, add_direction=True, locale=locale)
-
class Contact(orm.AbstractModel):
_name = 'ir.qweb.field.contact'
_inherit = 'ir.qweb.field.many2one'
@@ -951,4 +974,233 @@ def get_field_type(column, options):
"""
return options.get('widget', column._type)
+class AssetsBundle(object):
+ cache = openerp.tools.lru.LRU(32)
+ rx_css_import = re.compile("(@import[^;{]+;?)", re.M)
+
+ def __init__(self, xmlid, html=None, debug=False):
+ self.debug = debug
+ self.xmlid = xmlid
+ self.javascripts = []
+ self.stylesheets = []
+ self.remains = []
+ self._checksum = None
+ if html:
+ self.parse(html)
+
+ def parse(self, html):
+ fragments = lxml.html.fragments_fromstring(html)
+ for el in fragments:
+ if isinstance(el, basestring):
+ self.remains.append(el)
+ elif isinstance(el, lxml.html.HtmlElement):
+ src = el.get('src')
+ href = el.get('href')
+ if el.tag == 'style':
+ self.stylesheets.append(StylesheetAsset(source=el.text))
+ elif el.tag == 'link' and el.get('rel') == 'stylesheet' and self.can_aggregate(href):
+ self.stylesheets.append(StylesheetAsset(url=href))
+ elif el.tag == 'script' and not src:
+ self.javascripts.append(JavascriptAsset(source=el.text))
+ elif el.tag == 'script' and self.can_aggregate(src):
+ self.javascripts.append(JavascriptAsset(url=src))
+ else:
+ self.remains.append(lxml.html.tostring(el))
+ else:
+ try:
+ self.remains.append(lxml.html.tostring(el))
+ except Exception:
+ # notYETimplementederror
+ raise NotImplementedError
+
+ def can_aggregate(self, url):
+ return not urlparse(url).netloc and not url.startswith(('/web/css', '/web/js'))
+
+ def to_html(self, sep='\n'):
+ response = []
+ if self.stylesheets:
+ response.append('' % self.xmlid)
+ if self.javascripts:
+ response.append('' % self.xmlid)
+ response.extend(self.remains)
+
+ return sep.join(response)
+
+ @openerp.tools.func.lazy_property
+ def last_modified(self):
+ return max(itertools.chain(
+ (asset.last_modified for asset in self.javascripts),
+ (asset.last_modified for asset in self.stylesheets),
+ [datetime.datetime(1970, 1, 1)],
+ ))
+
+ @openerp.tools.func.lazy_property
+ def checksum(self):
+ checksum = hashlib.new('sha1')
+ for asset in itertools.chain(self.javascripts, self.stylesheets):
+ checksum.update(asset.content.encode("utf-8"))
+ return checksum.hexdigest()
+
+ def js(self):
+ key = 'js_' + self.checksum
+ if key not in self.cache:
+ content =';\n'.join(asset.minify() for asset in self.javascripts)
+ self.cache[key] = content
+ if self.debug:
+ return "/*\n%s\n*/\n" % '\n'.join(
+ [asset.filename for asset in self.javascripts if asset.filename]) + self.cache[key]
+ return self.cache[key]
+
+ def css(self):
+ key = 'css_' + self.checksum
+ if key not in self.cache:
+ content = '\n'.join(asset.minify() for asset in self.stylesheets)
+ # move up all @import rules to the top
+ matches = []
+ def push(matchobj):
+ matches.append(matchobj.group(0))
+ return ''
+
+ content = re.sub(self.rx_css_import, push, content)
+
+ matches.append(content)
+ content = u'\n'.join(matches)
+ self.cache[key] = content
+ if self.debug:
+ return "/*\n%s\n*/\n" % '\n'.join(
+ [asset.filename for asset in self.javascripts if asset.filename]) + self.cache[key]
+ return self.cache[key]
+
+class WebAsset(object):
+ def __init__(self, source=None, url=None):
+ self.source = source
+ self.url = url
+ self._filename = None
+ self._content = None
+
+ @property
+ def filename(self):
+ if self._filename is None and self.url:
+ module = filter(None, self.url.split('/'))[0]
+ try:
+ mpath = openerp.http.addons_manifest[module]['addons_path']
+ except Exception:
+ raise KeyError("Could not find asset '%s' for '%s' addon" % (self.url, module))
+ self._filename = mpath + self.url.replace('/', os.path.sep)
+ return self._filename
+
+ @property
+ def content(self):
+ if self._content is None:
+ self._content = self.get_content()
+ return self._content
+
+ def get_content(self):
+ if self.source:
+ return self.source
+
+ with open(self.filename, 'rb') as fp:
+ return fp.read().decode('utf-8')
+
+ def minify(self):
+ return self.content
+
+ @property
+ def last_modified(self):
+ if self.source:
+ # TODO: return last_update of bundle's ir.ui.view
+ return datetime.datetime(1970, 1, 1)
+ return datetime.datetime.fromtimestamp(os.path.getmtime(self.filename))
+
+class JavascriptAsset(WebAsset):
+ def minify(self):
+ return rjsmin(self.content)
+
+class StylesheetAsset(WebAsset):
+ rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
+ rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
+ rx_comments = re.compile(r"""/\*.*\*/""", re.S)
+ rx_sourceMap = re.compile(r'(/\*# sourceMappingURL=.*)', re.U)
+
+ def _get_content(self):
+ if self.source:
+ return self.source
+
+ with open(self.filename, 'rb') as fp:
+ firstline = fp.readline()
+ m = re.match(r'@charset "([^"]+)";', firstline)
+ if m:
+ encoding = m.group(1)
+ else:
+ encoding = "utf-8"
+ # "reinject" first line as it's not @charset
+ fp.seek(0)
+
+ return fp.read().decode(encoding)
+
+ def get_content(self):
+ content = self._get_content()
+ if self.url:
+ web_dir = os.path.dirname(self.url)
+
+ content = self.rx_import.sub(
+ r"""@import \1%s/""" % (web_dir,),
+ content,
+ )
+
+ content = self.rx_url.sub(
+ r"url(\1%s/" % (web_dir,),
+ content,
+ )
+ return content
+
+ def minify(self):
+ # remove existing sourcemaps, make no sense after re-mini
+ return self.rx_sourceMap.sub('', self.content)
+ # return self.rx_comments.sub('', self.content)
+
+def rjsmin(script):
+ """ Minify js with a clever regex.
+ Taken from http://opensource.perlig.de/rjsmin
+ Apache License, Version 2.0 """
+ def subber(match):
+ """ Substitution callback """
+ groups = match.groups()
+ return (
+ groups[0] or
+ groups[1] or
+ groups[2] or
+ groups[3] or
+ (groups[4] and '\n') or
+ (groups[5] and ' ') or
+ (groups[6] and ' ') or
+ (groups[7] and ' ') or
+ ''
+ )
+
+ result = re.sub(
+ r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
+ r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
+ r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
+ r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
+ r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
+ r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
+ r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
+ r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
+ r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
+ r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
+ r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
+ r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
+ r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
+ r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
+ r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
+ r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
+ r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
+ r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
+ r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
+ r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
+ r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script
+ ).strip()
+ return result
+
# vim:et:
diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py
index 697afd1e28f..ea2c9d6ecbe 100644
--- a/openerp/addons/base/ir/ir_ui_view.py
+++ b/openerp/addons/base/ir/ir_ui_view.py
@@ -868,7 +868,8 @@ class view(osv.osv):
values = dict()
qcontext = dict(
keep_query=keep_query,
- request=request,
+ request=request, # might be unbound if we're not in an httprequest context
+ debug=request.debug if request else False,
json=simplejson,
quote_plus=werkzeug.url_quote_plus,
)
diff --git a/openerp/http.py b/openerp/http.py
index 83bcf81575f..6258816cd63 100644
--- a/openerp/http.py
+++ b/openerp/http.py
@@ -559,7 +559,7 @@ class HttpRequest(WebRequest):
response.set_cookie(k, v)
return response
- def render(self, template, qcontext=None, **kw):
+ def render(self, template, qcontext=None, lazy=True, **kw):
""" Lazy render of QWeb template.
The actual rendering of the given template will occur at then end of
@@ -568,8 +568,12 @@ class HttpRequest(WebRequest):
:param basestring template: template to render
:param dict qcontext: Rendering context to use
+ :param dict lazy: Lazy rendering is processed later in wsgi response layer (default True)
"""
- return Response(template=template, qcontext=qcontext, **kw)
+ response = Response(template=template, qcontext=qcontext, **kw)
+ if not lazy:
+ return response.render()
+ return response
def not_found(self, description=None):
""" Helper for 404 response, return its result from the method
diff --git a/openerp/tools/func.py b/openerp/tools/func.py
index 89cdf9fb622..7d04c62a1c9 100644
--- a/openerp/tools/func.py
+++ b/openerp/tools/func.py
@@ -34,20 +34,19 @@ class lazy_property(object):
"""
def __init__(self, fget):
self.fget = fget
- self.name = fget.__name__
def __get__(self, obj, cls):
if obj is None:
return self
value = self.fget(obj)
- setattr(obj, self.name, value)
+ setattr(obj, self.fget.__name__, value)
return value
@staticmethod
def reset_all(obj):
""" Reset all lazy properties on the instance `obj`. """
cls = type(obj)
- obj_dict = obj.__dict__
+ obj_dict = vars(obj)
for name in obj_dict.keys():
if isinstance(getattr(cls, name, None), lazy_property):
obj_dict.pop(name)