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 @@
+
+
+
+ x
+ x
+
+
+
+
+
+
+ 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%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%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('')
+ # append(tagName)
+ # 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'')),
+ self._append(ast.Name(id='tagName', ctx=ast.Load())),
+ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<!DOCTYPE html>
+
+
+