Files
tryton/trytond/model/tree.py
T
2018-12-29 14:19:08 +01:00

129 lines
5.4 KiB
Python

# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from itertools import chain
from trytond.i18n import gettext
from .modelstorage import ValidationError
class RecursionError(ValidationError):
pass
def tree(parent='parent', name='name', separator=None):
class TreeMixin(object):
if separator:
@classmethod
def __setup__(cls):
super(TreeMixin, cls).__setup__()
field = getattr(cls, name)
clause = (name, 'not like', '%' + separator + '%')
# If TreeMixin is after the class where name is defined in
# __mro__, it modifies the base field copied so it must ensure
# to add only once the domain
if clause not in field.domain:
domain = [clause]
if field.domain:
domain.append(field.domain)
field.domain = domain
def get_rec_name(self, _):
record, names = self, []
while record:
names.append(getattr(record, name))
record = getattr(record, parent)
return separator.join(reversed(names))
@classmethod
def search_rec_name(cls, _, clause):
if isinstance(clause[2], str):
values = list(reversed(clause[2].split(separator)))
else:
values = [[]]
for value in clause[2]:
if value is None:
values[0].append(value)
continue
for i, v in range(reversed(value.split(separator))):
while len(values) <= i:
values.append([])
values[i].append(v)
domain = []
field = name
for value in values:
domain.append((field, clause[1], value.strip()))
field = parent + '.' + field
if ((clause[1].endswith('like')
and not clause[2].replace(
'%%', '__').startswith('%'))
or not clause[1].endswith('like')):
if clause[1].startswith('not') or clause[1] == '!=':
operator = '!='
domain.insert(0, 'OR')
else:
operator = '='
top_parent = '.'.join((parent,) * len(values))
domain.append((top_parent, operator, None))
if (clause[1].endswith('like')
and clause[2].replace('%%', '__').endswith('%')):
ids = list(map(int, cls.search(domain, order=[])))
domain = [(parent, 'child_of', ids)]
return domain
@classmethod
def validate(cls, records):
super(TreeMixin, cls).validate(records)
cls.check_recursion(records)
@classmethod
def check_recursion(cls, records):
'''
Function that checks if there is no recursion in the tree
composed with parent as parent field name.
'''
if hasattr(super(TreeMixin, cls), 'check_recursion'):
super(TreeMixin, cls).check_recursion(records)
parent_type = cls._fields[parent]._type
if parent_type not in ('many2one', 'many2many', 'one2one'):
raise ValueError(
'Unsupported field type "%s" for field "%s" on "%s"'
% (parent_type, parent, cls.__name__))
visited = set()
for record in records:
walked = set()
walker = getattr(record, parent)
while walker:
if parent_type == 'many2many':
for walk in walker:
walked.add(walk.id)
if walk.id == record.id:
parent_name = ', '.join(getattr(r, name)
for r in getattr(record, parent))
raise RecursionError(
gettext('ir.recursion_error',
rec_name=getattr(record, name),
parent_rec_name=parent_name))
walker = list(chain(*(
getattr(walk, parent)
for walk in walker
if walk.id not in visited)))
else:
walked.add(walker.id)
if walker.id == record.id:
parent_name = getattr(
getattr(record, parent), name)
raise RecursionError(
gettext('ir.recursion_error',
rec_name=getattr(record, name),
parent_rec_name=parent_name))
walker = (getattr(walker, parent) not in visited
and getattr(walker, parent))
visited.update(walked)
return TreeMixin