diff --git a/addons/web_editor/models/ir_qweb.py b/addons/web_editor/models/ir_qweb.py
index b6082bc0597..a3376e67ca4 100644
--- a/addons/web_editor/models/ir_qweb.py
+++ b/addons/web_editor/models/ir_qweb.py
@@ -41,23 +41,23 @@ logger = logging.getLogger(__name__)
class QWeb(orm.AbstractModel):
""" QWeb object for rendering editor stuff
"""
- _name = 'ir.qweb'
_inherit = 'ir.qweb'
- re_remove_spaces = re.compile('\s+')
+ # compile directives
- def _compile_directive_snippet(self, el, ast_calls):
+ def _compile_directive_snippet(self, el, options):
el.set('t-call', el.attrib.pop('t-snippet'))
- qwebcontext = ir_qweb.QWebContext(self.env, {})
name = self.env['ir.ui.view'].search([('key', '=', el.attrib.get('t-call'))]).display_name
thumbnail = el.attrib.pop('t-thumbnail', "oe-thumbnail")
div = u'
' % (escape(ir_qweb.unicodifier(name)), escape(ir_qweb.unicodifier(thumbnail)))
- return [self._append(ast.Str(div))] + self._compile_node(el, ast_calls) + [self._append(ast.Str(u'
'))]
+ return [self._append(ast.Str(div))] + self._compile_node(el, options) + [self._append(ast.Str(u''))]
- def _compile_directive_tag(self, el, ast_calls):
+ def _compile_directive_tag(self, el, options):
if el.get('t-placeholder'):
el.set('t-att-placeholder', el.attrib.pop('t-placeholder'))
- return super(QWeb, self)._compile_directive_tag(el, ast_calls)
+ return super(QWeb, self)._compile_directive_tag(el, options)
+
+ # order and ignore
def _directives_eval_order(self):
directives = super(QWeb, self)._directives_eval_order()
@@ -67,6 +67,7 @@ class QWeb(orm.AbstractModel):
def _nondirectives_ignore(self):
return {'t-thumbnail'} | super(QWeb, self)._nondirectives_ignore()
+
#------------------------------------------------------
# QWeb fields
#------------------------------------------------------
diff --git a/addons/web_editor/models/ir_ui_view.py b/addons/web_editor/models/ir_ui_view.py
index 762312bc28f..d7bf92b1cab 100644
--- a/addons/web_editor/models/ir_ui_view.py
+++ b/addons/web_editor/models/ir_ui_view.py
@@ -42,7 +42,8 @@ class view(osv.osv):
Model = self.env[el.get('data-oe-model')]
field = el.get('data-oe-field')
- converter = self.env['ir.qweb'].get_field_for(el.get('data-oe-type'))
+ model = 'ir.qweb.field.' + el.get('data-oe-type')
+ converter = self.env[model] if model in self.env else self.env['ir.qweb.field']
value = converter.from_html(Model, Model._fields[field], el)
if value is not None:
diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py
index 6c9cd9f41b7..77cd961a636 100644
--- a/addons/website/models/ir_qweb.py
+++ b/addons/website/models/ir_qweb.py
@@ -20,13 +20,6 @@ class QWeb(orm.AbstractModel):
'img': 'src',
}
- PRESERVE_WHITESPACE = [
- 'pre',
- 'textarea',
- 'script',
- 'style',
- ]
-
def _website_build_attribute(self, tagName, name, value, qwebcontext):
context = qwebcontext.env.context or {}
if not context.get('rendering_bundle'):
@@ -36,7 +29,10 @@ class QWeb(orm.AbstractModel):
return request.website.get_cdn_url(value or '')
return value
- def _wrap_build_attributes(self, el, items):
+ def _wrap_build_attributes(self, el, items, options):
+ if options.get('rendering_bundle'):
+ return items
+
url_att = self.URL_ATTRS.get(el.tag)
cdn_att = self.CDN_TRIGGERS.get(el.tag)
for item in items:
@@ -52,21 +48,26 @@ class QWeb(orm.AbstractModel):
ast.Str(item[0]),
item[1],
ast.Name(id='qwebcontext', ctx=ast.Load()),
+ ast.Name(id='options', ctx=ast.Load()),
], keywords=[],
starargs=None, kwargs=None
))
return items
- def _serialize_static_attributes(self, el):
- items = super(QWeb, self)._serialize_static_attributes(el)
- return self._wrap_build_attributes(el, items)
+ def _serialize_static_attributes(self, el, options):
+ items = super(QWeb, self)._serialize_static_attributes(el, options)
+ return self._wrap_build_attributes(el, items, options)
- def _compile_dynamic_attributes(self, el):
- items = super(QWeb, self)._compile_dynamic_attributes(el)
- return self._wrap_build_attributes(el, items)
+ 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 _compile_dynamic_att(self, tagName, atts, qwebcontext):
atts = super(QWeb, self)._compile_dynamic_att(tagName, atts, qwebcontext)
+ if options.get('rendering_bundle'):
+ return atts
for name, value in atts.iteritems():
atts[name] = self.build_attribute(tagName, name, value, qwebcontext)
return atts
diff --git a/openerp/addons/base/ir/ir_qweb/__init__.py b/openerp/addons/base/ir/ir_qweb/__init__.py
index 926d8047576..10f5dfce4df 100644
--- a/openerp/addons/base/ir/ir_qweb/__init__.py
+++ b/openerp/addons/base/ir/ir_qweb/__init__.py
@@ -3,8 +3,7 @@ from openerp.tools import safe_eval, html_escape as escape
from . import fields
from . import assetsbundle
-from . import utils
from .assetsbundle import AssetsBundle
from .utils import QWebContext, unicodifier
-from .qweb import QWeb
+from .ir_qweb import ir_QWeb, QWeb
diff --git a/openerp/addons/base/ir/ir_qweb/ir_qweb.py b/openerp/addons/base/ir/ir_qweb/ir_qweb.py
index 141ec916d91..7b59425684a 100644
--- a/openerp/addons/base/ir/ir_qweb/ir_qweb.py
+++ b/openerp/addons/base/ir/ir_qweb/ir_qweb.py
@@ -40,26 +40,19 @@ class ir_QWeb(models.AbstractModel, QWeb):
_logger.warning("Unused method '%s' is found in ir.qweb." % method)
body = []
-
- import time
- _ = []
- ap = _.append
- t = time.time()
- def append(data):
- ap([time.time()-t, data])
- body.append(data)
-
context = dict(self.env.context, dev_mode='qweb' in tools.config['dev_mode'])
context.update(options)
- self.compile(id_or_xml_id, context)(self, append, values or {})
-
- # t = _[0][0]
- # for v in _:
- # print round((v[0]-t)*1000000)/1000, v[1]
-
+ self.compile(id_or_xml_id, context)(self, body.append, values or {})
return u''.join(body)
+ def default_values(self):
+ """ attributes add to the values for each computed template
+ """
+ default = super(ir_QWeb, self).default_values()
+ default.update(request=request)
+ return default
+
# assume cache will be invalidated by third party on write to ir.ui.view
def _get_template_cache_keys(self):
""" Return the list of context keys to use for caching ``_get_template``. """
@@ -155,9 +148,10 @@ class ir_QWeb(models.AbstractModel, QWeb):
files, remains = self._get_asset_content(xmlid, options)
return AssetsBundle(xmlid, files, remains, env=self.env)
- @tools.ormcache('xmlid', 'options["lang"]')
+ @tools.ormcache('xmlid', 'options.get("lang", "en_US")')
def _get_asset_content(self, xmlid, options):
- options = dict(options,
+ options = dict(self.env.context)
+ options.update(options,
inherit_branding=False, inherit_branding_auto=False,
edit_translations=False, translatable=False,
rendering_bundle=True)
@@ -264,3 +258,14 @@ class ir_QWeb(models.AbstractModel, QWeb):
# ast.Expression().body -> expr
return Contextifier().visit(st).body
+
+ def _get_attr_bool(self, attr, default=False):
+ if attr:
+ if attr is True:
+ return ast.Name(id='True', ctx=ast.Load())
+ attr = attr.lower()
+ if attr in ('false', '0'):
+ return ast.Name(id='False', ctx=ast.Load())
+ elif attr in ('true', '1'):
+ return ast.Name(id='True', ctx=ast.Load())
+ return ast.Name(id=str(attr if attr is False else default), ctx=ast.Load())
diff --git a/openerp/addons/base/ir/ir_qweb/qweb.py b/openerp/addons/base/ir/ir_qweb/qweb.py
index 1dfa52e0502..8d14c324cb8 100644
--- a/openerp/addons/base/ir/ir_qweb/qweb.py
+++ b/openerp/addons/base/ir/ir_qweb/qweb.py
@@ -1,24 +1,15 @@
# -*- coding: utf-8 -*-
-
import ast
-from collections import OrderedDict
-import json
-from urlparse import urlparse
+from collections import OrderedDict, Sized, Mapping, defaultdict
from lxml import etree, html
import re
-import itertools
-import textwrap
import traceback
-
-import openerp.tools
-from odoo.exceptions import QWebException
-
-from openerp.tools import html_escape
-from . import utils
-from .utils import QWebContext, unicodifier
-from .assetsbundle import AssetsBundle
-
-from odoo import models
+from itertools import count
+from textwrap import dedent
+from werkzeug.utils import escape
+from itertools import izip, tee
+import __builtin__
+builtin_defaults = {name: getattr(__builtin__, name) for name in dir(__builtin__)}
try:
import astor
@@ -29,178 +20,470 @@ import logging
_logger = logging.getLogger(__name__)
-class QWeb(models.AbstractModel):
- """ Base QWeb rendering engine
- * to customize ``t-field`` rendering, subclass ``ir.qweb.field`` and
- create new models called :samp:`ir.qweb.field.{widget}`
- * alternatively, override :meth:`~.get_field_for` and return an
- arbitrary model to use as field converter
- Beware that if you need extensions or alterations which could be
- incompatible with other subsystems, you should create a local object
- inheriting from ``ir.qweb`` and customize that.
+####################################
+### qweb tools ###
+####################################
+
+
+class Contextifier(ast.NodeTransformer):
+ """ For user-provided template expressions, replaces any ``name`` by
+ :sampe:`values.get('{name}')` so all variable accesses are
+ performed on the values rather than in the "native" values
"""
- _name = 'ir.qweb'
+ # some people apparently put lambdas in template expressions. Turns out
+ # the AST -> bytecode compiler does *not* appreciate parameters of lambdas
+ # being converted from names to subscript expressions, and most likely the
+ # reference to those parameters inside the lambda's body should probably
+ # remain as-is. Because we're transforming an AST, the structure should
+ # be lexical, so just store a set of "safe" parameter names and recurse
+ # through the lambda using a new NodeTransformer
+ def __init__(self, params=()):
+ super(Contextifier, self).__init__()
+ self._safe_names = tuple(params)
- def get_template(self, name, loader, origin_template=None):
- try:
- document = loader(name)
- except ValueError:
- raise QWebException("%s\nLoader could not find template %r" % (traceback.format_exc(), name), template=origin_template)
+ def visit_Name(self, node):
+ if node.id in self._safe_names:
+ return node
+
+ return ast.copy_location(
+ # values.get(name)
+ ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='values', ctx=ast.Load()),
+ attr='get',
+ ctx=ast.Load()
+ ),
+ args=[ast.Str(node.id)], keywords=[],
+ starargs=None, kwargs=None
+ ),
+ node
+ )
+
+ def visit_Lambda(self, node):
+ args = node.args
+ # assume we don't have any tuple parameter, just names
+ names = [arg.id for arg in args.args]
+ if args.vararg: names.append(args.vararg)
+ if args.kwarg: names.append(args.kwarg)
+ # remap defaults in case there's any
+ return ast.copy_location(ast.Lambda(
+ args=ast.arguments(
+ args=args.args,
+ defaults=map(self.visit, args.defaults),
+ vararg=args.vararg,
+ kwarg=args.kwarg,
+ ),
+ body=Contextifier(self._safe_names + tuple(names)).visit(node.body)
+ ), node)
+
+ # "lambda problem" also exists with comprehensions
+ def _visit_comp(self, node):
+ # CompExp(?, comprehension* generators)
+ # comprehension = (expr target, expr iter, expr* ifs)
+
+ # collect names in generators.target
+ names = tuple(
+ node.id
+ for gen in node.generators
+ for node in ast.walk(gen.target)
+ if isinstance(node, ast.Name)
+ )
+ transformer = Contextifier(self._safe_names + names)
+ # copy node
+ newnode = ast.copy_location(type(node)(), node)
+ # then visit the comp ignoring those names, transformation is
+ # probably expensive but shouldn't be many comprehensions
+ for field, value in ast.iter_fields(node):
+ # map transformation of comprehensions
+ if isinstance(value, list):
+ setattr(newnode, field, map(transformer.visit, value))
+ else: # set transformation of key/value/expr fields
+ setattr(newnode, field, transformer.visit(value))
+ return newnode
+ visit_GeneratorExp = visit_ListComp = visit_SetComp = visit_DictComp = _visit_comp
+
+
+class QWebException(Exception):
+ def __init__(self, message, error=None, path=None, html=None, name=None, astmod=None):
+ self.error = error
+ self.message = message
+ self.path = path
+ self.html = html
+ self.name = name
+ self.stack = traceback.format_exc()
+ if astmod:
+ if astor:
+ self.code = astor.to_source(astmod)
+ else:
+ self.code = "Please install astor to display the compiled code"
+ self.stack += "\nInstall `astor` for compiled source information."
else:
- if document is not None:
- if hasattr(document, 'documentElement'):
- dom = document
- elif document.startswith("', 'exec'), ns)
+ except QWebException, e:
+ raise e
+ except Exception, e:
+ path = _options['last_path_node']
+ node = element.getroottree().xpath(path)
+ raise QWebException("Error when compiling AST", e, path, node and etree.tostring(node[0]), name)
+
+ # return the wrapped function
+
+ def _compiled_fn(self, append, values):
+ log = {'last_path_node': None}
+ values.update(self.default_values())
+ try:
+ return ns[_options['ast_calls'][0].name](self, append, values, options, log)
+ except QWebException, e:
+ raise e
+ except Exception, e:
+ path = log['last_path_node']
+ element, document = self.get_template(template, options)
+ node = element.getroottree().xpath(path)
+ raise QWebException("Error to render compiling AST", e, path, node and etree.tostring(node[0]), name)
+
+ return _compiled_fn
+
+ def default_values(self):
+ """ attributes add to the values for each computed template
+ """
+ return {
+ 'True': True,
+ 'False': False,
+ 'None': None,
+ 'str': str,
+ 'unicode': unicode,
+ 'bool': bool,
+ 'int': int,
+ 'float': float,
+ 'long': long,
+ 'enumerate': enumerate,
+ 'dict': dict,
+ 'list': list,
+ 'tuple': tuple,
+ 'map': map,
+ 'abs': abs,
+ 'min': min,
+ 'max': max,
+ 'sum': sum,
+ 'reduce': reduce,
+ 'filter': filter,
+ 'round': round,
+ 'len': len,
+ 'repr': repr,
+ 'set': set,
+ 'all': all,
+ 'any': any,
+ 'ord': ord,
+ 'chr': chr,
+ 'cmp': cmp,
+ 'divmod': divmod,
+ 'isinstance': isinstance,
+ 'range': range,
+ 'xrange': xrange,
+ 'zip': zip,
+
+ 'format': self.format
+ }
+
+ def get_template(self, template, options):
+ if isinstance(template, etree._Element):
+ document = template
+ template = etree.tostring(template)
+ else:
+ try:
+ document = options.get('load', self.load)(template, options)
+ except QWebException, e:
+ raise e
+ except Exception, e:
+ raise QWebException("load could not load template", name=template)
+
+ if document is not None:
+ if isinstance(document, etree._Element):
+ element = document
+ document = etree.tostring(document)
+ elif document.startswith("', 'exec'), ns)
- except Exception, e:
- raise formatException(e, "Error when compiling AST")
-
- def _compiled_fn(qw):
- try:
- return ns[ast_calls[-1].name](self, qw)
- except Exception, e:
- raise formatException(e, "Error to render compiling AST")
-
- return _compiled_fn
+ return ast.Call(
+ func=ast.Name(id=name, ctx=ast.Load()),
+ args=[
+ ast.Name(id='self', ctx=ast.Load()),
+ ast.Name(id=append, ctx=ast.Load()) if isinstance(append, str) else append,
+ ast.Name(id=values, ctx=ast.Load()),
+ ast.Name(id='options', ctx=ast.Load()),
+ ast.Name(id='log', ctx=ast.Load()),
+ ],
+ keywords=[], starargs=None, kwargs=None
+ )
def _append(self, item):
assert isinstance(item, ast.expr) or isinstance(item, ast.Expr)
return ast.Expr(ast.Call(
- func=ast.Attribute(
- value=ast.Name(id='output', ctx=ast.Load()),
- attr='append',
- ctx=ast.Load()
- ), args=[item], keywords=[],
+ func=ast.Name(id='append', ctx=ast.Load()),
+ args=[item], keywords=[],
starargs=None, kwargs=None
))
def _extend(self, items):
- return ast.Expr(ast.Call(
- func=ast.Attribute(
- value=ast.Name(id='output', ctx=ast.Load()),
- attr='extend',
- ctx=ast.Load()
- ), args=[items], keywords=[],
- starargs=None, kwargs=None
- ))
+ # for x in iterator:
+ # append(x)
+ var = self._make_name()
+ return ast.For(
+ target=ast.Name(id=var, ctx=ast.Store()),
+ iter=items,
+ body=[ast.Expr(ast.Call(
+ func=ast.Name(id='append', ctx=ast.Load()),
+ args=[ast.Name(id=var, ctx=ast.Load())], keywords=[],
+ starargs=None, kwargs=None
+ ))],
+ orelse=[]
+ )
def _if_content_is_not_Falsy(self, body, orelse):
return ast.If(
@@ -220,53 +503,33 @@ class QWeb(models.AbstractModel):
)
]
),
- # output.append(escape($content))
+ # append(escape($content))
body=body,
- # output.append(body default value)
+ # append(body default value)
orelse=orelse,
)
- def _call_body(self, ast_calls, body, args=('self', 'qwebcontext',), prefix='fn', lineno=None):
- """
- If ``body`` is non-empty, generates (and globally store) the
- corresponding function definition and returns the relevant ast.Call
- node.
- If ``body`` is empty, doesn't do anything and returns ``None``.
- """
- if not body:
- return None
- name = self._make_name(prefix)
- ast_calls.append(utils.base_fn_def(body, name=name, lineno=lineno))
- return ast.Call(
- func=ast.Name(id=name, ctx=ast.Load()),
- args=[ast.Name(id=arg, ctx=ast.Load()) for arg in args],
- keywords=[], starargs=None, kwargs=None
- )
-
def _make_name(self, prefix='var'):
return "%s_%s" % (prefix, next(self._name_gen))
- def _compile_document(self, element):
- """
- Compiles a document rooted in ``element`` to a Python AST.
- Calls the following hooks:
- * :samp:`_compile_directive_{name}` compiles the directive of the
- specified name to AST
- In either case, the current etree._Element is received as first
- parameter and the list of ast_calls as second. A brand new list
- of AST nodes *must* be returned (can be an empty list).
- :return a list of AST nodes, the last AST node is the template call
- """
- ast_calls = []
- self._call_body(ast_calls,
- self._compile_directive_content(element, ast_calls),
- prefix='template_%s' % element.get('t-name', 'unknown').replace('.', '_'))
- return ast_calls
-
- def _compile_node(self, el, ast_calls):
+ def _compile_node(self, el, options):
"""
:return ast list
"""
+ path = options['root'].getpath(el)
+ if options['last_path_node'] != path:
+ options['last_path_node'] = path
+ # options['last_path_node'] = $path
+ body = [ast.Assign(
+ targets=[ast.Subscript(
+ value=ast.Name(id='log', ctx=ast.Load()),
+ slice=ast.Index(ast.Str('last_path_node')),
+ ctx=ast.Store())],
+ value=ast.Str(path)
+ )]
+ else:
+ body = []
+
ignored = self._nondirectives_ignore()
if el.get("groups"):
@@ -284,6 +547,7 @@ class QWeb(models.AbstractModel):
# skip directives not present on the element
if name not in directives:
continue
+
directives.remove(name)
mname = name.replace('-', '_')
compile_handler = getattr(self, '_compile_directive_%s' % mname, None)
@@ -293,11 +557,22 @@ class QWeb(models.AbstractModel):
"Directive '%s' must be AOT-compiled. Dynamic interpreter %s will ignored",
name, interpret_handler
)
- return compile_handler(el, ast_calls)
+ if not compile_handler:
+ continue
+ return body + compile_handler(el, options)
if directives:
raise "Unknown directive '%s' on %s" % ("', '".join(directives), etree.tostring(el))
- return []
+ return body + self._compile_directive_content(el, options)
+
+ def _values_var(self, varname, ctx):
+ return ast.Subscript(
+ value=ast.Name(id='values', ctx=ast.Load()),
+ slice=ast.Index(varname),
+ ctx=ctx
+ )
+
+ # order and ignore
def _directives_eval_order(self):
""" Should list all supported directives in the order in which they
@@ -315,8 +590,7 @@ class QWeb(models.AbstractModel):
"""
return [
'debug',
- 'groups', 'foreach', 'if',
- 'call-assets',
+ 'groups', 'foreach', 'if', 'else',
'field',
'tag',
'call',
@@ -331,50 +605,45 @@ class QWeb(models.AbstractModel):
:returns: set
"""
return {
- 't-name', 't-field-options', 't-as', 't-lang',
- 't-value', 't-valuef', 't-ignore', 't-js', 't-css', 't-async', 't-placeholder',
+ 't-name', 't-field-options', 't-call-options'
+ 't-as''t-value', 't-valuef', 't-ignore',
+ 't-js', 't-css', 't-async', 't-placeholder',
}
- def _serialize_static_attributes(self, el):
+ # compile directives
+
+ def _serialize_static_attributes(self, el, options):
nodes = []
for key, value in el.attrib.iteritems():
if not key.startswith('t-'):
nodes.append((key, ast.Str(value)))
return nodes
- def _compile_dynamic_attributes(self, el):
+ def _compile_dynamic_attributes(self, el, options):
nodes = []
for name, value in el.attrib.iteritems():
if name.startswith('t-attf-'):
- nodes.append((name[7:], utils.compile_format(value)))
+ nodes.append((name[7:], self._compile_format(value)))
elif name.startswith('t-att-'):
- nodes.append((name[6:], utils.compile_expr(value)))
+ nodes.append((name[6:], self._compile_expr(value)))
elif name == 't-att':
nodes.append(ast.Call(
func=ast.Attribute(
value=ast.Name(id='self', ctx=ast.Load()),
- attr='_compile_dynamic_att',
+ attr='_get_dynamic_att',
ctx=ast.Load()
),
args=[
ast.Str(el.tag),
- utils.compile_expr(value),
- ast.Name(id='qwebcontext', ctx=ast.Load()),
+ self._compile_expr(value),
+ ast.Name(id='options', ctx=ast.Load()),
+ ast.Name(id='values', ctx=ast.Load()),
], keywords=[],
starargs=None, kwargs=None
))
return nodes
- def _compile_dynamic_att(self, tagName, atts, qwebcontext):
- if isinstance(atts, OrderedDict):
- return atts
- if isinstance(atts, (list, tuple)) and not isinstance(atts[0], (list, tuple)):
- atts = [atts]
- if isinstance(atts, (list, tuple)):
- atts = OrderedDict(atts)
- return atts
-
- def _compile_all_attributes(self, el, attr_already_created=False):
+ def _compile_all_attributes(self, el, options, attr_already_created=False):
body = []
if any(name.startswith('t-att') or not name.startswith('t-') for name, value in el.attrib.iteritems()):
if not attr_already_created:
@@ -390,7 +659,7 @@ class QWeb(models.AbstractModel):
)
)
- items = self._serialize_static_attributes(el) + self._compile_dynamic_attributes(el)
+ items = self._serialize_static_attributes(el, options) + self._compile_dynamic_attributes(el, options)
for item in items:
if isinstance(item, tuple):
body.append(ast.Assign(
@@ -416,11 +685,11 @@ class QWeb(models.AbstractModel):
if attr_already_created:
# for name, value in t_attrs.iteritems():
# if value or isinstance(value, basestring)):
- # output.append(u' ')
- # output.append(name)
- # output.append(u'="')
- # output.append(escape(unicodifier(value)))
- # output.append(u'"')
+ # append(u' ')
+ # append(name)
+ # append(u'="')
+ # append(qweb_escape(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(
@@ -453,14 +722,8 @@ class QWeb(models.AbstractModel):
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='unicodifier', ctx=ast.Load()),
- args=[ast.Name(id='value', ctx=ast.Load())], keywords=[],
- starargs=None, kwargs=None
- )
- ], keywords=[],
+ func=ast.Name(id='qweb_escape', ctx=ast.Load()),
+ args=[ast.Name(id='value', ctx=ast.Load())], keywords=[],
starargs=None, kwargs=None
)),
self._append(ast.Str(u'"')),
@@ -472,9 +735,9 @@ class QWeb(models.AbstractModel):
return body
- def _compile_tag(self, el, content, attr_already_created=False):
+ def _compile_tag(self, el, content, options, attr_already_created=False):
body = [self._append(ast.Str(u'<%s' % el.tag))]
- body.extend(self._compile_all_attributes(el, attr_already_created))
+ body.extend(self._compile_all_attributes(el, options, attr_already_created))
if el.tag in self._void_elements:
body.append(self._append(ast.Str(u'/>')))
body.extend(content)
@@ -484,93 +747,113 @@ class QWeb(models.AbstractModel):
body.append(self._append(ast.Str(u'%s>' % el.tag)))
return body
- def _compile_directive_debug(self, el, ast_calls):
+ def _compile_directive_debug(self, el, options):
debugger = el.attrib.pop('t-debug')
- body = self._compile_node(el, ast_calls)
- if 'qweb' in openerp.tools.config['dev_mode']:
+ body = self._compile_node(el, options)
+ if options['dev_mode']:
body = ast.parse("__import__('%s').set_trace()" % re.sub('[^a-zA-Z]', '', debugger)).body + body # pdb, ipdb, pudb, ...
else:
- _logger.warning("@t-debug in template is only available in --dev mode")
+ _logger.warning("@t-debug in template is only available in dev mode options")
return body
- def _compile_directive_tag(self, el, ast_calls):
+ def _compile_directive_tag(self, el, options):
el.attrib.pop('t-tag', None)
- content = self._compile_node(el, ast_calls)
+ content = self._compile_node(el, options)
if el.tag == 't':
return content
- return self._compile_tag(el, content, False)
+ return self._compile_tag(el, content, options, False)
+
+ def _compile_directive_set(self, el, options):
+ body = []
+ varname = el.attrib.pop('t-set')
+ varset = self._values_var(ast.Str(varname), ctx=ast.Store())
- def _compile_directive_set(self, el, ast_calls):
if 't-value' in el.attrib:
- value = utils.compile_expr(el.attrib.pop('t-value'))
+ value = self._compile_expr(el.attrib.pop('t-value'))
elif 't-valuef' in el.attrib:
- value = utils.compile_format(el.attrib.pop('t-valuef'))
+ value = self._compile_format(el.attrib.pop('t-valuef'))
else:
- render = self._call_body(ast_calls, self._compile_directive_content(el, ast_calls), prefix='set', lineno=el.sourceline)
- if render is None:
- value = ast.Str(u'')
+ # set the content as value
+ body = self._compile_directive_content(el, options)
+ if body:
+ return [
+ # $varset = []
+ ast.Assign(
+ targets=[
+ self._values_var(ast.Str(varname), ctx=ast.Store())
+ ],
+ value=ast.List(elts=[], ctx=ast.Load())
+ ),
+ # set(self, $varset.append, $varset, options)
+ ast.Expr(self._call_body(options, body,
+ append=ast.Attribute(
+ value=self._values_var(ast.Str(varname), ctx=ast.Load()),
+ attr='append',
+ ctx=ast.Load()
+ ),
+ prefix='set',
+ lineno=el.sourceline)
+ ),
+ # $varset = u''.join($varset)
+ ast.Assign(
+ targets=[self._values_var(ast.Str(varname), ctx=ast.Store())],
+ value=ast.Call(
+ func=ast.Attribute(value=ast.Str(u''), attr='join', ctx=ast.Load()),
+ args=[self._values_var(ast.Str(varname), ctx=ast.Load())], keywords=[],
+ starargs=None, kwargs=None
+ )
+ )
+ ]
+
else:
- # concat body render to string
- value = ast.Call(
- func=ast.Attribute(value=ast.Str(u''), attr='join', ctx=ast.Load()),
- args=[render], keywords=[],
- starargs=None, kwargs=None
- )
+ value = ast.Str(u'')
- return [
- ast.Assign(
- targets=[ast.Subscript(
- value=ast.Name(id='qwebcontext', ctx=ast.Load()),
- slice=ast.Index(ast.Str(el.attrib.pop('t-set'))),
- ctx=ast.Store()
- )],
- value=value
- )]
+ # $varset = $value
+ return [ast.Assign(
+ targets=[self._values_var(ast.Str(varname), ctx=ast.Store())],
+ value=value
+ )]
- def _compile_directive_esc(self, el, ast_calls):
+ def _compile_directive_esc(self, el, options):
return [
# content = t-raw value
ast.Assign(
targets=[ast.Name(id='content', ctx=ast.Store())],
- value=utils.compile_expr0(el.attrib.pop('t-esc')),
+ value=self._compile_expr0(el.attrib.pop('t-esc')),
),
# if content is not None: display
self._if_content_is_not_Falsy(
- # output.append(escape($content))
+ # append(escape($content))
body=[self._append(ast.Call(
- func=ast.Name(id='escape', ctx=ast.Load()),
- args=[ast.Call(
- func=ast.Name(id='unicodifier', ctx=ast.Load()),
- args=[ast.Name(id='content', ctx=ast.Load())], keywords=[],
- starargs=None, kwargs=None
- )],
- keywords=[], starargs=None, kwargs=None
+ func=ast.Name(id='qweb_escape', ctx=ast.Load()),
+ args=[ast.Name(id='content', ctx=ast.Load())], keywords=[],
+ starargs=None, kwargs=None
))],
- # output.append(body default value)
- orelse=self._compile_directive_content(el, ast_calls),
+ # append(body default value)
+ orelse=self._compile_directive_content(el, options),
)
]
- def _compile_directive_raw(self, el, ast_calls):
+ def _compile_directive_raw(self, el, options):
return [
# content = t-raw value
ast.Assign(
targets=[ast.Name(id='content', ctx=ast.Store())],
- value=utils.compile_expr0(el.attrib.pop('t-raw')),
+ value=self._compile_expr0(el.attrib.pop('t-raw')),
),
self._if_content_is_not_Falsy(
- # output.append($content)
+ # append($content)
body=[self._append(ast.Call(
func=ast.Name(id='unicodifier', ctx=ast.Load()),
args=[ast.Name(id='content', ctx=ast.Load())], keywords=[],
starargs=None, kwargs=None
))],
- # output.append(body default value)
- orelse=self._compile_directive_content(el, ast_calls),
+ # append(body default value)
+ orelse=self._compile_directive_content(el, options),
)
]
- def _compile_directive_content(self, el, ast_calls):
+ def _compile_directive_content(self, el, options):
body = []
if el.text is not None:
body.append(self._append(ast.Str(unicodifier(el.text))))
@@ -582,139 +865,33 @@ class QWeb(models.AbstractModel):
item.set('t-tag', item.tag)
if not (set(['t-esc', 't-raw', 't-field']) & set(item.attrib)):
item.set('t-content', 'True')
- body.extend(self._compile_node(item, ast_calls))
+ body.extend(self._compile_node(item, options))
body.extend(self._compile_tail(item))
return body
- def _compile_directive_call(self, el, ast_calls):
- """
- :param etree._Element el:
- :param list ast call:
- :return: new body
- :rtype: list(ast.AST)
- """
- tmpl = el.attrib.pop('t-call')
- qw = self._make_name('qwebcontext_copy')
- sub_content = self._call_body(ast_calls, self._compile_directive_content(el, ast_calls),
- prefix='body_call_content', args=('self', qw,), lineno=el.sourceline)
- lang = el.get('t-lang', "'en_US'")
+ def _compile_directive_else(self, el, options):
+ if not options.pop('t_if', None):
+ raise "t-else directive must be call by t-if directive"
+ el.attrib.pop('t-else')
+ return self._compile_node(el, options)
- content = [
- # qwebcontext_copy = qwebcontext.copy()
- ast.Assign(
- targets=[ast.Name(id=qw, ctx=ast.Store())],
- value=ast.Call(
- func=ast.Attribute(
- value=ast.Name(id='qwebcontext', ctx=ast.Load()),
- attr='copy',
- ctx=ast.Load()
- ),
- args=[], keywords=[],
- starargs=None, kwargs=None
- )
- )
- ]
-
- qw_0_ast = ast.Subscript(
- value=ast.Name(id=qw, ctx=ast.Load()),
- slice=ast.Index(ast.Num(0)),
- ctx=ast.Store()
- )
-
- # qwebcontext_copy[0] = body_call_content or u''
- content.append(
- ast.Assign(
- targets=[qw_0_ast],
- value=sub_content or ast.Str(u'')
- ))
-
- # qwebcontext_copy.env = qwebcontext.env(context=dict(qwebcontext.env.context, lang=$lang))
- if lang:
- content.append(
- ast.Assign(
- targets=[ast.Attribute(
- value=ast.Name(id=qw, ctx=ast.Load()),
- attr='env',
- ctx=ast.Store()
- )],
- value=ast.Call(
- func=ast.Attribute(
- value=ast.Name(id='qwebcontext', ctx=ast.Load()),
- attr='env',
- ctx=ast.Load()
- ),
- args=[],
- keywords=[ast.keyword('context', ast.Call(
- func=ast.Name(id='dict', ctx=ast.Load()),
- args=[ast.Attribute(
- value=ast.Attribute(
- value=ast.Name(id='qwebcontext', ctx=ast.Load()),
- attr='env',
- ctx=ast.Load()
- ),
- attr='context',
- ctx=ast.Load()
- )],
- keywords=[ast.keyword('lang', utils.compile_strexpr(lang))],
- starargs=None, kwargs=None
- ))],
- starargs=None, kwargs=None
- )
- ))
-
- # qwebcontext.env['ir.qweb']
- compute_self = ast.Subscript(
- value=ast.Attribute(
- value=ast.Name(id=qw, ctx=ast.Load()),
- attr='env',
- ctx=ast.Load()
- ),
- slice=ast.Index(ast.Str('ir.qweb')),
- ctx=ast.Load()
- )
- else:
- compute_self = ast.Name(id='self', ctx=ast.Load())
-
- # ouput.append(self._get_template($tmpl, qwebcontext)(self, qwebcontext_copy))
- content.append(
- self._append(
- ast.Call(
- func=ast.Call(
- func=ast.Attribute(
- value=compute_self,
- attr='_get_template',
- ctx=ast.Load()
- ),
- args=[
- ast.Str(str(tmpl)),
- ast.Name(id=qw, ctx=ast.Load()),
- ],
- keywords=[], starargs=None, kwargs=None
- ),
- args=[ast.Name(id=qw, ctx=ast.Load())],
- keywords=[], starargs=None, kwargs=None
- )
- ))
- return content
-
- def _compile_directive_if(self, el, ast_calls):
+ def _compile_directive_if(self, el, options):
orelse = []
if el.tail and re.search(r'\S', el.tail):
next = el.getnext()
if next is not None and 't-else' in next.attrib:
el.tail = None
- next.attrib.pop('t-else')
- orelse = self._compile_node(next, ast_calls)
+ orelse = self._compile_node(next, dict(options, t_if=True))
next.getparent().remove(next)
return [
ast.If(
- test=utils.compile_expr(el.attrib.pop('t-if')),
- body=self._compile_node(el, ast_calls),
+ test=self._compile_expr(el.attrib.pop('t-if')),
+ body=self._compile_node(el, options),
orelse=orelse
)
]
- def _compile_directive_groups(self, el, ast_calls):
+ def _compile_directive_groups(self, el, options):
return [
ast.If(
test=ast.Call(
@@ -726,106 +903,36 @@ class QWeb(models.AbstractModel):
args=[ast.Str(el.attrib.pop('t-groups'))], keywords=[],
starargs=None, kwargs=None
),
- body=self._compile_node(el, ast_calls),
+ body=self._compile_node(el, options),
orelse=[]
)
]
- def _compile_directive_foreach(self, el, ast_calls):
- expr = utils.compile_expr(el.attrib.pop('t-foreach'))
+ def _compile_directive_foreach(self, el, options):
+ expr = self._compile_expr(el.attrib.pop('t-foreach'))
varname = el.attrib.pop('t-as').replace('.', '_')
-
- # foreach_iterator(qwebcontext, , varname)
- it = ast.Call(
- func=ast.Name(id='foreach_iterator', ctx=ast.Load()),
- args=[ast.Name(id='qwebcontext', ctx=ast.Load()), expr, ast.Str(varname)],
- keywords=[], starargs=None, kwargs=None
- )
- # itertools.repeat(self)
- selfs = ast.Call(
- func=ast.Attribute(
- value=ast.Name(id='itertools', ctx=ast.Load()),
- attr='repeat',
- ctx=ast.Load()
- ), args=[ast.Name(id='self', ctx=ast.Load())], keywords=[],
- starargs=None, kwargs=None
- )
+ values = self._make_name('values')
# create function $foreach
- call = self._call_body(ast_calls, self._compile_node(el, ast_calls), prefix='foreach', lineno=el.sourceline)
+ call = self._call_body(options, self._compile_node(el, options), values=values, prefix='foreach', lineno=el.sourceline)
- # itertools.imap($foreach(), repeat(self), it)
- it = ast.Call(
- func=ast.Attribute(
- value=ast.Name(id='itertools', ctx=ast.Load()),
- attr='imap',
- ctx=ast.Load()
- ), args=[call.func, selfs, it], keywords=[],
- starargs=None, kwargs=None
- )
- # itertools.chain.from_iterable(previous)
- it = ast.Call(
- func=ast.Attribute(
- value=ast.Attribute(
- value=ast.Name(id='itertools', ctx=ast.Load()),
- attr='chain',
- ctx=ast.Load()
- ),
- attr='from_iterable',
- ctx=ast.Load()
- ), args=[it], keywords=[],
- starargs=None, kwargs=None
- )
- return [self._extend(it)]
+ # for x in foreach_iterator(values, $expr, $varname):
+ # $foreach(self, append, values, options)
+ return [ast.For(
+ target=ast.Name(id=values, ctx=ast.Store()),
+ iter=ast.Call(
+ func=ast.Name(id='foreach_iterator', ctx=ast.Load()),
+ args=[ast.Name(id='values', ctx=ast.Load()), expr, ast.Str(varname)],
+ keywords=[], starargs=None, kwargs=None
+ ),
+ body=[ast.Expr(call)],
+ orelse=[]
+ )]
def _compile_tail(self, el):
return el.tail is not None and [self._append(ast.Str(unicodifier(el.tail)))] or []
- def _compile_directive_call_assets(self, el, ast_calls):
- """ This special 't-call' tag can be used in order to aggregate/minify javascript and css assets"""
- if len(el):
- raise "t-call-assets cannot contain children nodes"
-
- # self._get_asset(xmlid, qwebcontext).to_html(css, js, debug, asunc qwebcontext)
- return [
- self._append(ast.Call(
- func=ast.Attribute(
- 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='qwebcontext', ctx=ast.Load()),
- ],
- keywords=[], starargs=None, kwargs=None
- ),
- attr='to_html',
- ctx=ast.Load()
- ),
- args=[],
- keywords=[
- ast.keyword('css', utils.get_attr_bool(el.get('t-css', True))),
- ast.keyword('js', utils.get_attr_bool(el.get('t-js', True))),
- ast.keyword('debug', ast.Call(
- func=ast.Attribute(
- value=ast.Name(id='qwebcontext', ctx=ast.Load()),
- attr='get',
- ctx=ast.Load()
- ),
- args=[ast.Str('debug')],
- keywords=[], starargs=None, kwargs=None
- )),
- ast.keyword('async', utils.get_attr_bool(el.get('async', False))),
- ast.keyword('qwebcontext', ast.Name(id='qwebcontext', ctx=ast.Load())),
- ],
- starargs=None, kwargs=None
- ))
- ]
-
- def _compile_directive_field(self, el, ast_calls):
+ def _compile_directive_field(self, el, options):
""" eg: +1 555 555 8069"""
node_name = el.tag
assert node_name not in ("table", "tbody", "thead", "tfoot", "tr", "td",
@@ -837,21 +944,62 @@ class QWeb(models.AbstractModel):
"t-field must have at least a dot like 'record.field_name'"
expression = el.attrib.pop('t-field')
- options = el.attrib.pop('t-field-options', None)
+ field_options = el.attrib.pop('t-field-options', None)
record, field_name = expression.rsplit('.', 1)
+ default_content = self._make_name('default_content')
- return [
+ content = [
# record
ast.Assign(
targets=[ast.Name(id='record', ctx=ast.Store())],
- value=utils.compile_expr(record)
- ),
- # default_content = u''.join($body_content())
- ast.Assign(
- targets=[ast.Name(id='default_content', ctx=ast.Store())],
- value=self._call_body(ast_calls, self._compile_node(el, ast_calls), prefix='body_content', lineno=el.sourceline)
- ),
- # t_attrs, content = self._get_field(record, field_name, expression, options, qwebcontext)
+ value=self._compile_expr(record)
+ )
+ ]
+
+ body = self._compile_directive_content(el, options)
+ if body:
+ content.extend([
+ # default_content = []
+ ast.Assign(
+ targets=[ast.Name(id=default_content, ctx=ast.Store())],
+ value=ast.List(elts=[], ctx=ast.Load())
+ ),
+ # body_call_content(self, default_content.append, values, options)
+ ast.Expr(self._call_body(options,
+ body=body,
+ append=ast.Attribute(
+ value=ast.Name(id=default_content, ctx=ast.Load()),
+ attr='append',
+ ctx=ast.Load()
+ ),
+ prefix='body_call_content',
+ lineno=el.sourceline
+ )),
+ # default_content = u''.join(default_content)
+ ast.Assign(
+ targets=[ast.Name(id=default_content, ctx=ast.Store())],
+ value=ast.Call(
+ func=ast.Attribute(
+ value=ast.Str(u''),
+ attr='join',
+ ctx=ast.Load()
+ ),
+ args=[
+ ast.Name(id=default_content, ctx=ast.Load())
+ ],
+ keywords=[], starargs=None, kwargs=None
+ )
+ ),
+ ])
+ else:
+ # default_content = u''
+ content.append(ast.Assign(
+ targets=[ast.Name(id=default_content, ctx=ast.Store())],
+ value=ast.Str(u'')
+ ))
+
+ content.extend([
+ # t_attrs, content = self._get_field(record, field_name, expression, field options, template options, values)
ast.Assign(
targets=[ast.Tuple(elts=[ast.Name(id='t_attrs', ctx=ast.Store()), ast.Name(id='content', ctx=ast.Store())], ctx=ast.Store())],
value=ast.Call(
@@ -864,10 +1012,11 @@ class QWeb(models.AbstractModel):
ast.Name(id='record', ctx=ast.Load()),
ast.Str(field_name),
ast.Str(expression),
- ast.Name(id='default_content', ctx=ast.Load()),
+ ast.Name(id=default_content, ctx=ast.Load()),
ast.Str(node_name),
- options and utils.compile_expr(options) or ast.Dict(keys=[], values=[]),
- ast.Name(id='qwebcontext', ctx=ast.Load()),
+ field_options and self._compile_expr(field_options) or ast.Dict(keys=[], values=[]),
+ ast.Name(id='options', ctx=ast.Load()),
+ ast.Name(id='values', ctx=ast.Load()),
],
keywords=[], starargs=None, kwargs=None
),
@@ -875,97 +1024,230 @@ class QWeb(models.AbstractModel):
# if content is not None: display the tag
self._if_content_is_not_Falsy(
- body=self._compile_tag(el, [self._append(ast.Name(id='content', ctx=ast.Load()))], True),
+ body=self._compile_tag(el, [self._append(ast.Name(id='content', ctx=ast.Load()))], options, True),
orelse=[]
)
+ ])
+ return content
+
+ def _compile_directive_call(self, el, options):
+ """
+ :param etree._Element el:
+ :param list ast call:
+ :return: new body
+ :rtype: list(ast.AST)
+ """
+ tmpl = el.attrib.pop('t-call')
+ _values = self._make_name('values_copy')
+ call_options = el.attrib.pop('t-call-options', None)
+
+ _values = self._make_name('values_copy')
+
+ content = [
+ # values_copy = values.copy()
+ ast.Assign(
+ targets=[ast.Name(id=_values, ctx=ast.Store())],
+ value=ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='values', ctx=ast.Load()),
+ attr='copy',
+ ctx=ast.Load()
+ ),
+ args=[], keywords=[],
+ starargs=None, kwargs=None
+ )
+ )
]
- # assume cache will be invalidated by third party on write to ir.ui.view
- def _get_template_cache_keys(self):
- """ Return the list of context keys to use for caching ``_get_template``. """
- return ['lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations']
+ body = self._compile_directive_content(el, options)
+ if body:
- # apply ormcache_context decorator unless in dev mode...
- @openerp.tools.conditional(
- 'xml' not in openerp.tools.config['dev_mode'],
- openerp.tools.ormcache('xmlid', 'tuple(map(self._context.get, self._get_template_cache_keys()))'),
- )
- def _get_template(self, xmlid, qwebcontext):
- element = self.get_template(xmlid, qwebcontext.loader, origin_template=qwebcontext.get('__caller__') or qwebcontext.get('__stack__', [xmlid])[0])
- element.attrib.pop("name", False)
- return self.compile(element)
-
- def _get_asset(self, xmlid, qwebcontext):
- files, remains = self._get_asset_content(xmlid, qwebcontext)
- return AssetsBundle(xmlid, files, remains, env=qwebcontext.env)
-
- @openerp.tools.ormcache('xmlid', 'qwebcontext.env.lang')
- def _get_asset_content(self, xmlid, qwebcontext):
- context = dict(self.env.context if qwebcontext is None else qwebcontext.env.context,
- inherit_branding=False, inherit_branding_auto=False,
- edit_translations=False, translatable=False,
- rendering_bundle=True)
-
- env = self.env(context=context)
- if qwebcontext is None:
- qwebcontext = QWebContext(env, {})
+ # call_content = []
+ content.append(
+ ast.Assign(
+ targets=[ast.Name(id='call_content', ctx=ast.Store())],
+ value=ast.List(elts=[], ctx=ast.Load())
+ )
+ )
+ # body_call_content(self, call_content.append, values, options)
+ content.append(
+ ast.Expr(self._call_body(options,
+ body=body,
+ append=ast.Attribute(
+ value=ast.Name(id='call_content', ctx=ast.Load()),
+ attr='append',
+ ctx=ast.Load()
+ ),
+ values=_values,
+ prefix='body_call_content',
+ lineno=el.sourceline
+ ))
+ )
+ # values_copy[0] = call_content
+ content.append(
+ ast.Assign(
+ targets=[ast.Subscript(
+ value=ast.Name(id=_values, ctx=ast.Load()),
+ slice=ast.Index(ast.Num(0)),
+ ctx=ast.Store()
+ )],
+ value=ast.Name(id='call_content', ctx=ast.Load())
+ )
+ )
else:
- qwebcontext = qwebcontext.copy()
- qwebcontext.env = env
+ # values_copy[0] = []
+ content.append(
+ ast.Assign(
+ targets=[ast.Subscript(
+ value=ast.Name(id=_values, ctx=ast.Load()),
+ slice=ast.Index(ast.Num(0)),
+ ctx=ast.Store()
+ )],
+ value=ast.List(elts=[], ctx=ast.Load())
+ )
+ )
- template = env['ir.qweb']._get_template(xmlid, qwebcontext)(qwebcontext)
+ if call_options:
+ name_options = self._make_name('options')
+ content.extend([
+ # options_ = options.copy()
+ ast.Assign(
+ targets=[ast.Name(id=name_options, ctx=ast.Store())],
+ value=ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='options', ctx=ast.Load()),
+ attr='copy',
+ ctx=ast.Load()
+ ),
+ args=[], keywords=[], starargs=None, kwargs=None
+ )
+ ),
+ # options_.update(template options)
+ ast.Expr(ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id=name_options, ctx=ast.Load()),
+ attr='update',
+ ctx=ast.Load()
+ ),
+ args=[self._compile_expr(call_options)],
+ keywords=[], starargs=None, kwargs=None
+ ))
+ ])
+ else:
+ name_options = 'options'
- files = []
- remains = []
- for el in html.fragments_fromstring(template):
- if isinstance(el, basestring):
- remains.append(el)
- elif isinstance(el, html.HtmlElement):
- href = el.get('href', '')
- atype = el.get('type')
- media = el.get('media')
+ # self.compile($tmpl, options)(self, append, values_copy)
+ content.append(
+ ast.Expr(ast.Call(
+ func=ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='self', ctx=ast.Load()),
+ attr='compile',
+ ctx=ast.Load()
+ ),
+ args=[
+ ast.Str(str(tmpl)),
+ ast.Name(id=name_options, ctx=ast.Load()),
+ ],
+ keywords=[], starargs=None, kwargs=None
+ ),
+ args=[
+ ast.Name(id='self', ctx=ast.Load()),
+ ast.Name(id='append', ctx=ast.Load()),
+ ast.Name(id=_values, ctx=ast.Load())
+ ],
+ keywords=[], starargs=None, kwargs=None
+ ))
+ )
+ return content
- can_aggregate = not urlparse(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 href.endswith('.sass'):
- atype = 'text/sass'
- elif href.endswith('.less'):
- atype = 'text/less'
- if atype not in ('text/less', 'text/sass'):
- atype = 'text/css'
- files.append({'atype': atype, 'url': href, 'content': el.text, 'media': media})
- elif el.tag == 'script':
- atype = 'text/javascript'
- files.append({'atype': atype, 'url': el.get('src', ''), 'content': el.text, 'media': media})
- else:
- remains.append(html.tostring(el))
- else:
- try:
- remains.append(html.tostring(el))
- except Exception:
- # notYETimplementederror
- raise NotImplementedError
+ # method called by computing code
- return (files, remains)
+ def _get_dynamic_att(self, tagName, atts, options, values):
+ if isinstance(atts, OrderedDict):
+ return atts
+ if isinstance(atts, (list, tuple)) and not isinstance(atts[0], (list, tuple)):
+ atts = [atts]
+ if isinstance(atts, (list, tuple)):
+ atts = OrderedDict(atts)
+ return atts
- def _get_field(self, record, field_name, expression, default_content, tagName, options, qwebcontext):
- field = record._fields[field_name]
+ def _get_field(self, record, field_name, expression, default_content, tagName, field_options, options, values):
+ return (attributes, escape(unicodifier(getattr(record, field_name, default_content))))
- field_type = options.get('widget', field.type)
+ # compile expression
- inherit_branding = self._context.get('inherit_branding', self._context.get('inherit_branding_auto') and record.check_access_rights('write', False))
- translate = self._context.get('edit_translations') and self._context.get('translatable') and getattr(field, 'translate', False)
+ def _compile_strexpr(self, expr):
+ # ensure result is unicode
+ return ast.Call(
+ func=ast.Name(id='unicodifier', ctx=ast.Load()),
+ args=[self._compile_expr(expr)], keywords=[],
+ starargs=None, kwargs=None
+ )
- options['tagName'] = tagName
- options['type'] = field_type
- options['expression'] = expression
- options['default_content'] = default_content
- options['inherit_branding'] = inherit_branding
- options['translate'] = translate
+ def _compile_expr0(self, expr):
+ if expr == "0":
+ # values.get(0) and u''.join(values[0])
+ return ast.BoolOp(
+ op=ast.And(),
+ values=[
+ ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='values', ctx=ast.Load()),
+ attr='get',
+ ctx=ast.Load()
+ ),
+ args=[ast.Num(0)], keywords=[],
+ starargs=None, kwargs=None
+ ),
+ ast.Call(
+ func=ast.Attribute(
+ value=ast.Str(u''),
+ attr='join',
+ ctx=ast.Load()
+ ),
+ args=[
+ self._values_var(ast.Num(0), ctx=ast.Load())
+ ],
+ keywords=[], starargs=None, kwargs=None
+ )
+ ]
+ )
+ return self._compile_expr(expr)
- converter = self.get_field_for(field_type, qwebcontext.cr, qwebcontext.uid, qwebcontext.context)
+ def _compile_format(self, f):
+ """ Parses the provided format string and compiles it to a single
+ expression ast, uses string concatenation via "+"
+ """
+ elts = []
+ base_idx = 0
+ for m in _FORMAT_REGEX.finditer(f):
+ literal = f[base_idx:m.start()]
+ if literal:
+ elts.append(ast.Str(literal if isinstance(literal, unicode) else literal.decode('utf-8')))
- content = converter.record_to_html(record, field_name, options, qwebcontext)
- attributes = converter.attributes(record, field_name, options, qwebcontext)
+ expr = m.group(1) or m.group(2)
+ elts.append(self._compile_strexpr(expr))
+ base_idx = m.end()
+ # string past last regex match
+ literal = f[base_idx:]
+ if literal:
+ elts.append(ast.Str(literal if isinstance(literal, unicode) else literal.decode('utf-8')))
- return (attributes, content or default_content or (u"" if inherit_branding or translate else None))
+ return reduce(lambda acc, it: ast.BinOp(
+ left=acc,
+ op=ast.Add(),
+ right=it
+ ), elts)
+
+ def _compile_expr(self, expr):
+ """ Compiles a purported Python expression to ast, and alter its variable
+ references to access values data instead
+ Can be overwrited to use a safe eval method
+ It's unsafe, overwrited this method to use a safe code check
+ """
+ # string must be stripped otherwise whitespace before the start for
+ # formatting purpose are going to break parse/compile
+ st = ast.parse(expr.strip(), mode='eval')
+ # ast.Expression().body -> expr
+ return Contextifier().visit(st).body
diff --git a/openerp/addons/base/ir/ir_qweb/utils.py b/openerp/addons/base/ir/ir_qweb/utils.py
deleted file mode 100644
index 307ea2353ee..00000000000
--- a/openerp/addons/base/ir/ir_qweb/utils.py
+++ /dev/null
@@ -1,316 +0,0 @@
-# -*- coding: utf-8 -*-
-
-import ast
-import itertools
-from collections import Sized, Mapping, defaultdict
-from openerp.tools import safe_eval
-from odoo.exceptions import QWebException
-import sys
-import re
-
-
-def base_module():
- """ module base supporting qweb template functions (provides basic
- imports and utilities)
- Currently provides:
- * collections
- * itertools
- * openerp.tools.html_escape as escape
- * unicodifier (empty string for a None or False, otherwise unicode string)
- """
-
- return ast.parse("""import __builtin__
-from collections import OrderedDict
-import itertools
-from openerp.addons.base.ir.ir_qweb.utils import unicodifier, foreach_iterator
-from openerp.tools import safe_eval, html_escape as escape
-""")
-
-
-def unicodifier(val):
- if val is None or val is False:
- return u''
- if isinstance(val, str):
- return val.decode('utf-8')
- return unicode(val)
-
-def foreach_iterator(base_ctx, enum, name):
- ctx = base_ctx.copy()
- if not enum:
- return
- if isinstance(enum, int):
- enum = xrange(enum)
- size = None
- if isinstance(enum, Sized):
- ctx["%s_size" % name] = size = len(enum)
- if isinstance(enum, Mapping):
- enum = enum.iteritems()
- else:
- enum = itertools.izip(*itertools.tee(enum))
- value_key = '%s_value' % name
- index_key = '%s_index' % name
- first_key = '%s_first' % name
- last_key = '%s_last' % name
- parity_key = '%s_parity' % name
- even_key = '%s_even' % name
- odd_key = '%s_odd' % name
- for index, (item, value) in enumerate(enum):
- ctx[name] = item
- ctx[value_key] = value
- ctx[index_key] = index
- ctx[first_key] = index == 0
- if size is not None:
- ctx[last_key] = index + 1 == size
- if index % 2:
- ctx[parity_key] = 'odd'
- ctx[even_key] = False
- ctx[odd_key] = True
- else:
- ctx[parity_key] = 'even'
- ctx[even_key] = True
- ctx[odd_key] = False
- yield ctx
- # copy changed items back into source context (?)
- # FIXME: maybe QWebContext could provide a ChainMap-style clone?
- for k in base_ctx.keys():
- base_ctx[k] = ctx[k]
-
-def base_fn_def(body, name='fn', lineno=None):
- """ Generates a "qweb function" definition:
- * takes a single ``qwebcontext`` parameter
- * defines a local ``output`` list
- * returns ``u''.join(output)``
- The provided body should be a list of ast nodes, they will be injected
- between the initialization of ``body`` and its concatenation
- """
-
- body = [ast.Assign([ast.Name(id='output', ctx=ast.Store())], ast.List(elts=[], ctx=ast.Load()))] + \
- body + \
- [ast.Return(ast.Call(
- func=ast.Attribute(value=ast.Str(u''), attr='join', ctx=ast.Load()),
- args=[ast.Name(id='output', ctx=ast.Load())], keywords=[],
- starargs=None, kwargs=None
- ))]
-
- fn = ast.FunctionDef(
- name=name,
- args=ast.arguments(args=[
- ast.Name('self', ast.Param()),
- ast.Name('qwebcontext', ast.Param())
- ], defaults=[], vararg=None, kwarg=None),
- body=body,
- decorator_list=[])
- if lineno is not None:
- fn.lineno = lineno
- return fn
-
-def get_attr_bool(attr, default=False):
- if attr:
- if attr is True:
- return ast.Name(id='True', ctx=ast.Load())
- attr = attr.lower()
- if attr in ('false', '0'):
- return ast.Name(id='False', ctx=ast.Load())
- elif attr in ('true', '1'):
- return ast.Name(id='True', ctx=ast.Load())
- return ast.Name(id=str(attr if attr is False else default), ctx=ast.Load())
-
-
-class Contextifier(ast.NodeTransformer):
- """ For user-provided template expressions, replaces any ``name`` by
- :sampe:`qwebcontext.get('{name}')` so all variable accesses are
- performed on the qwebcontext rather than in the "native" scope
- """
-
- # some people apparently put lambdas in template expressions. Turns out
- # the AST -> bytecode compiler does *not* appreciate parameters of lambdas
- # being converted from names to subscript expressions, and most likely the
- # reference to those parameters inside the lambda's body should probably
- # remain as-is. Because we're transforming an AST, the structure should
- # be lexical, so just store a set of "safe" parameter names and recurse
- # through the lambda using a new NodeTransformer
- def __init__(self, params=()):
- super(Contextifier, self).__init__()
- self._safe_names = tuple(params)
-
- def visit_Name(self, node):
- if node.id in self._safe_names:
- return node
-
- return ast.copy_location(
- # qwebcontext.get(name, getattr(__builtin__, name, None))
- ast.Call(
- func=ast.Attribute(
- value=ast.Name(id='qwebcontext', ctx=ast.Load()),
- attr='get',
- ctx=ast.Load()
- ),
- args=[
- ast.Str(node.id),
- ast.Call(
- func=ast.Name(id='getattr', ctx=ast.Load()),
- args=[
- ast.Name(id='__builtin__', ctx=ast.Load()),
- ast.Str(node.id),
- ast.Name(id='None', ctx=ast.Load()),
- ], keywords=[],
- starargs=None, kwargs=None
- )
- ], keywords=[],
- starargs=None, kwargs=None
- ),
- node
- )
-
- def visit_Lambda(self, node):
- args = node.args
- # assume we don't have any tuple parameter, just names
- names = [arg.id for arg in args.args]
- if args.vararg: names.append(args.vararg)
- if args.kwarg: names.append(args.kwarg)
- # remap defaults in case there's any
- return ast.copy_location(ast.Lambda(
- args=ast.arguments(
- args=args.args,
- defaults=map(self.visit, args.defaults),
- vararg=args.vararg,
- kwarg=args.kwarg,
- ),
- body=Contextifier(self._safe_names + tuple(names)).visit(node.body)
- ), node)
-
- # "lambda problem" also exists with comprehensions
- def _visit_comp(self, node):
- # CompExp(?, comprehension* generators)
- # comprehension = (expr target, expr iter, expr* ifs)
-
- # collect names in generators.target
- names = tuple(
- node.id
- for gen in node.generators
- for node in ast.walk(gen.target)
- if isinstance(node, ast.Name)
- )
- transformer = Contextifier(self._safe_names + names)
- # copy node
- newnode = ast.copy_location(type(node)(), node)
- # then visit the comp ignoring those names, transformation is
- # probably expensive but shouldn't be many comprehensions
- for field, value in ast.iter_fields(node):
- # map transformation of comprehensions
- if isinstance(value, list):
- setattr(newnode, field, map(transformer.visit, value))
- else: # set transformation of key/value/expr fields
- setattr(newnode, field, transformer.visit(value))
- return newnode
- visit_GeneratorExp = visit_ListComp = visit_SetComp = visit_DictComp = _visit_comp
-
-
-def compile_strexpr(expr):
- # ensure result is unicode
- return ast.Call(
- func=ast.Name(id='unicodifier', ctx=ast.Load()),
- args=[compile_expr(expr)], keywords=[],
- starargs=None, kwargs=None
- )
-
-def compile_expr0(expr):
- if expr == "0":
- return ast.Call(
- func=ast.Attribute(
- value=ast.Name(id='qwebcontext', ctx=ast.Load()),
- attr='get',
- ctx=ast.Load()
- ),
- args=[ast.Num(0), ast.Str('')], keywords=[],
- starargs=None, kwargs=None
- )
- return compile_expr(expr)
-
-def compile_expr(expr):
- """ Compiles a purported Python expression to ast, verifies that it's safe
- (according to safe_eval's semantics) and alter its variable references to
- access qwebcontext data instead
- """
- # string must be stripped otherwise whitespace before the start for
- # formatting purpose are going to break parse/compile
- st = ast.parse(expr.strip(), mode='eval')
- safe_eval.assert_valid_codeobj(
- safe_eval._SAFE_OPCODES,
- compile(st, '<>', 'eval'), # could be expr, but eval *should* be fine
- expr
- )
-
- # ast.Expression().body -> expr
- return Contextifier().visit(st).body
-
-def compile_format(f):
- """ Parses the provided format string and compiles it to a single
- expression ast, uses string concatenation via "+"
- """
- elts = []
- base_idx = 0
- for m in _FORMAT_REGEX.finditer(f):
- literal = f[base_idx:m.start()]
- if literal:
- elts.append(ast.Str(literal if isinstance(literal, unicode) else literal.decode('utf-8')))
-
- expr = m.group(1) or m.group(2)
- elts.append(compile_strexpr(expr))
- base_idx = m.end()
- # string past last regex match
- literal = f[base_idx:]
- if literal:
- elts.append(ast.Str(literal if isinstance(literal, unicode) else literal.decode('utf-8')))
-
- return reduce(lambda acc, it: ast.BinOp(
- left=acc,
- op=ast.Add(),
- right=it
- ), elts)
-
-
-_FORMAT_REGEX = re.compile(
- '(?:'
- # ruby-style pattern
- '#\{(.+?)\}'
- ')|(?:'
- # jinja-style pattern
- '\{\{(.+?)\}\}'
- ')')
-
-
-class QWebTemplateNotFound(QWebException):
- pass
-
-
-class QWebContext(dict):
- def __init__(self, env, data, loader=None, templates=None):
- self.env = env
- self.loader = loader or (lambda name: env['ir.ui.view'].read_template(name))
- super(QWebContext, self).__init__(data)
- self['defined'] = lambda key: key in self
- self.templates = templates or {}
-
- # deprecated, use 'env' instead
- cr = property(lambda self: self.env.cr)
- uid = property(lambda self: self.env.uid)
- context = property(lambda self: self.env.context)
-
- def safe_eval(self, expr):
- locals_dict = defaultdict(lambda: None)
- locals_dict.update(self)
- locals_dict.pop('cr', None)
- locals_dict.pop('loader', None)
- return safe_eval.safe_eval(expr, None, locals_dict, nocopy=True, locals_builtins=True)
-
- def copy(self):
- """ Clone the current context, conserving all data and metadata
- (loader, template cache, ...)
- """
- return QWebContext(self.env, dict.copy(self),
- loader=self.loader,
- templates=self.templates)
-
- def __copy__(self):
- return self.copy()
diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py
index 5d2813efde3..def17b06dbc 100644
--- a/openerp/addons/base/ir/ir_ui_view.py
+++ b/openerp/addons/base/ir/ir_ui_view.py
@@ -303,7 +303,6 @@ actual arch.
def _check_groups(self):
for view in self:
if view.type == 'qweb' and view.groups_id:
- print view.key, view.groups_id
raise ValidationError(_("Qweb view can not have 'Groups' define on the record. Use 'groups' atttributes inside the view definition"))
_sql_constraints = [
@@ -1026,20 +1025,11 @@ actual arch.
time=time,
datetime=datetime,
relativedelta=relativedelta,
+ xmlid=self.key,
)
qcontext.update(values or {})
- # 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.
- def get_modules_order():
- if request:
- from odoo.addons.web.controllers.main import module_boot
- return json.dumps(module_boot())
- return '[]'
- qcontext['get_modules_order'] = get_modules_order
-
- return self.env[engine].render(self.id, qcontext, loader=self.read_template)
+ return self.env[engine].render(self.id, qcontext)
#------------------------------------------------------
# Misc
diff --git a/openerp/exceptions.py b/openerp/exceptions.py
index 8d0bb779d3e..d83e350c624 100644
--- a/openerp/exceptions.py
+++ b/openerp/exceptions.py
@@ -12,7 +12,6 @@ If you consider introducing new exceptions, check out the test_exceptions addon.
import logging
from inspect import currentframe
-from lxml import etree
from tools.func import frame_codeinfo
_logger = logging.getLogger(__name__)
@@ -92,12 +91,6 @@ class DeferredException(Exception):
self.message = msg
self.traceback = tb
-class QWebException(Exception):
- def __init__(self, message, **kw):
- super(QWebException, self).__init__(message)
- self.qweb = dict(kw)
- def pretty_xml(self):
- if 'node' not in self.qweb:
- return ''
- return etree.tostring(self.qweb['node'], pretty_print=True)
+class QWebException(Exception):
+ pass