[FIX] ir.rule,expression: domain expressions must be normalized before they can be safely combined

This adds a normalize_domain() method to osv.expression, which ir.rule calls
before combining expressions due to multiple rules or groups being applicable
to a certain user.
YAML tests are also added with a trivial unit test of normalize_domain() and
some additional tests for ir_rule in order to verify that unnormalized domains
are properly normalized before being combined by ir.rule.

bzr revid: odo@openerp.com-20110324111757-uwuoqvm3lxkipr08
This commit is contained in:
Olivier Dony 2011-03-24 12:17:57 +01:00
parent 6a46a15185
commit 444df6affa
4 changed files with 96 additions and 15 deletions

View File

@ -19,7 +19,7 @@
#
##############################################################################
from osv import fields,osv
from osv import fields, osv, expression
import time
from operator import itemgetter
from functools import partial
@ -37,7 +37,7 @@ class ir_rule(osv.osv):
if rule.domain_force:
eval_user_data = {'user': self.pool.get('res.users').browse(cr, 1, uid),
'time':time}
res[rule.id] = eval(rule.domain_force, eval_user_data)
res[rule.id] = expression.expression.normalize_domain(eval(rule.domain_force, eval_user_data))
else:
res[rule.id] = []
return res
@ -91,7 +91,7 @@ class ir_rule(osv.osv):
dom += rule.domain
count += 1
if count:
return ['&'] * (count-1) + dom
return [expression.AND_OPERATOR] * (count-1) + dom
return []
@tools.cache()
@ -127,9 +127,9 @@ class ir_rule(osv.osv):
group_domains += group_domain
count += 1
if count and global_domain:
return ['&'] + global_domain + ['|'] * (count-1) + group_domains
return [expression.AND_OPERATOR] + global_domain + [expression.OR_OPERATOR] * (count-1) + group_domains
if count:
return ['|'] * (count-1) + group_domains
return [expression.OR_OPERATOR] * (count-1) + group_domains
return global_domain
return []

View File

@ -132,6 +132,47 @@
!python {model: res.partner }: |
ids = self.search(cr, ref('base.user_demo'), [])
assert ids, "Demo user should see some partner."
-
Modify the ir_rule for employee so that demo is not
allowed to see partners anymore. We use a domain
with implicit AND operator for later tests on normalization.
-
!record {model: ir.rule, id: test_rule2}:
domain_force: "[('id','=',False),('name','=',False)]"
-
Check that demo user does not see partners anymore.
-
!python {model: res.partner }: |
ids = self.search(cr, ref('base.user_demo'), [])
assert (not ids), "Demo user should not see any partner anymore"
-
Create a new group with demo user in it, and a complex rule that should
re-allow demo to see partners, because he belongs to both groups.
-
!record {model: res.groups, id: test_group}:
name: Test Group
users:
- base.user_demo
-
Add the rule to the new group, with a domain containing an implicit AND operator,
which is more tricky because it will have to be normalized before combining it.
-
!record {model: ir.rule, id: test_rule3}:
model_id: base.model_res_partner
domain_force: "[('name', '!=', False),('id', '!=', False)]"
name: test_rule4
groups:
- test_group
perm_unlink: 1
perm_write: 1
perm_read: 1
perm_create: 1
-
Read the partners again as demo user, which should give results again.
-
!python {model: res.partner }: |
ids = self.search(cr, ref('base.user_demo'), [])
assert ids, "Demo user should see some partners again, due to combined rules."
-
Delete global domains (to combine only group domains).
-
@ -140,7 +181,7 @@
assert ids, "Demo user should see some partner."
self.unlink(cr, uid, ids)
-
Read as demo user the partners (three 1=1 domains, no global domain).
Read as demo user the partners (several group domains, no global domain).
-
!python {model: res.partner }: |
ids = self.search(cr, ref('base.user_demo'), [])

View File

@ -110,4 +110,16 @@
res_ids = self.search(cr, uid, [('company_id.partner_id', 'not in', [])])
res_ids.sort()
assert res_ids == all_ids, "Searching against empty set failed, returns %r" % res_ids
-
Verify that normalize_domain() works.
-
!python {model: res.partner}: |
from osv.expression import expression
norm_domain = domain = ['&',(1,'=',1),('a','=','b')]
assert norm_domain == expression.normalize_domain(domain), "Normalized domains should be left untouched"
domain = [('x','in',['y','z']),('a.v','=','e'),'|','|',('a','=','b'),'!',('c','>','d'),('e','!=','f'),('g','=','h')]
norm_domain = ['&','&','&'] + domain
assert norm_domain == expression.normalize_domain(domain), "Non-normalized domains should be properly normalized"

View File

@ -23,6 +23,9 @@
from openerp.tools import flatten, reverse_enumerate
import fields
NOT_OPERATOR = '!'
OR_OPERATOR = '|'
AND_OPERATOR = '&'
class expression(object):
"""
@ -32,10 +35,12 @@ class expression(object):
For more info: http://christophe-simonis-at-tiny.blogspot.com/2008/08/new-new-domain-notation.html
"""
def _is_operator(self, element):
return isinstance(element, (str, unicode)) and element in ['&', '|', '!']
@classmethod
def _is_operator(cls, element):
return isinstance(element, (str, unicode)) and element in [AND_OPERATOR, OR_OPERATOR, NOT_OPERATOR]
def _is_leaf(self, element, internal=False):
@classmethod
def _is_leaf(cls, element, internal=False):
OPS = ('=', '!=', '<>', '<=', '<', '>', '>=', '=?', '=like', '=ilike', 'like', 'not like', 'ilike', 'not ilike', 'in', 'not in', 'child_of')
INTERNAL_OPS = OPS + ('inselect',)
return (isinstance(element, tuple) or isinstance(element, list)) \
@ -43,6 +48,30 @@ class expression(object):
and (((not internal) and element[1] in OPS) \
or (internal and element[1] in INTERNAL_OPS))
@classmethod
def normalize_domain(cls, domain_expr):
"""Returns a normalized version of ``domain_expr``, where all implicit '&' operators
have been made explicit. One property of normalized domain expressions is that they
can be easily combined together as if they were single domain components.
"""
assert isinstance(domain_expr, (list, tuple)), "Domain to normalize must have a 'domain' form: a list or tuple of domain components"
missing_operators = -1
for item in domain_expr:
if cls._is_operator(item):
if item != NOT_OPERATOR:
missing_operators -= 1
else:
missing_operators += 1
return [AND_OPERATOR] * missing_operators + domain_expr
def normalize(self):
"""Make this expression normalized, i.e. change it so that all implicit '&'
operator become explicit. If the expression had already been parsed,
there is no need to do it again.
"""
self.__exp = expression.normalize_domain(self.__exp)
return self
def __execute_recursive_in(self, cr, s, f, w, ids, op, type):
# todo: merge into parent query as sub-query
res = []
@ -92,8 +121,8 @@ class expression(object):
doms = []
for o in table.browse(cr, uid, ids, context=context):
if doms:
doms.insert(0, '|')
doms += ['&', ('parent_left', '<', o.parent_right), ('parent_left', '>=', o.parent_left)]
doms.insert(0, OR_OPERATOR)
doms += [AND_OPERATOR, ('parent_left', '<', o.parent_right), ('parent_left', '>=', o.parent_left)]
if prefix:
return [(left, 'in', table.search(cr, uid, doms, context=context))]
return doms
@ -172,7 +201,7 @@ class expression(object):
else:
# we assume that the expression is valid
# we create a dummy leaf for forcing the parsing of the resulting expression
self.__exp[i] = '&'
self.__exp[i] = AND_OPERATOR
self.__exp.insert(i + 1, self.__DUMMY_LEAF)
for j, se in enumerate(subexp):
self.__exp.insert(i + 2 + j, se)
@ -387,7 +416,6 @@ class expression(object):
]
self.__exp[i] = ('id', 'inselect', (query1, query2))
return self
def __leaf_to_sql(self, leaf, table):
@ -491,10 +519,10 @@ class expression(object):
params.insert(0, p)
stack.append(q)
else:
if e == '!':
if e == NOT_OPERATOR:
stack.append('(NOT (%s))' % (stack.pop(),))
else:
ops = {'&': ' AND ', '|': ' OR '}
ops = {AND_OPERATOR: ' AND ', OR_OPERATOR: ' OR '}
q1 = stack.pop()
q2 = stack.pop()
stack.append('(%s %s %s)' % (q1, ops[e], q2,))