[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:
@@ -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
|
||||
#------------------------------------------------------
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user