From cbb5488f978ae592914fa56a2378cd54aa1bd00c Mon Sep 17 00:00:00 2001 From: Fabien Meghazi Date: Tue, 8 Apr 2014 09:52:05 +0200 Subject: [PATCH 01/25] [ADD] t-call-assets bzr revid: fme@openerp.com-20140408075205-9v8fxg786jj0fj77 --- openerp/addons/base/ir/ir_qweb.py | 40 +++++++++++++++++++++++++++ openerp/http.py | 46 +++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index 3c195c93c6c..391dbf610e9 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -8,12 +8,14 @@ import math import re import sys import xml # FIXME use lxml and etree +import lxml import babel import babel.dates import werkzeug.utils from PIL import Image +import openerp.http import openerp.tools from openerp.tools.safe_eval import safe_eval as eval from openerp.osv import osv, orm, fields @@ -385,6 +387,44 @@ class QWeb(orm.AbstractModel): return self.render(cr, uid, self.eval_format(template_attributes["call"], d), d) + def render_tag_call_assets(self, element, template_attributes, generated_attributes, qwebcontext): + template_attributes['call'] = template_attributes['call-assets'] + content = self.render_tag_call(element, template_attributes, generated_attributes, qwebcontext) + html = lxml.html.fragment_fromstring('%s' % content) # avoid this hack if possible + + tags = dict() + current_style = None + current_script = None + + for el in html: + if el.tag == 'link' and el.attrib.get('rel') == 'stylesheet': + cssfile = el.attrib.get('href') + if current_style is None: + current_style = el + tags[current_style] = [] + el.attrib['href'] = '/web/css/' + else: + html.remove(el) + tags[current_style].append(cssfile) + if el.tag == 'script': + if 'src' not in el.attrib: + current_script = None + else: + jsfile = el.attrib['src'] + if current_script is None: + current_script = el + tags[current_script] = [] + el.attrib['src'] = '/web/js/' + else: + html.remove(el) + tags[current_script].append(jsfile) + + for tag, files in tags.items(): + attr = 'href' if 'href' in tag.attrib else 'src' + tag.attrib[attr] += openerp.http.pathlist_to_base64(files) + r = lxml.html.tostring(html)[3:-4] # avoid this hack if possible + return r + 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) diff --git a/openerp/http.py b/openerp/http.py index 2e21d27528a..ccff43ed271 100644 --- a/openerp/http.py +++ b/openerp/http.py @@ -3,6 +3,7 @@ # OpenERP HTTP layer #---------------------------------------------------------- import ast +import base64 import collections import contextlib import errno @@ -137,6 +138,51 @@ def redirect_with_hash(url, code=303): return werkzeug.utils.redirect(url, code) return "" % url +def pathlist_to_base64(files): + """ Serialize a path list to an url-safe string trying to avoid path + duplication while keeping files order. + """ + seen = [] + concat = [] + current_path = None + for f in files: + if f in seen: + continue + seen.append(f) + path, fname = os.path.split(f) + if not path: + path = '/' + if path != current_path: + current_path = path + concat.append(path) + concat.append(fname) + r = '|'.join(concat) + return base64.urlsafe_b64encode(r.encode('zlib')) + +def base64_to_pathlist(s, fspath=False): + """ Deserialize a string generated by filelist_to_base64 + + :param fspath: returns tuple(filesystem path, url path) if True + """ + files = [] + decoded = base64.urlsafe_b64decode(str(s)).decode('zlib') + items = decoded.split('|') + current_path = '/' + while items: + item = items.pop(0) + if item.startswith('/'): + current_path = item + else: + path = current_path + '/' + item + if fspath: + module = filter(bool, path.split('/'))[0] + mpath = addons_manifest[module]['addons_path'] + fs_path = mpath + path.replace('/', os.path.sep) + files.append((fs_path, path)) + else: + files.append(path) + return files + class WebRequest(object): """ Parent class for all OpenERP Web request types, mostly deals with initialization and setup of the request object (the dispatching itself has From c1dc46770173647971de55531e343132f8533c77 Mon Sep 17 00:00:00 2001 From: Fabien Meghazi Date: Wed, 9 Apr 2014 18:17:58 +0200 Subject: [PATCH 02/25] [WIP] AssetsBundle bzr revid: fme@openerp.com-20140409161758-3hye6u21rjmoiio9 --- openerp/addons/base/ir/ir_qweb.py | 127 +++++++++++++++++++-------- openerp/addons/base/ir/ir_ui_view.py | 3 +- openerp/http.py | 54 ++---------- 3 files changed, 99 insertions(+), 85 deletions(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index 391dbf610e9..da58455d4be 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -5,10 +5,12 @@ import datetime import json import logging import math +import os import re import sys import xml # FIXME use lxml and etree import lxml +from urlparse import urlparse import babel import babel.dates @@ -388,42 +390,18 @@ class QWeb(orm.AbstractModel): return self.render(cr, uid, self.eval_format(template_attributes["call"], d), d) def render_tag_call_assets(self, element, template_attributes, generated_attributes, qwebcontext): - template_attributes['call'] = template_attributes['call-assets'] + """ This special 't-call' tag can be used in order to aggregate/minify javascript and css assets""" + name = template_attributes['call-assets'] + template_attributes['call'] = name + + # Backward compatibility hack for manifest usage + qwebcontext['manifest_list'] = openerp.addons.web.controllers.main.manifest_list + content = self.render_tag_call(element, template_attributes, generated_attributes, qwebcontext) - html = lxml.html.fragment_fromstring('%s' % content) # avoid this hack if possible - - tags = dict() - current_style = None - current_script = None - - for el in html: - if el.tag == 'link' and el.attrib.get('rel') == 'stylesheet': - cssfile = el.attrib.get('href') - if current_style is None: - current_style = el - tags[current_style] = [] - el.attrib['href'] = '/web/css/' - else: - html.remove(el) - tags[current_style].append(cssfile) - if el.tag == 'script': - if 'src' not in el.attrib: - current_script = None - else: - jsfile = el.attrib['src'] - if current_script is None: - current_script = el - tags[current_script] = [] - el.attrib['src'] = '/web/js/' - else: - html.remove(el) - tags[current_script].append(jsfile) - - for tag, files in tags.items(): - attr = 'href' if 'href' in tag.attrib else 'src' - tag.attrib[attr] += openerp.http.pathlist_to_base64(files) - r = lxml.html.tostring(html)[3:-4] # avoid this hack if possible - return r + 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: @@ -848,7 +826,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' @@ -923,4 +900,82 @@ def get_field_type(column, options): """ return options.get('widget', column._type) +class AssetsBundle(object): + def __init__(self, xmlid, html=None): + self.xmlid = xmlid + self.javascripts = [] + self.stylesheets = [] + self.remains = [] + if html: + self.parse(html) + + def parse(self, html): + fragments = lxml.html.fragments_fromstring(html) + for el in fragments: + if type(el) is basestring: + self.remains.append(el) + elif type(el) is lxml.html.HtmlElement: + src = el.attrib.get('src') + href = el.attrib.get('href') + if el.tag == 'style': + self.stylesheets.append(StylesheetAsset(inline=el.text)) + elif el.tag == 'link' and el.attrib['rel'] == 'stylesheet' and self.is_internal_url(href): + self.stylesheets.append(StylesheetAsset(url=href)) + elif el.tag == 'script' and not src: + self.javascripts.append(JavascriptAsset(inline=el.text)) + elif el.tag == 'script' and self.is_internal_url(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: + raise NotImplementedError + + def is_internal_url(self, url): + return not urlparse(url).netloc + + def to_html(self, sep='\n'): + response = list(self.remains) + if self.stylesheets: + response.insert(0, '' % self.xmlid) + if self.javascripts: + response.insert(0, '' % self.xmlid) + return sep.join(response) + + def js(self): + content = '\n'.join([asset.get_content() for asset in self.javascripts]) + return content + + def css(self): + content = '\n'.join([asset.get_content() for asset in self.stylesheets]) + return content + +class WebAsset(object): + def __init__(self, source=None, url=None, filename=None): + self.source = source + self.filename = filename + self.url = url + + def get_content(self): + if self.source: + return self.source + if self.url: + module = filter(bool, self.url.split('/'))[0] + mpath = openerp.http.addons_manifest[module]['addons_path'] + self.filename = mpath + self.url.replace('/', os.path.sep) + with open(self.filename, 'rb') as fp: + data = fp.read().decode('utf-8') + return data + +class JavascriptAsset(WebAsset): + def get_content(self): + content = super(JavascriptAsset, self).get_content() + return content + ';' + +class StylesheetAsset(WebAsset): + pass + + # vim:et: diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 7c857e767ba..b6d785f5054 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -858,7 +858,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 ccff43ed271..2c3e8d93c52 100644 --- a/openerp/http.py +++ b/openerp/http.py @@ -3,7 +3,6 @@ # OpenERP HTTP layer #---------------------------------------------------------- import ast -import base64 import collections import contextlib import errno @@ -138,51 +137,6 @@ def redirect_with_hash(url, code=303): return werkzeug.utils.redirect(url, code) return "" % url -def pathlist_to_base64(files): - """ Serialize a path list to an url-safe string trying to avoid path - duplication while keeping files order. - """ - seen = [] - concat = [] - current_path = None - for f in files: - if f in seen: - continue - seen.append(f) - path, fname = os.path.split(f) - if not path: - path = '/' - if path != current_path: - current_path = path - concat.append(path) - concat.append(fname) - r = '|'.join(concat) - return base64.urlsafe_b64encode(r.encode('zlib')) - -def base64_to_pathlist(s, fspath=False): - """ Deserialize a string generated by filelist_to_base64 - - :param fspath: returns tuple(filesystem path, url path) if True - """ - files = [] - decoded = base64.urlsafe_b64decode(str(s)).decode('zlib') - items = decoded.split('|') - current_path = '/' - while items: - item = items.pop(0) - if item.startswith('/'): - current_path = item - else: - path = current_path + '/' + item - if fspath: - module = filter(bool, path.split('/'))[0] - mpath = addons_manifest[module]['addons_path'] - fs_path = mpath + path.replace('/', os.path.sep) - files.append((fs_path, path)) - else: - files.append(path) - return files - class WebRequest(object): """ Parent class for all OpenERP Web request types, mostly deals with initialization and setup of the request object (the dispatching itself has @@ -613,7 +567,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 @@ -622,8 +576,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 From 8e878a8e013d095d3fc639c5c0fa9798e98ba016 Mon Sep 17 00:00:00 2001 From: Fabien Meghazi Date: Thu, 10 Apr 2014 12:18:47 +0200 Subject: [PATCH 03/25] [WIP] bundle minify bzr revid: fme@openerp.com-20140410101847-srs54wvf39rjjf2j --- openerp/addons/base/ir/ir_qweb.py | 126 +++++++++++++++++++++++++----- 1 file changed, 108 insertions(+), 18 deletions(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index da58455d4be..c5eb25b28cc 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -901,6 +901,9 @@ def get_field_type(column, options): return options.get('widget', column._type) class AssetsBundle(object): + rx_css_charset = re.compile("(@charset.+;$)", re.M) + rx_css_import = re.compile("(@import.+;$)", re.M) + def __init__(self, xmlid, html=None): self.xmlid = xmlid self.javascripts = [] @@ -938,18 +941,35 @@ class AssetsBundle(object): def to_html(self, sep='\n'): response = list(self.remains) - if self.stylesheets: - response.insert(0, '' % self.xmlid) if self.javascripts: response.insert(0, '' % self.xmlid) + if self.stylesheets: + response.insert(0, '' % self.xmlid) return sep.join(response) - def js(self): - content = '\n'.join([asset.get_content() for asset in self.javascripts]) + def js(self, minified=True): + if minified: + content = ';\n'.join([asset.minify() for asset in self.javascripts]) + else: + content = ';\n'.join([asset.get_content() for asset in self.javascripts]) return content - def css(self): - content = '\n'.join([asset.get_content() for asset in self.stylesheets]) + def css(self, minified=True): + if minified: + content = '\n'.join([asset.minify() for asset in self.stylesheets]) + else: + content = '\n'.join([asset.get_content() for asset in self.stylesheets]) + # move up all @import and @charset rules to the top + matches = [] + def push(matchobj): + matches.append(matchobj.group(0)) + return '' + + content = re.sub(self.rx_css_charset, push, content) + content = re.sub(self.rx_css_import, push, content) + + matches.append(content) + content = '\n'.join(matches) return content class WebAsset(object): @@ -960,22 +980,92 @@ class WebAsset(object): def get_content(self): if self.source: - return self.source - if self.url: - module = filter(bool, self.url.split('/'))[0] - mpath = openerp.http.addons_manifest[module]['addons_path'] - self.filename = mpath + self.url.replace('/', os.path.sep) - with open(self.filename, 'rb') as fp: - data = fp.read().decode('utf-8') - return data + data = self.source + else: + if self.url and not self.filename: + module = filter(bool, self.url.split('/'))[0] + mpath = openerp.http.addons_manifest[module]['addons_path'] + self.filename = mpath + self.url.replace('/', os.path.sep) + with open(self.filename, 'rb') as fp: + data = fp.read().decode('utf-8') + return data + + def minify(self): + return self.get_content() class JavascriptAsset(WebAsset): - def get_content(self): - content = super(JavascriptAsset, self).get_content() - return content + ';' + def minify(self): + return rjsmin(self.get_content()) class StylesheetAsset(WebAsset): - pass + 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) + def get_content(self): + content = super(StylesheetAsset, self).get_content() + if self.url: + web_dir = os.path.dirname(self.url) + + content = re.sub( + self.rx_import, + r"""@import \1%s/""" % (web_dir,), + content, + ) + + content = re.sub( + self.rx_url, + r"url(\1%s/" % (web_dir,), + content, + ) + return content + + def minify(self): + content = self.get_content() + return self.rx_comments.sub('', 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: From b8e9e0e8f40d7520ac67ff8206ab648edda006a7 Mon Sep 17 00:00:00 2001 From: Fabien Meghazi Date: Thu, 10 Apr 2014 16:32:24 +0200 Subject: [PATCH 04/25] [ADD] last modified date bzr revid: fme@openerp.com-20140410143224-rb8892vn0yqmr56g --- openerp/addons/base/ir/ir_qweb.py | 35 ++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index c5eb25b28cc..ef5974556a7 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -934,6 +934,7 @@ class AssetsBundle(object): try: self.remains.append(lxml.html.tostring(el)) except Exception: + # notYETimplementederror raise NotImplementedError def is_internal_url(self, url): @@ -947,6 +948,12 @@ class AssetsBundle(object): response.insert(0, '' % self.xmlid) return sep.join(response) + @property + def last_modified(self): + jsdates = [asset.last_modified for asset in self.javascripts] + cssdates = [asset.last_modified for asset in self.stylesheets] + return max(jsdates + cssdates) or datetime.datetime(1970, 1, 1) + def js(self, minified=True): if minified: content = ';\n'.join([asset.minify() for asset in self.javascripts]) @@ -975,17 +982,22 @@ class AssetsBundle(object): class WebAsset(object): def __init__(self, source=None, url=None, filename=None): self.source = source - self.filename = filename + self._filename = filename self.url = url + @property + def filename(self): + if self._filename: + return self._filename + module = filter(bool, self.url.split('/'))[0] + mpath = openerp.http.addons_manifest[module]['addons_path'] + self._filename = mpath + self.url.replace('/', os.path.sep) + return self._filename + def get_content(self): if self.source: data = self.source else: - if self.url and not self.filename: - module = filter(bool, self.url.split('/'))[0] - mpath = openerp.http.addons_manifest[module]['addons_path'] - self.filename = mpath + self.url.replace('/', os.path.sep) with open(self.filename, 'rb') as fp: data = fp.read().decode('utf-8') return data @@ -993,6 +1005,13 @@ class WebAsset(object): def minify(self): return self.get_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.get_content()) @@ -1020,9 +1039,9 @@ class StylesheetAsset(WebAsset): ) return content - def minify(self): - content = self.get_content() - return self.rx_comments.sub('', content) + # def minify(self): + # content = self.get_content() + # return self.rx_comments.sub('', content) def rjsmin(script): """ Minify js with a clever regex. From 71d1a7f6e3ef9d3a00df1575a624eb0675411636 Mon Sep 17 00:00:00 2001 From: Fabien Meghazi Date: Thu, 10 Apr 2014 17:54:12 +0200 Subject: [PATCH 05/25] [ADD] checksum bzr revid: fme@openerp.com-20140410155412-64h843qo6k9vcwbz --- openerp/addons/base/ir/ir_qweb.py | 41 ++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index ef5974556a7..0941e70b6be 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -2,6 +2,7 @@ import collections import cStringIO import datetime +import hashlib import json import logging import math @@ -909,6 +910,7 @@ class AssetsBundle(object): self.javascripts = [] self.stylesheets = [] self.remains = [] + self._checksum = None if html: self.parse(html) @@ -954,18 +956,27 @@ class AssetsBundle(object): cssdates = [asset.last_modified for asset in self.stylesheets] return max(jsdates + cssdates) or datetime.datetime(1970, 1, 1) + @property + def checksum(self): + if self._checksum is None: + checksum = hashlib.new('sha1') + for asset in self.javascripts + self.stylesheets: + checksum.update(asset.content.encode("utf-8")) + self._checksum = checksum.hexdigest() + return self._checksum + def js(self, minified=True): if minified: content = ';\n'.join([asset.minify() for asset in self.javascripts]) else: - content = ';\n'.join([asset.get_content() for asset in self.javascripts]) + content = ';\n'.join([asset.content for asset in self.javascripts]) return content def css(self, minified=True): if minified: content = '\n'.join([asset.minify() for asset in self.stylesheets]) else: - content = '\n'.join([asset.get_content() for asset in self.stylesheets]) + content = '\n'.join([asset.content for asset in self.stylesheets]) # move up all @import and @charset rules to the top matches = [] def push(matchobj): @@ -983,17 +994,23 @@ class WebAsset(object): def __init__(self, source=None, url=None, filename=None): self.source = source self._filename = filename + self._content = None self.url = url @property def filename(self): - if self._filename: - return self._filename - module = filter(bool, self.url.split('/'))[0] - mpath = openerp.http.addons_manifest[module]['addons_path'] - self._filename = mpath + self.url.replace('/', os.path.sep) + if self._filename is None: + module = filter(bool, self.url.split('/'))[0] + mpath = openerp.http.addons_manifest[module]['addons_path'] + 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: data = self.source @@ -1003,7 +1020,10 @@ class WebAsset(object): return data def minify(self): - return self.get_content() + return self.content + + def compute_checksum(self): + return 'fewfwe' @property def last_modified(self): @@ -1014,7 +1034,7 @@ class WebAsset(object): class JavascriptAsset(WebAsset): def minify(self): - return rjsmin(self.get_content()) + return rjsmin(self.content) class StylesheetAsset(WebAsset): rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U) @@ -1040,8 +1060,7 @@ class StylesheetAsset(WebAsset): return content # def minify(self): - # content = self.get_content() - # return self.rx_comments.sub('', content) + # return self.rx_comments.sub('', self.content) def rjsmin(script): """ Minify js with a clever regex. From cc722b62a2907790687235c268fc7c41ae2e473e Mon Sep 17 00:00:00 2001 From: Fabien Meghazi Date: Thu, 10 Apr 2014 18:47:44 +0200 Subject: [PATCH 06/25] [ADD] cache bzr revid: fme@openerp.com-20140410164744-i7fr4bqax88430ao --- openerp/addons/base/ir/ir_qweb.py | 41 ++++++++++++++++--------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index 0941e70b6be..98cfa5f4e34 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -902,6 +902,7 @@ def get_field_type(column, options): return options.get('widget', column._type) class AssetsBundle(object): + cache = dict() rx_css_charset = re.compile("(@charset.+;$)", re.M) rx_css_import = re.compile("(@import.+;$)", re.M) @@ -965,30 +966,30 @@ class AssetsBundle(object): self._checksum = checksum.hexdigest() return self._checksum - def js(self, minified=True): - if minified: - content = ';\n'.join([asset.minify() for asset in self.javascripts]) - else: - content = ';\n'.join([asset.content for asset in self.javascripts]) - return content + 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 + return self.cache[key] - def css(self, minified=True): - if minified: + def css(self): + key = 'css_' + self.checksum + if key not in self.cache: content = '\n'.join([asset.minify() for asset in self.stylesheets]) - else: - content = '\n'.join([asset.content for asset in self.stylesheets]) - # move up all @import and @charset rules to the top - matches = [] - def push(matchobj): - matches.append(matchobj.group(0)) - return '' + # move up all @import and @charset rules to the top + matches = [] + def push(matchobj): + matches.append(matchobj.group(0)) + return '' - content = re.sub(self.rx_css_charset, push, content) - content = re.sub(self.rx_css_import, push, content) + content = re.sub(self.rx_css_charset, push, content) + content = re.sub(self.rx_css_import, push, content) - matches.append(content) - content = '\n'.join(matches) - return content + matches.append(content) + content = '\n'.join(matches) + self.cache[key] = content + return self.cache[key] class WebAsset(object): def __init__(self, source=None, url=None, filename=None): From 93073dff94904d8b87f2501ee7cde8f6d85a45bb Mon Sep 17 00:00:00 2001 From: Fabien Meghazi Date: Thu, 10 Apr 2014 19:40:25 +0200 Subject: [PATCH 07/25] [FIX] exclude /web/[css|js] in parsing bzr revid: fme@openerp.com-20140410174025-74hmzzalkxux3tvb --- openerp/addons/base/ir/ir_qweb.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index 9b0000f3838..85bf9aa9bbf 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -962,11 +962,11 @@ class AssetsBundle(object): href = el.attrib.get('href') if el.tag == 'style': self.stylesheets.append(StylesheetAsset(inline=el.text)) - elif el.tag == 'link' and el.attrib['rel'] == 'stylesheet' and self.is_internal_url(href): + elif el.tag == 'link' and el.attrib['rel'] == 'stylesheet' and self.can_aggregate(href): self.stylesheets.append(StylesheetAsset(url=href)) elif el.tag == 'script' and not src: self.javascripts.append(JavascriptAsset(inline=el.text)) - elif el.tag == 'script' and self.is_internal_url(src): + elif el.tag == 'script' and self.can_aggregate(src): self.javascripts.append(JavascriptAsset(url=src)) else: self.remains.append(lxml.html.tostring(el)) @@ -977,8 +977,8 @@ class AssetsBundle(object): # notYETimplementederror raise NotImplementedError - def is_internal_url(self, url): - return not urlparse(url).netloc + def can_aggregate(self, url): + return not urlparse(url).netloc and not url.startswith('/web/css') and not url.startswith('/web/js') def to_html(self, sep='\n'): response = list(self.remains) From 23eb0067729e3ef8c72601e64c7e449ed1d8cd42 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 16 Apr 2014 17:35:36 +0200 Subject: [PATCH 08/25] [IMP] style, default last_modified * replaced type is other by isinstance * used generators instead of list when applicable * removed unused methods, useless parameters * not sure why explicit call to dict() instead of literal, fixed it bzr revid: xmo@openerp.com-20140416153536-84s01gbnjfu6hysl --- openerp/addons/base/ir/ir_qweb.py | 61 ++++++++++++++++--------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index 85bf9aa9bbf..7f4adb5bb24 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -10,7 +10,8 @@ import os import re import sys import xml # FIXME use lxml and etree -import lxml +import itertools +import lxml.html from urlparse import urlparse import babel @@ -939,7 +940,7 @@ def get_field_type(column, options): return options.get('widget', column._type) class AssetsBundle(object): - cache = dict() + cache = {} rx_css_charset = re.compile("(@charset.+;$)", re.M) rx_css_import = re.compile("(@import.+;$)", re.M) @@ -955,17 +956,17 @@ class AssetsBundle(object): def parse(self, html): fragments = lxml.html.fragments_fromstring(html) for el in fragments: - if type(el) is basestring: + if isinstance(el, basestring): self.remains.append(el) - elif type(el) is lxml.html.HtmlElement: - src = el.attrib.get('src') - href = el.attrib.get('href') + elif isinstance(el, lxml.html.HtmlElement): + src = el.get('src') + href = el.get('href') if el.tag == 'style': - self.stylesheets.append(StylesheetAsset(inline=el.text)) - elif el.tag == 'link' and el.attrib['rel'] == 'stylesheet' and self.can_aggregate(href): + 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(inline=el.text)) + self.javascripts.append(JavascriptAsset(source=el.text)) elif el.tag == 'script' and self.can_aggregate(src): self.javascripts.append(JavascriptAsset(url=src)) else: @@ -978,27 +979,31 @@ class AssetsBundle(object): raise NotImplementedError def can_aggregate(self, url): - return not urlparse(url).netloc and not url.startswith('/web/css') and not url.startswith('/web/js') + return not urlparse(url).netloc and not url.startswith(('/web/css', '/web/js')) def to_html(self, sep='\n'): - response = list(self.remains) - if self.javascripts: - response.insert(0, '' % self.xmlid) + response = [] if self.stylesheets: - response.insert(0, '' % self.xmlid) + response.append('' % self.xmlid) + if self.javascripts: + response.append('' % self.xmlid) + response.extend(self.remains) + return sep.join(response) @property def last_modified(self): - jsdates = [asset.last_modified for asset in self.javascripts] - cssdates = [asset.last_modified for asset in self.stylesheets] - return max(jsdates + cssdates) or datetime.datetime(1970, 1, 1) + 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)], + )) @property def checksum(self): if self._checksum is None: checksum = hashlib.new('sha1') - for asset in self.javascripts + self.stylesheets: + for asset in itertools.chain(self.javascripts, self.stylesheets): checksum.update(asset.content.encode("utf-8")) self._checksum = checksum.hexdigest() return self._checksum @@ -1006,14 +1011,14 @@ class AssetsBundle(object): def js(self): key = 'js_' + self.checksum if key not in self.cache: - content =';\n'.join([asset.minify() for asset in self.javascripts]) + content =';\n'.join(asset.minify() for asset in self.javascripts) self.cache[key] = content 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]) + content = '\n'.join(asset.minify() for asset in self.stylesheets) # move up all @import and @charset rules to the top matches = [] def push(matchobj): @@ -1038,7 +1043,7 @@ class WebAsset(object): @property def filename(self): if self._filename is None: - module = filter(bool, self.url.split('/'))[0] + module = filter(None, self.url.split('/'))[0] mpath = openerp.http.addons_manifest[module]['addons_path'] self._filename = mpath + self.url.replace('/', os.path.sep) return self._filename @@ -1051,23 +1056,19 @@ class WebAsset(object): def get_content(self): if self.source: - data = self.source - else: - with open(self.filename, 'rb') as fp: - data = fp.read().decode('utf-8') - return data + return self.source + + with open(self.filename, 'rb') as fp: + return fp.read().decode('utf-8') def minify(self): return self.content - def compute_checksum(self): - return 'fewfwe' - @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.utcnow() return datetime.datetime.fromtimestamp(os.path.getmtime(self.filename)) class JavascriptAsset(WebAsset): From a2889a1cf8af47e7c7cc6455e8dae7463ac07be0 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 17 Apr 2014 14:52:13 +0200 Subject: [PATCH 09/25] [FIX] @charset in CSS files * there should be *at most* one per file, not 12 * use existing @charset to know file encoding when reading files (before concatenation) * remove existing @charset while reading, if any * werkzeug's default output charset is UTF-8 and CSS algo uses content-type encoding or assumes UTF-8, so don't put any @charset back in bzr revid: xmo@openerp.com-20140417125213-8c9z2d7eu900kkyw --- openerp/addons/base/ir/ir_qweb.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index 7f4adb5bb24..ca4c74994b1 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -941,7 +941,6 @@ def get_field_type(column, options): class AssetsBundle(object): cache = {} - rx_css_charset = re.compile("(@charset.+;$)", re.M) rx_css_import = re.compile("(@import.+;$)", re.M) def __init__(self, xmlid, html=None): @@ -1025,11 +1024,10 @@ class AssetsBundle(object): matches.append(matchobj.group(0)) return '' - content = re.sub(self.rx_css_charset, push, content) content = re.sub(self.rx_css_import, push, content) matches.append(content) - content = '\n'.join(matches) + content = u'\n'.join(matches) self.cache[key] = content return self.cache[key] @@ -1080,8 +1078,24 @@ class StylesheetAsset(WebAsset): rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U) rx_comments = re.compile(r"""/\*.*\*/""", re.S) + 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 = super(StylesheetAsset, self).get_content() + content = self._get_content() if self.url: web_dir = os.path.dirname(self.url) From f8964db4353e7902979546b999719c5265713d44 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 17 Apr 2014 15:12:49 +0200 Subject: [PATCH 10/25] [IMP] @import regex bzr revid: xmo@openerp.com-20140417131249-gbvnq2zb09kzdifv --- openerp/addons/base/ir/ir_qweb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index ca4c74994b1..da0ce467f36 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -941,7 +941,7 @@ def get_field_type(column, options): class AssetsBundle(object): cache = {} - rx_css_import = re.compile("(@import.+;$)", re.M) + rx_css_import = re.compile("(@import[^;{]+;?)", re.M) def __init__(self, xmlid, html=None): self.xmlid = xmlid From 5b081731ccfda3fbc1cfc450236566113da3ee27 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 17 Apr 2014 16:20:12 +0200 Subject: [PATCH 11/25] [FIX] remove existing sourcemaps in CSS files bzr revid: xmo@openerp.com-20140417142012-mhi8saeofkon5pe5 --- openerp/addons/base/ir/ir_qweb.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index da0ce467f36..c5c9d2e8e63 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -1077,6 +1077,7 @@ 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: @@ -1099,21 +1100,21 @@ class StylesheetAsset(WebAsset): if self.url: web_dir = os.path.dirname(self.url) - content = re.sub( - self.rx_import, + content = self.rx_import.sub( r"""@import \1%s/""" % (web_dir,), content, ) - content = re.sub( - self.rx_url, + content = self.rx_url.sub( r"url(\1%s/" % (web_dir,), content, ) return content - # def minify(self): - # return self.rx_comments.sub('', self.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. From d9f26840af6a774435a7e752860d43bfd3c13843 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 17 Apr 2014 16:35:25 +0200 Subject: [PATCH 12/25] [IMP] don't alter call-assets's template_attributes in-place, create fake one for sub-call bzr revid: xmo@openerp.com-20140417143525-0qyqddd1niwsg7ad --- openerp/addons/base/ir/ir_qweb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index c5c9d2e8e63..03b21ce4c9a 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -396,12 +396,12 @@ class QWeb(orm.AbstractModel): 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'] - template_attributes['call'] = name # Backward compatibility hack for manifest usage qwebcontext['manifest_list'] = openerp.addons.web.controllers.main.manifest_list - content = self.render_tag_call(element, template_attributes, generated_attributes, qwebcontext) + content = self.render_tag_call( + element, {'call': name}, generated_attributes, qwebcontext) if qwebcontext.get('debug'): return content bundle = AssetsBundle(name, html=content) From 424f03c172317a9cd7b2c3192cd606cd511ae93a Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 17 Apr 2014 16:36:56 +0200 Subject: [PATCH 13/25] [REV] datetime for source-only WebAsset thinking about it, this is the *inline* data taken directly from the template, probably makes more sense if it does not change the coalesced asset's last_modified bzr revid: xmo@openerp.com-20140417143656-upgj5b0syi10c9ve --- openerp/addons/base/ir/ir_qweb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index 03b21ce4c9a..77f75668930 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -1066,7 +1066,7 @@ class WebAsset(object): def last_modified(self): if self.source: # TODO: return last_update of bundle's ir.ui.view - return datetime.datetime.utcnow() + return datetime.datetime(1970, 1, 1) return datetime.datetime.fromtimestamp(os.path.getmtime(self.filename)) class JavascriptAsset(WebAsset): From 24512edac9e95ec489e8efa0de334cb740b0f063 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 18 Apr 2014 15:06:03 +0200 Subject: [PATCH 14/25] [IMP] don't reimplement lazy_property by hand when we already have one bzr revid: xmo@openerp.com-20140418130603-ekgsfsi1c07y7wuo --- openerp/addons/base/ir/ir_qweb.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index 77f75668930..1f89733c1e8 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -21,6 +21,7 @@ from PIL import Image import openerp.http import openerp.tools +import openerp.tools.func from openerp.tools.safe_eval import safe_eval as eval from openerp.osv import osv, orm, fields from openerp.tools.translate import _ @@ -990,7 +991,7 @@ class AssetsBundle(object): return sep.join(response) - @property + @openerp.tools.func.lazy_property def last_modified(self): return max(itertools.chain( (asset.last_modified for asset in self.javascripts), @@ -998,14 +999,12 @@ class AssetsBundle(object): [datetime.datetime(1970, 1, 1)], )) - @property + @openerp.tools.func.lazy_property def checksum(self): - if self._checksum is None: - checksum = hashlib.new('sha1') - for asset in itertools.chain(self.javascripts, self.stylesheets): - checksum.update(asset.content.encode("utf-8")) - self._checksum = checksum.hexdigest() - return self._checksum + 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 From 4934579abb81b231a3e0f0de27e21dfdda33989f Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 18 Apr 2014 15:06:29 +0200 Subject: [PATCH 15/25] [IMP] No cause to store self.fget.__name__ to its own attribute when it's only used once bzr revid: xmo@openerp.com-20140418130629-30vp1veih7jfmnx7 --- openerp/tools/func.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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) From 59b8b7ede06aace913796cea593675c445185d63 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 18 Apr 2014 15:29:29 +0200 Subject: [PATCH 16/25] [IMP] preclude unbounded growth by using an LRU cache for asset bundle compressed content bzr revid: xmo@openerp.com-20140418132929-qnyh3zoarjuo9q6a --- openerp/addons/base/ir/ir_qweb.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index 1f89733c1e8..76e053fdf9c 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -22,6 +22,7 @@ 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 _ @@ -941,7 +942,7 @@ def get_field_type(column, options): return options.get('widget', column._type) class AssetsBundle(object): - cache = {} + cache = openerp.tools.lru.LRU(32) rx_css_import = re.compile("(@import[^;{]+;?)", re.M) def __init__(self, xmlid, html=None): From 4c5b1d9484a40efd2f68bd785d45dfd69c8795b2 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 18 Apr 2014 15:30:17 +0200 Subject: [PATCH 17/25] [FIX] comment: we've already removed @charset movements entirely bzr revid: xmo@openerp.com-20140418133017-1cyq5wkxj4opygww --- openerp/addons/base/ir/ir_qweb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index 76e053fdf9c..e76d1163c07 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -1018,7 +1018,7 @@ class AssetsBundle(object): key = 'css_' + self.checksum if key not in self.cache: content = '\n'.join(asset.minify() for asset in self.stylesheets) - # move up all @import and @charset rules to the top + # move up all @import rules to the top matches = [] def push(matchobj): matches.append(matchobj.group(0)) From 1cde3cb02b80ec8ba8680bc97bf221b833c524d8 Mon Sep 17 00:00:00 2001 From: Fabien Meghazi Date: Tue, 22 Apr 2014 13:15:48 +0200 Subject: [PATCH 18/25] [ADD] debug mode in asset bundle bzr revid: fme@openerp.com-20140422111548-5htr1gfj7x3ioval --- openerp/addons/base/ir/ir_qweb.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index e76d1163c07..2c2e396b394 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -945,7 +945,8 @@ class AssetsBundle(object): cache = openerp.tools.lru.LRU(32) rx_css_import = re.compile("(@import[^;{]+;?)", re.M) - def __init__(self, xmlid, html=None): + def __init__(self, xmlid, html=None, debug=False): + self.debug = debug self.xmlid = xmlid self.javascripts = [] self.stylesheets = [] @@ -1011,6 +1012,9 @@ class AssetsBundle(object): key = 'js_' + self.checksum if key not in self.cache: content =';\n'.join(asset.minify() for asset in self.javascripts) + if self.debug: + content = "/*\n%s\n*/\n" % '\n'.join( + [asset.filename for asset in self.javascripts if asset.filename]) + content self.cache[key] = content return self.cache[key] @@ -1028,6 +1032,9 @@ class AssetsBundle(object): matches.append(content) content = u'\n'.join(matches) + if self.debug: + content = "/*\n%s\n*/\n" % '\n'.join( + [asset.filename for asset in self.javascripts if asset.filename]) + content self.cache[key] = content return self.cache[key] @@ -1040,7 +1047,7 @@ class WebAsset(object): @property def filename(self): - if self._filename is None: + if self._filename is None and self.url: module = filter(None, self.url.split('/'))[0] mpath = openerp.http.addons_manifest[module]['addons_path'] self._filename = mpath + self.url.replace('/', os.path.sep) From d2f51a75d518ea3bd513b091c8b1db2a0c097307 Mon Sep 17 00:00:00 2001 From: Fabien Meghazi Date: Tue, 22 Apr 2014 13:27:32 +0200 Subject: [PATCH 19/25] [FIX] asset debug shall bypass cache bzr revid: fme@openerp.com-20140422112732-1p94f77ymusmsan2 --- openerp/addons/base/ir/ir_qweb.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index 2c2e396b394..f2983df7dd9 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -1012,10 +1012,10 @@ class AssetsBundle(object): key = 'js_' + self.checksum if key not in self.cache: content =';\n'.join(asset.minify() for asset in self.javascripts) - if self.debug: - content = "/*\n%s\n*/\n" % '\n'.join( - [asset.filename for asset in self.javascripts if asset.filename]) + content 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): @@ -1032,10 +1032,10 @@ class AssetsBundle(object): matches.append(content) content = u'\n'.join(matches) - if self.debug: - content = "/*\n%s\n*/\n" % '\n'.join( - [asset.filename for asset in self.javascripts if asset.filename]) + content 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): From f4a37244cac024c8af907f7ead1fae73a2985bdf Mon Sep 17 00:00:00 2001 From: Fabien Meghazi Date: Tue, 22 Apr 2014 15:46:13 +0200 Subject: [PATCH 20/25] [ADD] query to assets bundle html bzr revid: fme@openerp.com-20140422134613-8652pfbrdso1qoqd --- openerp/addons/base/ir/ir_qweb.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index f2983df7dd9..a5442dbbcad 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -16,7 +16,7 @@ from urlparse import urlparse import babel import babel.dates -import werkzeug.utils +import werkzeug from PIL import Image import openerp.http @@ -407,7 +407,7 @@ class QWeb(orm.AbstractModel): if qwebcontext.get('debug'): return content bundle = AssetsBundle(name, html=content) - return bundle.to_html() + return bundle.to_html(query=qwebcontext.get('assets_query')) def render_tag_set(self, element, template_attributes, generated_attributes, qwebcontext): if "value" in template_attributes: @@ -983,12 +983,16 @@ class AssetsBundle(object): def can_aggregate(self, url): return not urlparse(url).netloc and not url.startswith(('/web/css', '/web/js')) - def to_html(self, sep='\n'): + def to_html(self, query=None, sep='\n'): response = [] + if query: + query = '?' + werkzeug.url_encode(query) + else: + query = '' if self.stylesheets: - response.append('' % self.xmlid) + response.append('' % (self.xmlid, query)) if self.javascripts: - response.append('' % self.xmlid) + response.append('' % (self.xmlid, query)) response.extend(self.remains) return sep.join(response) From 75408134f919bd33be8a10471a8b87890d0a64af Mon Sep 17 00:00:00 2001 From: Fabien Meghazi Date: Thu, 24 Apr 2014 18:56:59 +0200 Subject: [PATCH 21/25] [REM] Removed js and css manifest keys from base bzr revid: fme@openerp.com-20140424165659-qzhryvgp8vmo1ssd --- openerp/addons/base/__openerp__.py | 4 ---- 1 file changed, 4 deletions(-) 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: From 4b3212c0766be7ae0539d910aaad6f34e48337a6 Mon Sep 17 00:00:00 2001 From: Fabien Meghazi Date: Fri, 25 Apr 2014 15:28:04 +0200 Subject: [PATCH 22/25] [IMP] better exception handling in case of missing asset bzr revid: fme@openerp.com-20140425132804-e0dbsl5t2sod7kv5 --- openerp/addons/base/ir/ir_qweb.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index 6eb41879522..a85a0fdfc6a 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -1084,7 +1084,10 @@ class WebAsset(object): def filename(self): if self._filename is None and self.url: module = filter(None, self.url.split('/'))[0] - mpath = openerp.http.addons_manifest[module]['addons_path'] + 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 From 5d13ad7ee806208656545fb4206f5dfcabe070e7 Mon Sep 17 00:00:00 2001 From: Fabien Meghazi Date: Mon, 28 Apr 2014 14:44:31 +0200 Subject: [PATCH 23/25] [REM] query additional params for bundles `translatable` has been removed from assets templates, no need for this anymore bzr revid: fme@openerp.com-20140428124431-3b5rvyejjosftpq3 --- openerp/addons/base/ir/ir_qweb.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index a85a0fdfc6a..f0c6ce46f5c 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -416,7 +416,7 @@ class QWeb(orm.AbstractModel): if qwebcontext.get('debug'): return content bundle = AssetsBundle(name, html=content) - return bundle.to_html(query=qwebcontext.get('assets_query')) + return bundle.to_html() def render_tag_set(self, element, template_attributes, generated_attributes, qwebcontext): if "value" in template_attributes: @@ -1014,16 +1014,12 @@ class AssetsBundle(object): def can_aggregate(self, url): return not urlparse(url).netloc and not url.startswith(('/web/css', '/web/js')) - def to_html(self, query=None, sep='\n'): + def to_html(self, sep='\n'): response = [] - if query: - query = '?' + werkzeug.url_encode(query) - else: - query = '' if self.stylesheets: - response.append('' % (self.xmlid, query)) + response.append('' % self.xmlid) if self.javascripts: - response.append('' % (self.xmlid, query)) + response.append('' % self.xmlid) response.extend(self.remains) return sep.join(response) From 677de74fefeba9fa8d07c28700c3a5e022e1a56e Mon Sep 17 00:00:00 2001 From: Fabien Meghazi Date: Wed, 30 Apr 2014 09:35:37 +0200 Subject: [PATCH 24/25] [REM] @filename from WebAsset constructor bzr revid: fme@openerp.com-20140430073537-lk4i126urhfkjdtz --- openerp/addons/base/ir/ir_qweb.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index f0c6ce46f5c..04bd68a9aca 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -1070,11 +1070,11 @@ class AssetsBundle(object): return self.cache[key] class WebAsset(object): - def __init__(self, source=None, url=None, filename=None): + def __init__(self, source=None, url=None): self.source = source - self._filename = filename - self._content = None self.url = url + self._filename = None + self._content = None @property def filename(self): From a1b46eb0d27f05ce9a60c176acb5c4d0fb73e048 Mon Sep 17 00:00:00 2001 From: Fabien Meghazi Date: Wed, 30 Apr 2014 10:10:42 +0200 Subject: [PATCH 25/25] [FIX] Disable inherit_branding for t-call-assets bzr revid: fme@openerp.com-20140430081042-f77suergc85qemnn --- openerp/addons/base/ir/ir_qweb.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index 04bd68a9aca..798d0e5fe24 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -411,8 +411,10 @@ class QWeb(orm.AbstractModel): # 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, qwebcontext) + element, {'call': name}, generated_attributes, d) if qwebcontext.get('debug'): return content bundle = AssetsBundle(name, html=content)