diff --git a/addons/im_livechat/controllers/main.py b/addons/im_livechat/controllers/main.py index 805b6ba797a..14df16fb6c3 100644 --- a/addons/im_livechat/controllers/main.py +++ b/addons/im_livechat/controllers/main.py @@ -16,7 +16,7 @@ class LivechatController(http.Controller): # _get_asset return the bundle html code (script and link list) but we want to use the attachment content xmlid = 'im_livechat.external_lib' files, remains = request.env["ir.qweb"]._get_asset_content(xmlid, options=request.context) - asset = AssetsBundle(xmlid, files, remains) + asset = AssetsBundle(xmlid, files) mock_attachment = getattr(asset, ext)() if isinstance(mock_attachment, list): # suppose that CSS asset will not required to be split in pages diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py index 943a603d211..c2386d9466c 100644 --- a/addons/website/models/ir_qweb.py +++ b/addons/website/models/ir_qweb.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - -import ast +from collections import OrderedDict from odoo import models from odoo.http import request @@ -13,81 +12,33 @@ class QWeb(models.AbstractModel): _inherit = 'ir.qweb' URL_ATTRS = { - 'form': 'action', - 'a': 'href', + 'form': 'action', + 'a': 'href', + 'link': 'href', + 'script': 'src', + 'img': 'src', } - CDN_TRIGGERS = { - 'link': 'href', - 'script': 'src', - 'img': 'src', - } - - def _get_asset(self, xmlid, options, css=True, js=True, debug=False, async=False, values=None): - website = getattr(request, 'website', None) if request else None - if website and website.cdn_activated: - values = dict(values or {}, url_for=website.get_cdn_url) - return super(QWeb, self)._get_asset(xmlid, options, css, js, debug, async, values) - - def _website_build_attribute(self, tagName, name, value, options, values): - """ Compute the value of an attribute while rendering the template. """ - if name == self.URL_ATTRS.get(tagName) and values.get('url_for'): - return values.get('url_for')(value or '') - elif request and getattr(request, 'website', None) and request.website.cdn_activated and (name == self.URL_ATTRS.get(tagName) or name == self.CDN_TRIGGERS.get(tagName)): - return request.website.get_cdn_url(value or '') - return value - - def _wrap_build_attributes(self, el, items, options): - """ Map items corresponding to URL and CDN attributes to an ast expression. """ - if options.get('rendering_bundle'): - return items - - url_att = self.URL_ATTRS.get(el.tag) - cdn_att = self.CDN_TRIGGERS.get(el.tag) - - def process(item): - if isinstance(item, tuple) and (item[0] in (url_att, cdn_att)): - return (item[0], ast.Call( - func=ast.Attribute( - value=ast.Name(id='self', ctx=ast.Load()), - attr='_website_build_attribute', - ctx=ast.Load() - ), - args=[ - ast.Str(el.tag), - ast.Str(item[0]), - item[1], - ast.Name(id='options', ctx=ast.Load()), - ast.Name(id='values', ctx=ast.Load()), - ], keywords=[], - starargs=None, kwargs=None - )) - else: - return item - - return [process(it) for it in items] - - def _compile_static_attributes(self, el, options): - items = super(QWeb, self)._compile_static_attributes(el, options) - return self._wrap_build_attributes(el, items, options) - - def _compile_dynamic_attributes(self, el, options): - items = super(QWeb, self)._compile_dynamic_attributes(el, options) - return self._wrap_build_attributes(el, items, options) - - # method called by computing code - - def _get_dynamic_att(self, tagName, atts, options, values): - atts = super(QWeb, self)._get_dynamic_att(tagName, atts, options, values) - if options.get('rendering_bundle'): + def _post_processing_att(self, tagName, atts, options): + if atts.get('data-no-post-process'): return atts - for name, value in atts.items(): - atts[name] = self._website_build_attribute(tagName, name, value, options, values) - return atts - def _is_static_node(self, el): - url_att = self.URL_ATTRS.get(el.tag) - cdn_att = self.CDN_TRIGGERS.get(el.tag) - return super(QWeb, self)._is_static_node(el) and \ - (not url_att or not el.get(url_att)) and \ - (not cdn_att or not el.get(cdn_att)) + atts = super(QWeb, self)._post_processing_att(tagName, atts, options) + + if options.get('inherit_branding') or options.get('rendering_bundle') or \ + options.get('edit_translations') or options.get('debug') or (request and request.debug): + return atts + + website = request and getattr(request, 'website', None) + if not website and options.get('website_id'): + website = self.env['website'].browse(options['website_id']) + + if not website or not website.cdn_activated: + return atts + + name = self.URL_ATTRS.get(tagName) + if name and name in atts: + atts = OrderedDict(atts) + atts[name] = website.get_cdn_url(atts[name]) + + return atts diff --git a/addons/website/models/website.py b/addons/website/models/website.py index eb5483f7915..81002f54038 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -85,7 +85,11 @@ class Website(models.Model): @api.multi def write(self, values): self._get_languages.clear_cache(self) - return super(Website, self).write(values) + result = super(Website, self).write(values) + if 'cdn_activated' in values or 'cdn_url' in values or 'cdn_filters' in values: + # invalidate the caches from static node at compile time + self.env['ir.qweb'].clear_caches() + return result #---------------------------------------------------------- # Page Management @@ -563,15 +567,15 @@ class Website(models.Model): size = '' if size is None else '/%s' % size return '/web/image/%s/%s/%s%s?unique=%s' % (record._name, record.id, field, size, sha) - @api.model def get_cdn_url(self, uri): - # Currently only usable in a website_enable request context - if request and request.website and not request.debug and request.website.user_id.id == request.uid: - cdn_url = request.website.cdn_url - cdn_filters = (request.website.cdn_filters or '').splitlines() - for flt in cdn_filters: - if flt and re.match(flt, uri): - return urls.url_join(cdn_url, uri) + self.ensure_one() + if not uri: + return '' + cdn_url = self.cdn_url + cdn_filters = (self.cdn_filters or '').splitlines() + for flt in cdn_filters: + if flt and re.match(flt, uri): + return urls.url_join(cdn_url, uri) return uri @api.model diff --git a/addons/website/tests/__init__.py b/addons/website/tests/__init__.py index 121eff4ff44..a4415acbb7d 100644 --- a/addons/website/tests/__init__.py +++ b/addons/website/tests/__init__.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import test_converter +from . import test_qweb from . import test_crawl from . import test_ui from . import test_views diff --git a/addons/website/tests/template_qweb_test.xml b/addons/website/tests/template_qweb_test.xml new file mode 100644 index 00000000000..ad92b6966e6 --- /dev/null +++ b/addons/website/tests/template_qweb_test.xml @@ -0,0 +1,33 @@ + + + + + + + \ No newline at end of file diff --git a/addons/website/tests/test_qweb.py b/addons/website/tests/test_qweb.py new file mode 100644 index 00000000000..a0423e025ad --- /dev/null +++ b/addons/website/tests/test_qweb.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import tools +from odoo.modules.module import get_module_resource +from odoo.tests.common import TransactionCase + + +class TestQweb(TransactionCase): + def _load(self, module, *args): + tools.convert_file(self.cr, 'website', + get_module_resource(module, *args), + {}, 'init', False, 'test', self.registry._assertion_report) + + def test_qweb_cdn(self): + self._load('website', 'tests', 'template_qweb_test.xml') + + website = self.env['website'].browse(1) + website.write({ + "cdn_activated": True, + "cdn_url": "http://test.cdn" + }) + + demo = self.env['res.users'].search([('login', '=', 'demo')])[0] + demo_env = self.env(user=demo) + + html = demo_env['ir.qweb'].render('website.test_template', {}, website_id= website.id) + + attachments = demo_env['ir.attachment'].search([('url', '=like', '/web/content/%-%/website.test_bundle.%')]) + self.assertEquals(len(attachments), 2) + self.assertEqual(html.strip(), """ + + + + + + + + + + + + + + x + x + +""" % {"js": attachments[0].url, "css": attachments[1].url}) diff --git a/odoo/addons/base/ir/ir_qweb/assetsbundle.py b/odoo/addons/base/ir/ir_qweb/assetsbundle.py index 9c3b694a068..aae2f179ab5 100644 --- a/odoo/addons/base/ir/ir_qweb/assetsbundle.py +++ b/odoo/addons/base/ir/ir_qweb/assetsbundle.py @@ -9,9 +9,12 @@ import textwrap import uuid from datetime import datetime from subprocess import Popen, PIPE +from collections import OrderedDict from odoo import fields, tools +from odoo.tools.pycompat import string_types, to_text from odoo.http import request from odoo.modules.module import get_resource_path +from odoo.addons.base.ir.ir_qweb.qweb import escape import psycopg2 from odoo.tools import func, misc @@ -65,7 +68,6 @@ def rjsmin(script): ).strip() return result - class AssetError(Exception): pass @@ -79,17 +81,16 @@ class AssetsBundle(object): rx_preprocess_imports = re.compile("""(@import\s?['"]([^'"]+)['"](;?))""") rx_css_split = re.compile("\/\*\! ([a-f0-9-]+) \*\/") - def __init__(self, name, files, remains, env=None): + # remains attribute is depreciated and will remove after v11 + def __init__(self, name, files, remains=None, env=None): self.name = name self.env = request.env if env is None else env self.max_css_rules = self.env.context.get('max_css_rules', MAX_CSS_RULES) self.javascripts = [] self.stylesheets = [] self.css_errors = [] - self.remains = [] self._checksum = None self.files = files - self.remains = remains for f in files: if f['atype'] == 'text/sass': self.stylesheets.append(SassStylesheetAsset(self, url=f['url'], filename=f['filename'], inline=f['content'], media=f['media'])) @@ -100,10 +101,31 @@ class AssetsBundle(object): elif f['atype'] == 'text/javascript': self.javascripts.append(JavascriptAsset(self, url=f['url'], filename=f['filename'], inline=f['content'])) + # depreciated and will remove after v11 def to_html(self, sep=None, css=True, js=True, debug=False, async=False, url_for=(lambda url: url)): + nodes = self.to_node(css=css, js=js, debug=debug, async=async) + if sep is None: sep = u'\n ' response = [] + for tagName, attributes, content in nodes: + html = u"<%s " % tagName + for name, value in attributes.items(): + if value or isinstance(value, string_types): + html += u' %s="%s"' % (name, escape(to_text(value))) + if content is None: + html += u'/>' + else: + html += u'>%s' % (escape(to_text(content)), tagName) + response.append(html) + + return sep + sep.join(response) + + def to_node(self, css=True, js=True, debug=False, async=False): + """ + :returns [(tagName, attributes, content)] if the tag is auto close + """ + response = [] if debug == 'assets': if css and self.stylesheets: is_css_preprocessed, old_attachments = self.is_css_preprocessed() @@ -111,28 +133,37 @@ class AssetsBundle(object): self.preprocess_css(debug=debug, old_attachments=old_attachments) if self.css_errors: msg = '\n'.join(self.css_errors) - response.append(JavascriptAsset(self, inline=self.dialog_message(msg)).to_html()) - response.append(StylesheetAsset(self, url="/web/static/lib/bootstrap/css/bootstrap.css").to_html()) + response.append(JavascriptAsset(self, inline=self.dialog_message(msg)).to_node()) + response.append(StylesheetAsset(self, url="/web/static/lib/bootstrap/css/bootstrap.css").to_node()) if not self.css_errors: for style in self.stylesheets: - response.append(style.to_html()) + response.append(style.to_node()) if js: for jscript in self.javascripts: - response.append(jscript.to_html()) + response.append(jscript.to_node()) else: if css and self.stylesheets: css_attachments = self.css() or [] for attachment in css_attachments: - response.append(u'' % url_for(attachment.url)) + attr = OrderedDict([ + ["type", "text/css"], + ["rel", "stylesheet"], + ["href", attachment.url], + ]) + response.append(("link", attr, None)) if self.css_errors: msg = '\n'.join(self.css_errors) - response.append(JavascriptAsset(self, inline=self.dialog_message(msg)).to_html()) + response.append(JavascriptAsset(self, inline=self.dialog_message(msg)).to_node()) if js and self.javascripts: - response.append(u'' % (async and u'async="async"' or '', url_for(self.js().url))) - response.extend(self.remains) + attr = OrderedDict([ + ["async", "async" if async else None], + ["type", "text/javascript"], + ["src", self.js().url], + ]) + response.append(("script", attr, None)) - return sep + sep.join(response) + return response @func.lazy_property def last_modified(self): @@ -152,7 +183,7 @@ class AssetsBundle(object): Not really a full checksum. We compute a SHA1 on the rendered bundle + max linked files last_modified date """ - check = u"%s%s%s" % (json.dumps(self.files, sort_keys=True), u",".join(self.remains), self.last_modified) + check = u"%s%s" % (json.dumps(self.files, sort_keys=True), self.last_modified) return hashlib.sha1(check.encode('utf-8')).hexdigest() def clean_attachments(self, type): @@ -314,7 +345,7 @@ class AssetsBundle(object): outdated = False assets = dict((asset.html_url, asset) for asset in self.stylesheets if isinstance(asset, atype)) if assets: - assets_domain = [('url', 'in', list(assets))] + assets_domain = [('url', 'in', list(assets.keys()))] attachments = self.env['ir.attachment'].sudo().search(assets_domain) for attachment in attachments: asset = assets[attachment.url] @@ -474,7 +505,20 @@ class WebAsset(object): except Exception: raise AssetNotFound("Could not find %s" % self.name) + # depreciated and will remove after v11 def to_html(self): + tagName, attributes, content = self.to_node() + html = u"<%s " % tagName + for name, value in attributes.items(): + if value or isinstance(value, string_types): + html += u' %s="%s"' % (name, escape(to_text(value))) + if content is None: + html += u'/>' + else: + html += u'>%s' % (escape(to_text(content)), tagName) + return html + + def to_node(self): raise NotImplementedError() @func.lazy_property @@ -533,13 +577,19 @@ class JavascriptAsset(WebAsset): try: return super(JavascriptAsset, self)._fetch_content() except AssetError as e: - return "console.error(%s);" % json.dumps(str(e)) + return u"console.error(%s);" % json.dumps(to_text(e)) - def to_html(self): + def to_node(self): if self.url: - return '' % (self.html_url) + return ("script", OrderedDict([ + ["type", "text/javascript"], + ["src", self.html_url], + ]), None) else: - return '' % self.with_header() + return ("script", OrderedDict([ + ["type", "text/javascript"], + ["charset", "utf-8"], + ]), self.with_header()) class StylesheetAsset(WebAsset): @@ -595,13 +645,21 @@ class StylesheetAsset(WebAsset): content = re.sub(r' *([{}]) *', r'\1', content) return self.with_header(content) - def to_html(self): - media = (' media="%s"' % misc.html_escape(self.media)) if self.media else '' + def to_node(self): if self.url: - href = self.html_url - return '' % (href, media) + attr = OrderedDict([ + ["type", "text/css"], + ["rel", "stylesheet"], + ["href", self.html_url], + ["media", escape(to_text(self.media)) if self.media else None] + ]) + return ("link", attr, None) else: - return '' % (media, self.with_header()) + attr = OrderedDict([ + ["type", "text/css"], + ["media", escape(to_text(self.media)) if self.media else None] + ]) + return ("style", attr, self.with_header()) class PreprocessedCSS(StylesheetAsset): diff --git a/odoo/addons/base/ir/ir_qweb/ir_qweb.py b/odoo/addons/base/ir/ir_qweb/ir_qweb.py index 8bb333ea3fe..555f5d4eaa7 100644 --- a/odoo/addons/base/ir/ir_qweb/ir_qweb.py +++ b/odoo/addons/base/ir/ir_qweb/ir_qweb.py @@ -125,35 +125,122 @@ class IrQWeb(models.AbstractModel, QWeb): if len(el): raise SyntaxError("t-call-assets cannot contain children nodes") - # self._get_asset(xmlid, options, css=css, js=js, debug=values.get('debug'), async=async, values=values) + # nodes = self._get_asset(xmlid, options, css=css, js=js, debug=values.get('debug'), async=async, values=values) + # + # for index, (tagName, t_attrs, content) in enumerate(nodes): + # if index: + # append('\n ') + # append('<') + # append(tagName) + # + # self._post_processing_att(tagName, t_attrs, options) + # for name, value in t_attrs.items(): + # if value or isinstance(value, string_types)): + # append(u' ') + # append(name) + # append(u'="') + # append(escape(pycompat.to_text((value))) + # append(u'"') + # + # if not content and tagName in self._void_elements: + # append('/>') + # else: + # append('>') + # if content: + # append(content) + # append('') + # + space = el.getprevious() is not None and el.getprevious().tail or el.getparent().text + sep = u'\n' + space.rsplit('\n').pop() return [ - self._append(ast.Call( - func=ast.Attribute( - value=ast.Name(id='self', ctx=ast.Load()), - attr='_get_asset', - ctx=ast.Load() + ast.Assign( + targets=[ast.Name(id='nodes', ctx=ast.Store())], + value=ast.Call( + func=ast.Attribute( + value=ast.Name(id='self', ctx=ast.Load()), + attr='_get_asset', + ctx=ast.Load() + ), + args=[ + ast.Str(el.get('t-call-assets')), + ast.Name(id='options', ctx=ast.Load()), + ], + keywords=[ + ast.keyword('css', self._get_attr_bool(el.get('t-css', True))), + ast.keyword('js', self._get_attr_bool(el.get('t-js', True))), + ast.keyword('debug', ast.Call( + func=ast.Attribute( + value=ast.Name(id='values', ctx=ast.Load()), + attr='get', + ctx=ast.Load() + ), + args=[ast.Str('debug')], + keywords=[], starargs=None, kwargs=None + )), + ast.keyword('async', self._get_attr_bool(el.get('async', False))), + ast.keyword('values', ast.Name(id='values', ctx=ast.Load())), + ], + starargs=None, kwargs=None + ) + ), + ast.For( + target=ast.Tuple(elts=[ + ast.Name(id='index', ctx=ast.Store()), + ast.Tuple(elts=[ + ast.Name(id='tagName', ctx=ast.Store()), + ast.Name(id='t_attrs', ctx=ast.Store()), + ast.Name(id='content', ctx=ast.Store()) + ], ctx=ast.Store()) + ], ctx=ast.Store()), + iter=ast.Call( + func=ast.Name(id='enumerate', ctx=ast.Load()), + args=[ast.Name(id='nodes', ctx=ast.Load())], + keywords=[], + starargs=None, kwargs=None ), - args=[ - ast.Str(el.get('t-call-assets')), - ast.Name(id='options', ctx=ast.Load()), - ], - keywords=[ - ast.keyword('css', self._get_attr_bool(el.get('t-css', True))), - ast.keyword('js', self._get_attr_bool(el.get('t-js', True))), - ast.keyword('debug', ast.Call( - func=ast.Attribute( - value=ast.Name(id='values', ctx=ast.Load()), - attr='get', - ctx=ast.Load() + body=[ + ast.If( + test=ast.Name(id='index', ctx=ast.Load()), + body=[self._append(ast.Str(sep))], + orelse=[] + ), + self._append(ast.Str(u'<')), + self._append(ast.Name(id='tagName', ctx=ast.Load())), + ] + self._append_attributes() + [ + ast.If( + test=ast.BoolOp( + op=ast.And(), + values=[ + ast.UnaryOp(ast.Not(), ast.Name(id='content', ctx=ast.Load()), lineno=0, col_offset=0), + ast.Compare( + left=ast.Name(id='tagName', ctx=ast.Load()), + ops=[ast.In()], + comparators=[ast.Attribute( + value=ast.Name(id='self', ctx=ast.Load()), + attr='_void_elements', + ctx=ast.Load() + )] + ), + ] ), - args=[ast.Str('debug')], - keywords=[], starargs=None, kwargs=None - )), - ast.keyword('async', self._get_attr_bool(el.get('async', False))), - ast.keyword('values', ast.Name(id='values', ctx=ast.Load())), + body=[self._append(ast.Str(u'/>'))], + orelse=[ + self._append(ast.Str(u'>')), + ast.If( + test=ast.Name(id='content', ctx=ast.Load()), + body=[self._append(ast.Name(id='content', ctx=ast.Load()))], + orelse=[] + ), + self._append(ast.Str(u'')), + ] + ) ], - starargs=None, kwargs=None - )) + orelse=[] + ) ] # for backward compatibility to remove after v10 @@ -193,8 +280,9 @@ class IrQWeb(models.AbstractModel, QWeb): ) def _get_asset(self, xmlid, options, css=True, js=True, debug=False, async=False, values=None): files, remains = self._get_asset_content(xmlid, options) - asset = AssetsBundle(xmlid, files, remains, env=self.env) - return asset.to_html(css=css, js=js, debug=debug, async=async, url_for=(values or {}).get('url_for', lambda url: url)) + asset = AssetsBundle(xmlid, files, env=self.env) + remains = [node for node in remains if (css and node[0] == 'link') or (js and node[0] != 'link')] + return remains + asset.to_node(css=css, js=js, debug=debug, async=async) @tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', keys=("website_id",)) def _get_asset_content(self, xmlid, options): @@ -205,6 +293,9 @@ class IrQWeb(models.AbstractModel, QWeb): env = self.env(context=options) + def can_aggregate(url): + return not urls.url_parse(url).scheme and not urls.url_parse(url).netloc and not url.startswith('/web/content') + # TODO: This helper can be used by any template that wants to embedd the backend. # It is currently necessary because the ir.ui.view bundle inheritance does not # match the module dependency graph. @@ -218,16 +309,13 @@ class IrQWeb(models.AbstractModel, QWeb): files = [] remains = [] for el in html.fragments_fromstring(template): - if isinstance(el, pycompat.string_types): - remains.append(pycompat.to_text(el)) - elif isinstance(el, html.HtmlElement): + if isinstance(el, html.HtmlElement): href = el.get('href', '') src = el.get('src', '') atype = el.get('type') media = el.get('media') - can_aggregate = not urls.url_parse(href).netloc and not href.startswith('/web/content') - if el.tag == 'style' or (el.tag == 'link' and el.get('rel') == 'stylesheet' and can_aggregate): + if can_aggregate(href) and (el.tag == 'style' or (el.tag == 'link' and el.get('rel') == 'stylesheet')): if href.endswith('.sass'): atype = 'text/sass' elif href.endswith('.less'): @@ -237,19 +325,16 @@ class IrQWeb(models.AbstractModel, QWeb): path = [segment for segment in href.split('/') if segment] filename = get_resource_path(*path) if path else None files.append({'atype': atype, 'url': href, 'filename': filename, 'content': el.text, 'media': media}) - elif el.tag == 'script': + elif can_aggregate(src) and el.tag == 'script': atype = 'text/javascript' - path = [segment for segment in src.split('/') if segment] + path = [segment for segment in href.split('/') if segment] filename = get_resource_path(*path) if path else None files.append({'atype': atype, 'url': src, 'filename': filename, 'content': el.text, 'media': media}) else: - remains.append(html.tostring(el, encoding='unicode')) + remains.append((el.tag, OrderedDict(el.attrib), el.text)) else: - try: - remains.append(html.tostring(el, encoding='unicode')) - except Exception: - # notYETimplementederror - raise NotImplementedError + # the other cases are ignored + pass return (files, remains) diff --git a/odoo/addons/base/ir/ir_qweb/qweb.py b/odoo/addons/base/ir/ir_qweb/qweb.py index 309e25fbf2f..6dac133b9b6 100644 --- a/odoo/addons/base/ir/ir_qweb/qweb.py +++ b/odoo/addons/base/ir/ir_qweb/qweb.py @@ -695,6 +695,80 @@ class QWeb(object): ctx=ctx ) + def _append_attributes(self): + # t_attrs = self._post_processing_att(tagName, t_attrs, options) + # for name, value in t_attrs.items(): + # if value or isinstance(value, string_types)): + # append(u' ') + # append(name) + # append(u'="') + # append(escape(pycompat.to_text((value))) + # append(u'"') + return [ + ast.Assign( + targets=[ast.Name(id='t_attrs', ctx=ast.Store())], + value=ast.Call( + func=ast.Attribute( + value=ast.Name(id='self', ctx=ast.Load()), + attr='_post_processing_att', + ctx=ast.Load() + ), + args=[ + ast.Name(id='tagName', ctx=ast.Load()), + ast.Name(id='t_attrs', ctx=ast.Load()), + ast.Name(id='options', ctx=ast.Load()), + ], keywords=[], + starargs=None, kwargs=None + ) + ), + ast.For( + target=ast.Tuple(elts=[ast.Name(id='name', ctx=ast.Store()), ast.Name(id='value', ctx=ast.Store())], ctx=ast.Store()), + iter=ast.Call( + func=ast.Attribute( + value=ast.Name(id='t_attrs', ctx=ast.Load()), + attr='items', + ctx=ast.Load() + ), + args=[], keywords=[], + starargs=None, kwargs=None + ), + body=[ast.If( + test=ast.BoolOp( + op=ast.Or(), + values=[ + ast.Name(id='value', ctx=ast.Load()), + ast.Call( + func=ast.Name(id='isinstance', ctx=ast.Load()), + args=[ + ast.Name(id='value', ctx=ast.Load()), + ast.Name(id='string_types', ctx=ast.Load()) + ], + keywords=[], + starargs=None, kwargs=None + ) + ] + ), + body=[ + self._append(ast.Str(u' ')), + self._append(ast.Name(id='name', ctx=ast.Load())), + self._append(ast.Str(u'="')), + self._append(ast.Call( + func=ast.Name(id='escape', ctx=ast.Load()), + args=[ast.Call( + func=ast.Name(id='to_text', ctx=ast.Load()), + args=[ast.Name(id='value', ctx=ast.Load())], keywords=[], + starargs=None, kwargs=None + )], keywords=[], + starargs=None, kwargs=None + )), + self._append(ast.Str(u'"')), + ], + orelse=[] + )], + orelse=[] + ) + ] + # order def _directives_eval_order(self): @@ -733,7 +807,7 @@ class QWeb(object): if not el.nsmap: unqualified_el_tag = el_tag = el.tag content = self._compile_directive_content(el, options) - attrib = el.attrib + attrib = self._post_processing_att(el.tag, el.attrib, options) else: # Etree will remove the ns prefixes indirection by inlining the corresponding # nsmap definition into the tag attribute. Restore the tag and prefix here. @@ -763,6 +837,8 @@ class QWeb(object): else: attrib[key] = value + attrib = self._post_processing_att(el.tag, attrib, options) + # Update the dict of inherited namespaces before continuing the recursion. Note: # since `options['nsmap']` is a dict (and therefore mutable) and we do **not** # want changes done in deeper recursion to bevisible in earlier ones, we'll pass @@ -871,59 +947,12 @@ class QWeb(object): ))) if attr_already_created: - # for name, value in t_attrs.items(): - # if value or isinstance(value, basestring)): - # append(u' ') - # append(name) - # append(u'="') - # append(escape(to_text((value))) - # append(u'"') - body.append(ast.For( - target=ast.Tuple(elts=[ast.Name(id='name', ctx=ast.Store()), ast.Name(id='value', ctx=ast.Store())], ctx=ast.Store()), - iter=ast.Call( - func=ast.Attribute( - value=ast.Name(id='t_attrs', ctx=ast.Load()), - attr='items', - ctx=ast.Load() - ), - args=[], keywords=[], - starargs=None, kwargs=None - ), - body=[ast.If( - test=ast.BoolOp( - op=ast.Or(), - values=[ - ast.Name(id='value', ctx=ast.Load()), - ast.Call( - func=ast.Name(id='isinstance', ctx=ast.Load()), - args=[ - ast.Name(id='value', ctx=ast.Load()), - ast.Name(id='string_types', ctx=ast.Load()) - ], - keywords=[], - starargs=None, kwargs=None - ) - ] - ), - body=[ - self._append(ast.Str(u' ')), - self._append(ast.Name(id='name', ctx=ast.Load())), - self._append(ast.Str(u'="')), - self._append(ast.Call( - func=ast.Name(id='escape', ctx=ast.Load()), - args=[ast.Call( - func=ast.Name(id='to_text', ctx=ast.Load()), - args=[ast.Name(id='value', ctx=ast.Load())], keywords=[], - starargs=None, kwargs=None - )], keywords=[], - starargs=None, kwargs=None - )), - self._append(ast.Str(u'"')), - ], - orelse=[] - )], - orelse=[] - )) + # tagName = $el.tag + body.append(ast.Assign( + targets=[ast.Name(id='tagName', ctx=ast.Store())], + value=ast.Str(el.tag)) + ) + body.extend(self._append_attributes()) return body @@ -1513,6 +1542,14 @@ class QWeb(object): atts = OrderedDict(atts) return atts + def _post_processing_att(self, tagName, atts, options): + """ Method called by the compiled code. This method may be overwrited + to filter or modify the attributes after they are compiled. + + @returns OrderedDict + """ + return atts + def _get_field(self, record, field_name, expression, tagName, field_options, options, values): """ :returns: tuple: diff --git a/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py b/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py index 86c878b35e2..896c3564387 100644 --- a/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py +++ b/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py @@ -38,7 +38,7 @@ class TestJavascriptAssetsBundle(TransactionCase): def _get_asset(self, xmlid, env=None): env = (env or self.env) files, remains = env['ir.qweb']._get_asset_content(xmlid, env.context) - return AssetsBundle(xmlid, files, remains, env=env) + return AssetsBundle(xmlid, files, env=env) def _any_ira_for_bundle(self, type): """ Returns all ir.attachments associated to a bundle, regardless of the verion. @@ -118,7 +118,6 @@ class TestJavascriptAssetsBundle(TransactionCase): bundle0 = self._get_asset(self.jsbundle_xmlid) bundle0.js() files0 = bundle0.files - remains0 = bundle0.remains version0 = bundle0.version self.assertEquals(len(self._any_ira_for_bundle('js')), 1) @@ -141,11 +140,9 @@ class TestJavascriptAssetsBundle(TransactionCase): bundle1 = self._get_asset(self.jsbundle_xmlid, env=self.env(context={'check_view_ids': view.ids})) bundle1.js() files1 = bundle1.files - remains1 = bundle1.remains version1 = bundle1.version self.assertNotEquals(files0, files1) - self.assertEquals(remains0, remains1) self.assertNotEquals(version0, version1) # check if the previous attachment are correctly cleaned @@ -247,7 +244,6 @@ class TestJavascriptAssetsBundle(TransactionCase): bundle0 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'max_css_rules': 1})) bundle0.css() files0 = bundle0.files - remains0 = bundle0.remains version0 = bundle0.version self.assertEquals(len(self._any_ira_for_bundle('css')), 3) @@ -270,11 +266,9 @@ class TestJavascriptAssetsBundle(TransactionCase): bundle1 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'check_view_ids': view.ids, 'max_css_rules': 1})) bundle1.css() files1 = bundle1.files - remains1 = bundle1.remains version1 = bundle1.version self.assertNotEquals(files0, files1) - self.assertEquals(remains0, remains1) self.assertNotEquals(version0, version1) # check if the previous attachment are correctly cleaned @@ -318,6 +312,48 @@ class TestJavascriptAssetsBundle(TransactionCase): content = bundle0.to_html() self.assertEqual(content.count('test_assetsbundle.bundle2.0.css'), 1) + def test_15_exteral_lib_assets(self): + html = self.env['ir.ui.view'].render_template('test_assetsbundle.template2') + attachments = self.env['ir.attachment'].search([('url', '=like', '/web/content/%-%/test_assetsbundle.bundle4.%')]) + self.assertEquals(len(attachments), 2) + self.assertEqual(html.strip(), (""" + + + + + + + + + + + + +""" % {"js": attachments[0].url, "css": attachments[1].url}).encode('utf8')) + + def test_16_exteral_lib_assets_debug_mode(self): + html = self.env['ir.ui.view'].render_template('test_assetsbundle.template2', {"debug": "assets"}) + attachments = self.env['ir.attachment'].search([('url', '=like', '/web/content/%-%/test_assetsbundle.bundle4.%')]) + self.assertEquals(len(attachments), 0) + self.assertEqual(html.strip(), (""" + + + + + + + + + + + + + + + +""").encode('utf8')) + + class TestAssetsBundleInBrowser(HttpCase): def test_01_js_interpretation(self): diff --git a/odoo/addons/test_assetsbundle/views/views.xml b/odoo/addons/test_assetsbundle/views/views.xml index d953f930ce1..fa7cf4d5b78 100644 --- a/odoo/addons/test_assetsbundle/views/views.xml +++ b/odoo/addons/test_assetsbundle/views/views.xml @@ -23,4 +23,33 @@ + + + + +