From b3302398bc79f107588ff755666bf74a2ef4e11a Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Wed, 25 May 2016 11:40:51 +0200 Subject: [PATCH] [IMP] ir.qweb: extract and create independent qweb class from ir.qweb The goal is to make the code more readable, more easily be optimized, allowing to share the library "QWEB" (python and js) separately Odoo. --- addons/web_editor/models/ir_qweb.py | 15 +- addons/web_editor/models/ir_ui_view.py | 3 +- addons/website/models/ir_qweb.py | 29 +- openerp/addons/base/ir/ir_qweb/__init__.py | 3 +- openerp/addons/base/ir/ir_qweb/ir_qweb.py | 39 +- openerp/addons/base/ir/ir_qweb/qweb.py | 1432 ++++++++++++-------- openerp/addons/base/ir/ir_qweb/utils.py | 316 ----- openerp/addons/base/ir/ir_ui_view.py | 14 +- openerp/exceptions.py | 11 +- 9 files changed, 909 insertions(+), 953 deletions(-) delete mode 100644 openerp/addons/base/ir/ir_qweb/utils.py 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'' % 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