[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.
This commit is contained in:
Christophe Matthieu
2016-06-14 13:12:14 +02:00
parent 4530f65211
commit b3302398bc
9 changed files with 909 additions and 953 deletions
+8 -7
View File
@@ -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'<div name="%s" data-oe-type="snippet" data-oe-thumbnail="%s">' % (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'</div>'))]
return [self._append(ast.Str(div))] + self._compile_node(el, options) + [self._append(ast.Str(u'</div>'))]
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
#------------------------------------------------------
+2 -1
View File
@@ -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:
+15 -14
View File
@@ -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
+1 -2
View File
@@ -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
+22 -17
View File
@@ -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())
File diff suppressed because it is too large Load Diff
-316
View File
@@ -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()
+2 -12
View File
@@ -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
+2 -9
View File
@@ -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