[FIX] qweb: use cdn for asset files

Refactoring of qweb to call _post_processing_att for each nodes. This
change remove the crappy ovewrite in website module. The website overwrite
only the _post_processing_att to add the cdn parameters.

The static node (without t- attributes) can stay static (remove overwrite
of _is_static_node), the cdn is applied at the compile time for this
nodes instead of at the running time like the dynamic node.
This commit is contained in:
Christophe Matthieu
2018-06-15 08:42:15 +02:00
committed by Jeremy Kersten
parent b05e4d5f95
commit 9cd982bcc8
11 changed files with 493 additions and 211 deletions
+1 -1
View File
@@ -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 # _get_asset return the bundle html code (script and link list) but we want to use the attachment content
xmlid = 'im_livechat.external_lib' xmlid = 'im_livechat.external_lib'
files, remains = request.env["ir.qweb"]._get_asset_content(xmlid, options=request.context) 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)() mock_attachment = getattr(asset, ext)()
if isinstance(mock_attachment, list): # suppose that CSS asset will not required to be split in pages if isinstance(mock_attachment, list): # suppose that CSS asset will not required to be split in pages
+27 -76
View File
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import OrderedDict
import ast
from odoo import models from odoo import models
from odoo.http import request from odoo.http import request
@@ -13,81 +12,33 @@ class QWeb(models.AbstractModel):
_inherit = 'ir.qweb' _inherit = 'ir.qweb'
URL_ATTRS = { URL_ATTRS = {
'form': 'action', 'form': 'action',
'a': 'href', 'a': 'href',
'link': 'href',
'script': 'src',
'img': 'src',
} }
CDN_TRIGGERS = { def _post_processing_att(self, tagName, atts, options):
'link': 'href', if atts.get('data-no-post-process'):
'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'):
return atts 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): atts = super(QWeb, self)._post_processing_att(tagName, atts, options)
url_att = self.URL_ATTRS.get(el.tag)
cdn_att = self.CDN_TRIGGERS.get(el.tag) if options.get('inherit_branding') or options.get('rendering_bundle') or \
return super(QWeb, self)._is_static_node(el) and \ options.get('edit_translations') or options.get('debug') or (request and request.debug):
(not url_att or not el.get(url_att)) and \ return atts
(not cdn_att or not el.get(cdn_att))
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
+13 -9
View File
@@ -85,7 +85,11 @@ class Website(models.Model):
@api.multi @api.multi
def write(self, values): def write(self, values):
self._get_languages.clear_cache(self) 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 # Page Management
@@ -563,15 +567,15 @@ class Website(models.Model):
size = '' if size is None else '/%s' % size size = '' if size is None else '/%s' % size
return '/web/image/%s/%s/%s%s?unique=%s' % (record._name, record.id, field, size, sha) return '/web/image/%s/%s/%s%s?unique=%s' % (record._name, record.id, field, size, sha)
@api.model
def get_cdn_url(self, uri): def get_cdn_url(self, uri):
# Currently only usable in a website_enable request context self.ensure_one()
if request and request.website and not request.debug and request.website.user_id.id == request.uid: if not uri:
cdn_url = request.website.cdn_url return ''
cdn_filters = (request.website.cdn_filters or '').splitlines() cdn_url = self.cdn_url
for flt in cdn_filters: cdn_filters = (self.cdn_filters or '').splitlines()
if flt and re.match(flt, uri): for flt in cdn_filters:
return urls.url_join(cdn_url, uri) if flt and re.match(flt, uri):
return urls.url_join(cdn_url, uri)
return uri return uri
@api.model @api.model
+1
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_converter from . import test_converter
from . import test_qweb
from . import test_crawl from . import test_crawl
from . import test_ui from . import test_ui
from . import test_views from . import test_views
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="website.test_bundle">
<script type="text/javascript" src="/web/static/lib/qweb/qweb2.js"></script>
<script type="text/javascript" src="http://test.external.link/javascript1.js"></script>
<link rel="stylesheet" type="text/css" href="/web/static/lib/jquery.ui/jquery-ui.css"/>
<link rel="stylesheet" href="http://test.external.link/style1.css"/>
<script type="text/javascript" src="/web/static/src/js/boot.js"></script>
<script type="text/javascript" src="http://test.external.link/javascript2.js"></script>
<link rel="stylesheet" href="http://test.external.link/style2.css"/>
</template>
<template id="website.test_template" name="test template 2">
&lt;!DOCTYPE html&gt;
<html>
<head>
<t t-call-assets="website.test_bundle" t-js="False"/>
<meta/>
<t t-call-assets="website.test_bundle" t-css="False"/>
</head>
<body>
<img src="http://test.external.link/img.png"/>
<img src="/website/static/img.png"/>
<a href="http://test.external.link/link">x</a>
<a href="/web/content/local_link">x</a>
</body>
</html>
</template>
</odoo>
+48
View File
@@ -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(), """<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="http://test.external.link/style1.css"/>
<link rel="stylesheet" href="http://test.external.link/style2.css"/>
<link href="http://test.cdn%(css)s" rel="stylesheet"/>
<meta/>
<script type="text/javascript" src="http://test.external.link/javascript1.js"></script>
<script type="text/javascript" src="http://test.external.link/javascript2.js"></script>
<script type="text/javascript" src="http://test.cdn%(js)s"></script>
</head>
<body>
<img src="http://test.external.link/img.png"/>
<img src="http://test.cdn/website/static/img.png"/>
<a href="http://test.external.link/link">x</a>
<a href="http://test.cdn/web/content/local_link">x</a>
</body>
</html>""" % {"js": attachments[0].url, "css": attachments[1].url})
+82 -24
View File
@@ -9,9 +9,12 @@ import textwrap
import uuid import uuid
from datetime import datetime from datetime import datetime
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
from collections import OrderedDict
from odoo import fields, tools from odoo import fields, tools
from odoo.tools.pycompat import string_types, to_text
from odoo.http import request from odoo.http import request
from odoo.modules.module import get_resource_path from odoo.modules.module import get_resource_path
from odoo.addons.base.ir.ir_qweb.qweb import escape
import psycopg2 import psycopg2
from odoo.tools import func, misc from odoo.tools import func, misc
@@ -65,7 +68,6 @@ def rjsmin(script):
).strip() ).strip()
return result return result
class AssetError(Exception): class AssetError(Exception):
pass pass
@@ -79,17 +81,16 @@ class AssetsBundle(object):
rx_preprocess_imports = re.compile("""(@import\s?['"]([^'"]+)['"](;?))""") rx_preprocess_imports = re.compile("""(@import\s?['"]([^'"]+)['"](;?))""")
rx_css_split = re.compile("\/\*\! ([a-f0-9-]+) \*\/") 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.name = name
self.env = request.env if env is None else env 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.max_css_rules = self.env.context.get('max_css_rules', MAX_CSS_RULES)
self.javascripts = [] self.javascripts = []
self.stylesheets = [] self.stylesheets = []
self.css_errors = [] self.css_errors = []
self.remains = []
self._checksum = None self._checksum = None
self.files = files self.files = files
self.remains = remains
for f in files: for f in files:
if f['atype'] == 'text/sass': if f['atype'] == 'text/sass':
self.stylesheets.append(SassStylesheetAsset(self, url=f['url'], filename=f['filename'], inline=f['content'], media=f['media'])) 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': elif f['atype'] == 'text/javascript':
self.javascripts.append(JavascriptAsset(self, url=f['url'], filename=f['filename'], inline=f['content'])) 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)): 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: if sep is None:
sep = u'\n ' sep = u'\n '
response = [] 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 debug == 'assets':
if css and self.stylesheets: if css and self.stylesheets:
is_css_preprocessed, old_attachments = self.is_css_preprocessed() 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) self.preprocess_css(debug=debug, old_attachments=old_attachments)
if self.css_errors: if self.css_errors:
msg = '\n'.join(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())
response.append(StylesheetAsset(self, url="/web/static/lib/bootstrap/css/bootstrap.css").to_html()) response.append(StylesheetAsset(self, url="/web/static/lib/bootstrap/css/bootstrap.css").to_node())
if not self.css_errors: if not self.css_errors:
for style in self.stylesheets: for style in self.stylesheets:
response.append(style.to_html()) response.append(style.to_node())
if js: if js:
for jscript in self.javascripts: for jscript in self.javascripts:
response.append(jscript.to_html()) response.append(jscript.to_node())
else: else:
if css and self.stylesheets: if css and self.stylesheets:
css_attachments = self.css() or [] css_attachments = self.css() or []
for attachment in css_attachments: for attachment in css_attachments:
response.append(u'<link href="%s" rel="stylesheet"/>' % url_for(attachment.url)) attr = OrderedDict([
["type", "text/css"],
["rel", "stylesheet"],
["href", attachment.url],
])
response.append(("link", attr, None))
if self.css_errors: if self.css_errors:
msg = '\n'.join(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: if js and self.javascripts:
response.append(u'<script %s type="text/javascript" src="%s"></script>' % (async and u'async="async"' or '', url_for(self.js().url))) attr = OrderedDict([
response.extend(self.remains) ["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 @func.lazy_property
def last_modified(self): def last_modified(self):
@@ -152,7 +183,7 @@ class AssetsBundle(object):
Not really a full checksum. Not really a full checksum.
We compute a SHA1 on the rendered bundle + max linked files last_modified date 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() return hashlib.sha1(check.encode('utf-8')).hexdigest()
def clean_attachments(self, type): def clean_attachments(self, type):
@@ -314,7 +345,7 @@ class AssetsBundle(object):
outdated = False outdated = False
assets = dict((asset.html_url, asset) for asset in self.stylesheets if isinstance(asset, atype)) assets = dict((asset.html_url, asset) for asset in self.stylesheets if isinstance(asset, atype))
if assets: if assets:
assets_domain = [('url', 'in', list(assets))] assets_domain = [('url', 'in', list(assets.keys()))]
attachments = self.env['ir.attachment'].sudo().search(assets_domain) attachments = self.env['ir.attachment'].sudo().search(assets_domain)
for attachment in attachments: for attachment in attachments:
asset = assets[attachment.url] asset = assets[attachment.url]
@@ -474,7 +505,20 @@ class WebAsset(object):
except Exception: except Exception:
raise AssetNotFound("Could not find %s" % self.name) raise AssetNotFound("Could not find %s" % self.name)
# depreciated and will remove after v11
def to_html(self): 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() raise NotImplementedError()
@func.lazy_property @func.lazy_property
@@ -533,13 +577,19 @@ class JavascriptAsset(WebAsset):
try: try:
return super(JavascriptAsset, self)._fetch_content() return super(JavascriptAsset, self)._fetch_content()
except AssetError as e: 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: if self.url:
return '<script type="text/javascript" src="%s"></script>' % (self.html_url) return ("script", OrderedDict([
["type", "text/javascript"],
["src", self.html_url],
]), None)
else: else:
return '<script type="text/javascript" charset="utf-8">%s</script>' % self.with_header() return ("script", OrderedDict([
["type", "text/javascript"],
["charset", "utf-8"],
]), self.with_header())
class StylesheetAsset(WebAsset): class StylesheetAsset(WebAsset):
@@ -595,13 +645,21 @@ class StylesheetAsset(WebAsset):
content = re.sub(r' *([{}]) *', r'\1', content) content = re.sub(r' *([{}]) *', r'\1', content)
return self.with_header(content) return self.with_header(content)
def to_html(self): def to_node(self):
media = (' media="%s"' % misc.html_escape(self.media)) if self.media else ''
if self.url: if self.url:
href = self.html_url attr = OrderedDict([
return '<link rel="stylesheet" href="%s" type="text/css"%s/>' % (href, media) ["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: else:
return '<style type="text/css"%s>%s</style>' % (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): class PreprocessedCSS(StylesheetAsset):
+125 -40
View File
@@ -125,35 +125,122 @@ class IrQWeb(models.AbstractModel, QWeb):
if len(el): if len(el):
raise SyntaxError("t-call-assets cannot contain children nodes") 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 [ return [
self._append(ast.Call( ast.Assign(
func=ast.Attribute( targets=[ast.Name(id='nodes', ctx=ast.Store())],
value=ast.Name(id='self', ctx=ast.Load()), value=ast.Call(
attr='_get_asset', func=ast.Attribute(
ctx=ast.Load() 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=[ body=[
ast.Str(el.get('t-call-assets')), ast.If(
ast.Name(id='options', ctx=ast.Load()), test=ast.Name(id='index', ctx=ast.Load()),
], body=[self._append(ast.Str(sep))],
keywords=[ orelse=[]
ast.keyword('css', self._get_attr_bool(el.get('t-css', True))), ),
ast.keyword('js', self._get_attr_bool(el.get('t-js', True))), self._append(ast.Str(u'<')),
ast.keyword('debug', ast.Call( self._append(ast.Name(id='tagName', ctx=ast.Load())),
func=ast.Attribute( ] + self._append_attributes() + [
value=ast.Name(id='values', ctx=ast.Load()), ast.If(
attr='get', test=ast.BoolOp(
ctx=ast.Load() 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')], body=[self._append(ast.Str(u'/>'))],
keywords=[], starargs=None, kwargs=None orelse=[
)), self._append(ast.Str(u'>')),
ast.keyword('async', self._get_attr_bool(el.get('async', False))), ast.If(
ast.keyword('values', ast.Name(id='values', ctx=ast.Load())), 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 # 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): def _get_asset(self, xmlid, options, css=True, js=True, debug=False, async=False, values=None):
files, remains = self._get_asset_content(xmlid, options) files, remains = self._get_asset_content(xmlid, options)
asset = AssetsBundle(xmlid, files, remains, env=self.env) asset = AssetsBundle(xmlid, files, 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)) 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",)) @tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', keys=("website_id",))
def _get_asset_content(self, xmlid, options): def _get_asset_content(self, xmlid, options):
@@ -205,6 +293,9 @@ class IrQWeb(models.AbstractModel, QWeb):
env = self.env(context=options) 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. # 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 # It is currently necessary because the ir.ui.view bundle inheritance does not
# match the module dependency graph. # match the module dependency graph.
@@ -218,16 +309,13 @@ class IrQWeb(models.AbstractModel, QWeb):
files = [] files = []
remains = [] remains = []
for el in html.fragments_fromstring(template): for el in html.fragments_fromstring(template):
if isinstance(el, pycompat.string_types): if isinstance(el, html.HtmlElement):
remains.append(pycompat.to_text(el))
elif isinstance(el, html.HtmlElement):
href = el.get('href', '') href = el.get('href', '')
src = el.get('src', '') src = el.get('src', '')
atype = el.get('type') atype = el.get('type')
media = el.get('media') media = el.get('media')
can_aggregate = not urls.url_parse(href).netloc and not href.startswith('/web/content') if can_aggregate(href) and (el.tag == 'style' or (el.tag == 'link' and el.get('rel') == 'stylesheet')):
if el.tag == 'style' or (el.tag == 'link' and el.get('rel') == 'stylesheet' and can_aggregate):
if href.endswith('.sass'): if href.endswith('.sass'):
atype = 'text/sass' atype = 'text/sass'
elif href.endswith('.less'): elif href.endswith('.less'):
@@ -237,19 +325,16 @@ class IrQWeb(models.AbstractModel, QWeb):
path = [segment for segment in href.split('/') if segment] path = [segment for segment in href.split('/') if segment]
filename = get_resource_path(*path) if path else None filename = get_resource_path(*path) if path else None
files.append({'atype': atype, 'url': href, 'filename': filename, 'content': el.text, 'media': media}) 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' 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 filename = get_resource_path(*path) if path else None
files.append({'atype': atype, 'url': src, 'filename': filename, 'content': el.text, 'media': media}) files.append({'atype': atype, 'url': src, 'filename': filename, 'content': el.text, 'media': media})
else: else:
remains.append(html.tostring(el, encoding='unicode')) remains.append((el.tag, OrderedDict(el.attrib), el.text))
else: else:
try: # the other cases are ignored
remains.append(html.tostring(el, encoding='unicode')) pass
except Exception:
# notYETimplementederror
raise NotImplementedError
return (files, remains) return (files, remains)
+91 -54
View File
@@ -695,6 +695,80 @@ class QWeb(object):
ctx=ctx 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 # order
def _directives_eval_order(self): def _directives_eval_order(self):
@@ -733,7 +807,7 @@ class QWeb(object):
if not el.nsmap: if not el.nsmap:
unqualified_el_tag = el_tag = el.tag unqualified_el_tag = el_tag = el.tag
content = self._compile_directive_content(el, options) content = self._compile_directive_content(el, options)
attrib = el.attrib attrib = self._post_processing_att(el.tag, el.attrib, options)
else: else:
# Etree will remove the ns prefixes indirection by inlining the corresponding # Etree will remove the ns prefixes indirection by inlining the corresponding
# nsmap definition into the tag attribute. Restore the tag and prefix here. # nsmap definition into the tag attribute. Restore the tag and prefix here.
@@ -763,6 +837,8 @@ class QWeb(object):
else: else:
attrib[key] = value attrib[key] = value
attrib = self._post_processing_att(el.tag, attrib, options)
# Update the dict of inherited namespaces before continuing the recursion. Note: # Update the dict of inherited namespaces before continuing the recursion. Note:
# since `options['nsmap']` is a dict (and therefore mutable) and we do **not** # 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 # 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: if attr_already_created:
# for name, value in t_attrs.items(): # tagName = $el.tag
# if value or isinstance(value, basestring)): body.append(ast.Assign(
# append(u' ') targets=[ast.Name(id='tagName', ctx=ast.Store())],
# append(name) value=ast.Str(el.tag))
# append(u'="') )
# append(escape(to_text((value))) body.extend(self._append_attributes())
# 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=[]
))
return body return body
@@ -1513,6 +1542,14 @@ class QWeb(object):
atts = OrderedDict(atts) atts = OrderedDict(atts)
return 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): def _get_field(self, record, field_name, expression, tagName, field_options, options, values):
""" """
:returns: tuple: :returns: tuple:
@@ -38,7 +38,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
def _get_asset(self, xmlid, env=None): def _get_asset(self, xmlid, env=None):
env = (env or self.env) env = (env or self.env)
files, remains = env['ir.qweb']._get_asset_content(xmlid, env.context) 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): def _any_ira_for_bundle(self, type):
""" Returns all ir.attachments associated to a bundle, regardless of the verion. """ 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 = self._get_asset(self.jsbundle_xmlid)
bundle0.js() bundle0.js()
files0 = bundle0.files files0 = bundle0.files
remains0 = bundle0.remains
version0 = bundle0.version version0 = bundle0.version
self.assertEquals(len(self._any_ira_for_bundle('js')), 1) 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 = self._get_asset(self.jsbundle_xmlid, env=self.env(context={'check_view_ids': view.ids}))
bundle1.js() bundle1.js()
files1 = bundle1.files files1 = bundle1.files
remains1 = bundle1.remains
version1 = bundle1.version version1 = bundle1.version
self.assertNotEquals(files0, files1) self.assertNotEquals(files0, files1)
self.assertEquals(remains0, remains1)
self.assertNotEquals(version0, version1) self.assertNotEquals(version0, version1)
# check if the previous attachment are correctly cleaned # 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 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'max_css_rules': 1}))
bundle0.css() bundle0.css()
files0 = bundle0.files files0 = bundle0.files
remains0 = bundle0.remains
version0 = bundle0.version version0 = bundle0.version
self.assertEquals(len(self._any_ira_for_bundle('css')), 3) 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 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'check_view_ids': view.ids, 'max_css_rules': 1}))
bundle1.css() bundle1.css()
files1 = bundle1.files files1 = bundle1.files
remains1 = bundle1.remains
version1 = bundle1.version version1 = bundle1.version
self.assertNotEquals(files0, files1) self.assertNotEquals(files0, files1)
self.assertEquals(remains0, remains1)
self.assertNotEquals(version0, version1) self.assertNotEquals(version0, version1)
# check if the previous attachment are correctly cleaned # check if the previous attachment are correctly cleaned
@@ -318,6 +312,48 @@ class TestJavascriptAssetsBundle(TransactionCase):
content = bundle0.to_html() content = bundle0.to_html()
self.assertEqual(content.count('test_assetsbundle.bundle2.0.css'), 1) 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(), ("""<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="http://test.external.link/style1.css"/>
<link rel="stylesheet" href="http://test.external.link/style2.css"/>
<link type="text/css" rel="stylesheet" href="%(css)s"/>
<meta/>
<script type="text/javascript" src="http://test.external.link/javascript1.js"></script>
<script type="text/javascript" src="http://test.external.link/javascript2.js"></script>
<script type="text/javascript" src="%(js)s"></script>
</head>
<body>
</body>
</html>""" % {"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(), ("""<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="http://test.external.link/style1.css"/>
<link rel="stylesheet" href="http://test.external.link/style2.css"/>
<link type="text/css" rel="stylesheet" href="/test_assetsbundle/static/src/css/test_cssfile1.css"/>
<link type="text/css" rel="stylesheet" href="/test_assetsbundle/static/src/css/test_cssfile2.css"/>
<meta/>
<script type="text/javascript" src="http://test.external.link/javascript1.js"></script>
<script type="text/javascript" src="http://test.external.link/javascript2.js"></script>
<script type="text/javascript" src="/test_assetsbundle/static/src/js/test_jsfile1.js"></script>
<script type="text/javascript" src="/test_assetsbundle/static/src/js/test_jsfile2.js"></script>
<script type="text/javascript" src="/test_assetsbundle/static/src/js/test_jsfile3.js"></script>
</head>
<body>
</body>
</html>""").encode('utf8'))
class TestAssetsBundleInBrowser(HttpCase): class TestAssetsBundleInBrowser(HttpCase):
def test_01_js_interpretation(self): def test_01_js_interpretation(self):
@@ -23,4 +23,33 @@
<template id="test_assetsbundle.bundle3"> <template id="test_assetsbundle.bundle3">
<link rel="stylesheet" href="/test_assetsbundle/static/src/less/test_lessfile1.less"/> <link rel="stylesheet" href="/test_assetsbundle/static/src/less/test_lessfile1.less"/>
</template> </template>
<template id="test_assetsbundle.bundle4">
<script type="text/javascript" src="/test_assetsbundle/static/src/js/test_jsfile1.js"></script>
<script type="text/javascript" src="/test_assetsbundle/static/src/js/test_jsfile2.js"></script>
<script type="text/javascript" src="http://test.external.link/javascript1.js"></script>
<link rel="stylesheet" href="/test_assetsbundle/static/src/css/test_cssfile1.css"/>
<link rel="stylesheet" href="http://test.external.link/style1.css"/>
<link rel="stylesheet" href="/test_assetsbundle/static/src/css/test_cssfile2.css"/>
<script type="text/javascript" src="/test_assetsbundle/static/src/js/test_jsfile3.js"></script>
<script type="text/javascript" src="http://test.external.link/javascript2.js"></script>
<link rel="stylesheet" href="http://test.external.link/style2.css"/>
</template>
<template id="test_assetsbundle.template2" name="test template 2">
&lt;!DOCTYPE html&gt;
<html>
<head>
<t t-call-assets="test_assetsbundle.bundle4" t-js="False"/>
<meta/>
<t t-call-assets="test_assetsbundle.bundle4" t-css="False"/>
</head>
<body>
</body>
</html>
</template>
</odoo> </odoo>