This commit handles 2 things: 1) the merging of the `py_utils.context()` into the evaluation context returned by `BasicModel._getEvalContext` to access the same time-related keys, 2) the addition of 2 new keys into the `py_utils.context` (thus transmitted to the basic model): `today` (an alias for the already present `current_date`) and `now` which represents the current date and time value. Task 2269697
183 lines
5.3 KiB
Python
183 lines
5.3 KiB
Python
""" View validation code (using assertions, not the RNG schema). """
|
|
|
|
import ast
|
|
import collections
|
|
import logging
|
|
import os
|
|
import re
|
|
|
|
from lxml import etree
|
|
from odoo import tools
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
_validators = collections.defaultdict(list)
|
|
_relaxng_cache = {}
|
|
|
|
READONLY = re.compile(r"\breadonly\b")
|
|
|
|
|
|
def _get_attrs_symbols():
|
|
""" Return a set of predefined symbols for evaluating attrs. """
|
|
return {
|
|
'True', 'False', 'None', # those are identifiers in Python 2.7
|
|
'self',
|
|
'id',
|
|
'uid',
|
|
'context',
|
|
'context_today',
|
|
'active_id',
|
|
'active_ids',
|
|
'allowed_company_ids',
|
|
'current_company_id',
|
|
'active_model',
|
|
'time',
|
|
'datetime',
|
|
'relativedelta',
|
|
'current_date',
|
|
'today',
|
|
'now',
|
|
'abs',
|
|
'len',
|
|
'bool',
|
|
'float',
|
|
'str',
|
|
'unicode',
|
|
}
|
|
|
|
|
|
def get_variable_names(expr):
|
|
""" Return the subexpressions of the kind "VARNAME(.ATTNAME)*" in the given
|
|
string or AST node.
|
|
"""
|
|
IGNORED = _get_attrs_symbols()
|
|
names = set()
|
|
|
|
def get_name_seq(node):
|
|
if isinstance(node, ast.Name):
|
|
return [node.id]
|
|
elif isinstance(node, ast.Attribute):
|
|
left = get_name_seq(node.value)
|
|
return left and left + [node.attr]
|
|
|
|
def process(node):
|
|
seq = get_name_seq(node)
|
|
if seq and seq[0] not in IGNORED:
|
|
names.add('.'.join(seq))
|
|
else:
|
|
for child in ast.iter_child_nodes(node):
|
|
process(child)
|
|
|
|
if isinstance(expr, str):
|
|
expr = ast.parse(expr.strip(), mode='eval').body
|
|
process(expr)
|
|
|
|
return names
|
|
|
|
|
|
def get_dict_asts(expr):
|
|
""" Check that the given string or AST node represents a dict expression
|
|
where all keys are string literals, and return it as a dict mapping string
|
|
keys to the AST of values.
|
|
"""
|
|
if isinstance(expr, str):
|
|
expr = ast.parse(expr.strip(), mode='eval').body
|
|
|
|
if not isinstance(expr, ast.Dict):
|
|
raise ValueError("Non-dict expression")
|
|
if not all(isinstance(key, ast.Str) for key in expr.keys):
|
|
raise ValueError("Non-string literal dict key")
|
|
return {key.s: val for key, val in zip(expr.keys, expr.values)}
|
|
|
|
|
|
def _check(condition, explanation):
|
|
if not condition:
|
|
raise ValueError("Expression is not a valid domain: %s" % explanation)
|
|
|
|
|
|
def get_domain_identifiers(expr):
|
|
""" Check that the given string or AST node represents a domain expression,
|
|
and return a pair of sets ``(fields, vars)`` where ``fields`` are the field
|
|
names on the left-hand side of conditions, and ``vars`` are the variable
|
|
names on the right-hand side of conditions.
|
|
"""
|
|
if not expr: # case of expr=""
|
|
return (set(), set())
|
|
if isinstance(expr, str):
|
|
expr = ast.parse(expr.strip(), mode='eval').body
|
|
|
|
fnames = set()
|
|
vnames = set()
|
|
|
|
if isinstance(expr, ast.List):
|
|
for elem in expr.elts:
|
|
if isinstance(elem, ast.Str):
|
|
# note: this doesn't check the and/or structure
|
|
_check(elem.s in ('&', '|', '!'),
|
|
f"logical operators should be '&', '|', or '!', found {elem.s!r}")
|
|
continue
|
|
|
|
if not isinstance(elem, (ast.List, ast.Tuple)):
|
|
continue
|
|
|
|
_check(len(elem.elts) == 3,
|
|
f"segments should have 3 elements, found {len(elem.elts)}")
|
|
lhs, operator, rhs = elem.elts
|
|
_check(isinstance(operator, ast.Str),
|
|
f"operator should be a string, found {type(operator).__name__}")
|
|
if isinstance(lhs, ast.Str):
|
|
fnames.add(lhs.s)
|
|
|
|
vnames.update(get_variable_names(expr))
|
|
|
|
return (fnames, vnames)
|
|
|
|
|
|
def valid_view(arch, **kwargs):
|
|
for pred in _validators[arch.tag]:
|
|
check = pred(arch, **kwargs)
|
|
if not check:
|
|
_logger.error("Invalid XML: %s", pred.__doc__)
|
|
return False
|
|
if check == "Warning":
|
|
_logger.warning("Invalid XML: %s", pred.__doc__)
|
|
return "Warning"
|
|
return True
|
|
|
|
|
|
def validate(*view_types):
|
|
""" Registers a view-validation function for the specific view types
|
|
"""
|
|
def decorator(fn):
|
|
for arch in view_types:
|
|
_validators[arch].append(fn)
|
|
return fn
|
|
return decorator
|
|
|
|
|
|
def relaxng(view_type):
|
|
""" Return a validator for the given view type, or None. """
|
|
if view_type not in _relaxng_cache:
|
|
with tools.file_open(os.path.join('base', 'rng', '%s_view.rng' % view_type)) as frng:
|
|
try:
|
|
relaxng_doc = etree.parse(frng)
|
|
_relaxng_cache[view_type] = etree.RelaxNG(relaxng_doc)
|
|
except Exception:
|
|
_logger.exception('Failed to load RelaxNG XML schema for views validation')
|
|
_relaxng_cache[view_type] = None
|
|
return _relaxng_cache[view_type]
|
|
|
|
|
|
@validate('calendar', 'graph', 'pivot', 'search', 'tree', 'activity')
|
|
def schema_valid(arch, **kwargs):
|
|
""" Get RNG validator and validate RNG file."""
|
|
validator = relaxng(arch.tag)
|
|
if validator and not validator.validate(arch):
|
|
result = True
|
|
for error in validator.error_log:
|
|
_logger.error(tools.ustr(error))
|
|
result = False
|
|
return result
|
|
return True
|