Add stored path as alternative to MPTT
issue7177 review352501002
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
* Add stored path as alternative to MPTT
|
||||
* Activate and upgrade modules from configuration wizard
|
||||
* Add set_from_header in email tools
|
||||
* Add grouping attribute to numeric widgets
|
||||
|
||||
@@ -647,7 +647,7 @@ Instance methods:
|
||||
Many2One
|
||||
--------
|
||||
|
||||
.. class:: Many2One(model_name, string[, left[, right[, ondelete[, datetime_field[, target_search[, search_order[, search_context[, \**options]]]]]]])
|
||||
.. class:: Many2One(model_name, string[, left[, right[, path[, ondelete[, datetime_field[, target_search[, search_order[, search_context[, \**options]]]]]]]]])
|
||||
|
||||
A many-to-one relation field.
|
||||
|
||||
@@ -674,6 +674,21 @@ A many-to-one relation field.
|
||||
|
||||
The name of the field that stores the right value. See :attr:`left`.
|
||||
|
||||
.. attribute:: Many2One.path
|
||||
|
||||
The name of the :class:`Char` field that stores the path.
|
||||
It only works if the :attr:`model_name` is the same as the model.
|
||||
|
||||
.. note:: The path is used to optimize searches using the ``child_of`` or
|
||||
``parent_of`` operators.
|
||||
|
||||
.. warning::
|
||||
|
||||
The paths in the tree will be rebuilt during the database update if any of
|
||||
the records are found to have a path field equal to the default, or
|
||||
``NULL``.
|
||||
|
||||
|
||||
.. attribute:: Many2One.ondelete
|
||||
|
||||
Define the behavior of the record when the target record is deleted.
|
||||
|
||||
@@ -21,7 +21,7 @@ class Many2One(Field):
|
||||
_sql_type = 'INTEGER'
|
||||
_py_type = int
|
||||
|
||||
def __init__(self, model_name, string='', left=None, right=None,
|
||||
def __init__(self, model_name, string='', left=None, right=None, path=None,
|
||||
ondelete='SET NULL', datetime_field=None, target_search='join',
|
||||
search_order=None, search_context=None, help='', required=False,
|
||||
readonly=False, domain=None, states=None, select=False,
|
||||
@@ -33,6 +33,7 @@ class Many2One(Field):
|
||||
Modified Preorder Tree Traversal.
|
||||
See http://en.wikipedia.org/wiki/Tree_traversal
|
||||
:param right: The name of the field to store the right value. See left
|
||||
:param path: The name of the field used to store the path.
|
||||
:param ondelete: Define the behavior of the record when the target
|
||||
record is deleted. (``CASCADE``, ``RESTRICT``, ``SET NULL``)
|
||||
``SET NULL`` will be changed into ``RESTRICT`` if required is set.
|
||||
@@ -58,6 +59,7 @@ class Many2One(Field):
|
||||
self.model_name = model_name
|
||||
self.left = left
|
||||
self.right = right
|
||||
self.path = path
|
||||
self.datetime_field = datetime_field
|
||||
assert target_search in ['subquery', 'join']
|
||||
self.target_search = target_search
|
||||
@@ -115,6 +117,27 @@ class Many2One(Field):
|
||||
assert value is not False
|
||||
return super().sql_format(value)
|
||||
|
||||
def convert_domain_path(self, domain, tables):
|
||||
cursor = Transaction().connection.cursor()
|
||||
table, _ = tables[None]
|
||||
name, operator, ids = domain
|
||||
red_sql = reduce_ids(table.id, (i for i in ids if i is not None))
|
||||
Target = self.get_target()
|
||||
path_column = getattr(Target, self.path).sql_column(table)
|
||||
cursor.execute(*table.select(path_column, where=red_sql))
|
||||
if operator.endswith('child_of'):
|
||||
where = Or()
|
||||
for path, in cursor:
|
||||
where.append(path_column.like(path + '%'))
|
||||
else:
|
||||
ids = [int(x) for path, in cursor for x in path.split('/')[:-1]]
|
||||
where = reduce_ids(table.id, ids)
|
||||
if not where:
|
||||
where = Literal(False)
|
||||
if operator.startswith('not'):
|
||||
return ~where
|
||||
return where
|
||||
|
||||
def convert_domain_mptt(self, domain, tables):
|
||||
cursor = Transaction().connection.cursor()
|
||||
table, _ = tables[None]
|
||||
@@ -203,6 +226,9 @@ class Many2One(Field):
|
||||
elif self.left and self.right:
|
||||
return self.convert_domain_mptt(
|
||||
(name, operator, ids), tables)
|
||||
elif self.path:
|
||||
return self.convert_domain_path(
|
||||
(name, operator, ids), tables)
|
||||
else:
|
||||
return self.convert_domain_tree(
|
||||
(name, operator, ids), tables)
|
||||
|
||||
+108
-16
@@ -6,10 +6,10 @@ from collections import OrderedDict, defaultdict
|
||||
from functools import wraps
|
||||
|
||||
from sql import (Table, Column, Literal, Desc, Asc, Expression, Null,
|
||||
NullsFirst, NullsLast, For, Union)
|
||||
from sql.functions import CurrentTimestamp, Extract
|
||||
NullsFirst, NullsLast, For, Union, With)
|
||||
from sql.functions import CurrentTimestamp, Extract, Substring
|
||||
from sql.conditionals import Coalesce
|
||||
from sql.operators import Or, And, Operator, Equal
|
||||
from sql.operators import Or, And, Operator, Equal, Concat
|
||||
from sql.aggregate import Count, Max
|
||||
|
||||
from trytond.i18n import gettext
|
||||
@@ -289,19 +289,32 @@ class ModelSQL(ModelStorage):
|
||||
field_name, action=required and 'add' or 'remove')
|
||||
|
||||
for field_name, field in cls._fields.items():
|
||||
if isinstance(field, fields.Many2One) \
|
||||
and field.model_name == cls.__name__ \
|
||||
and field.left and field.right:
|
||||
left_default = cls._defaults.get(field.left, lambda: None)()
|
||||
right_default = cls._defaults.get(field.right, lambda: None)()
|
||||
cursor.execute(*sql_table.select(sql_table.id,
|
||||
where=(Column(sql_table, field.left) == left_default)
|
||||
| (Column(sql_table, field.left) == Null)
|
||||
| (Column(sql_table, field.right) == right_default)
|
||||
| (Column(sql_table, field.right) == Null),
|
||||
limit=1))
|
||||
if cursor.fetchone():
|
||||
cls._rebuild_tree(field_name, None, 0)
|
||||
if (isinstance(field, fields.Many2One)
|
||||
and field.model_name == cls.__name__):
|
||||
if field.path:
|
||||
default_path = cls._defaults.get(
|
||||
field.path, lambda: None)()
|
||||
cursor.execute(*sql_table.select(sql_table.id,
|
||||
where=(
|
||||
Column(sql_table, field.path) == default_path)
|
||||
| (Column(sql_table, field.path) == Null),
|
||||
limit=1))
|
||||
if cursor.fetchone():
|
||||
cls._rebuild_path(field_name)
|
||||
if field.left and field.right:
|
||||
left_default = cls._defaults.get(
|
||||
field.left, lambda: None)()
|
||||
right_default = cls._defaults.get(
|
||||
field.right, lambda: None)()
|
||||
cursor.execute(*sql_table.select(sql_table.id,
|
||||
where=(
|
||||
Column(sql_table, field.left) == left_default)
|
||||
| (Column(sql_table, field.left) == Null)
|
||||
| (Column(sql_table, field.right) == right_default)
|
||||
| (Column(sql_table, field.right) == Null),
|
||||
limit=1))
|
||||
if cursor.fetchone():
|
||||
cls._rebuild_tree(field_name, None, 0)
|
||||
|
||||
for ident, constraint, _ in cls._sql_constraints:
|
||||
table.add_constraint(ident, constraint)
|
||||
@@ -635,6 +648,10 @@ class ModelSQL(ModelStorage):
|
||||
|
||||
transaction.create_records[cls.__name__].update(new_ids)
|
||||
|
||||
# Update path before fields_to_set which could create children
|
||||
if cls._path_fields:
|
||||
field_names = list(sorted(cls._path_fields))
|
||||
cls._set_path(field_names, repeat(new_ids, len(field_names)))
|
||||
# Update mptt before fields_to_set which could create children
|
||||
if cls._mptt_fields:
|
||||
field_names = list(sorted(cls._mptt_fields))
|
||||
@@ -1031,6 +1048,11 @@ class ModelSQL(ModelStorage):
|
||||
if hasattr(field, 'set'):
|
||||
fields_to_set.setdefault(fname, []).extend((ids, value))
|
||||
|
||||
path_fields = cls._path_fields & values.keys()
|
||||
if path_fields:
|
||||
cls._update_path(
|
||||
list(sorted(path_fields)), repeat(ids, len(path_fields)))
|
||||
|
||||
mptt_fields = cls._mptt_fields & values.keys()
|
||||
if mptt_fields:
|
||||
cls._update_mptt(
|
||||
@@ -1504,6 +1526,76 @@ class ModelSQL(ModelStorage):
|
||||
<= transaction.context['_datetime'])
|
||||
return tables, expression
|
||||
|
||||
@classmethod
|
||||
def _rebuild_path(cls, field_name):
|
||||
"Rebuild path for the tree."
|
||||
cursor = Transaction().connection.cursor()
|
||||
field = cls._fields[field_name]
|
||||
table = cls.__table__()
|
||||
tree = With('id', 'path', recursive=True)
|
||||
tree.query = table.select(
|
||||
table.id, Concat(table.id, '/'),
|
||||
where=Column(table, field_name) == Null)
|
||||
tree.query |= (table
|
||||
.join(tree,
|
||||
condition=Column(table, field_name) == tree.id)
|
||||
.select(table.id, Concat(Concat(tree.path, table.id), '/')))
|
||||
query = table.update(
|
||||
[Column(table, field.path)],
|
||||
[tree.path],
|
||||
from_=[tree], where=table.id == tree.id,
|
||||
with_=[tree])
|
||||
cursor.execute(*query)
|
||||
|
||||
@classmethod
|
||||
def _set_path(cls, field_names, list_ids):
|
||||
cursor = Transaction().connection.cursor()
|
||||
table = cls.__table__()
|
||||
parent = cls.__table__()
|
||||
for field_name, ids in zip(field_names, list_ids):
|
||||
field = cls._fields[field_name]
|
||||
parent_column = Column(table, field_name)
|
||||
path_column = Column(table, field.path)
|
||||
query = table.update(
|
||||
[path_column],
|
||||
[Concat(Concat(Coalesce(
|
||||
parent.select(parent.path,
|
||||
where=parent.id == parent_column),
|
||||
''), table.id), '/')])
|
||||
for sub_ids in grouped_slice(ids):
|
||||
query.where = reduce_ids(table.id, sub_ids)
|
||||
cursor.execute(*query)
|
||||
|
||||
@classmethod
|
||||
def _update_path(cls, field_names, list_ids):
|
||||
transaction = Transaction()
|
||||
cursor = transaction.connection.cursor()
|
||||
update = transaction.connection.cursor()
|
||||
table = cls.__table__()
|
||||
parent = cls.__table__()
|
||||
for field_name, ids in zip(field_names, list_ids):
|
||||
field = cls._fields[field_name]
|
||||
parent_column = Column(table, field_name)
|
||||
parent_path_column = Column(parent, field.path)
|
||||
path_column = Column(table, field.path)
|
||||
query = (table
|
||||
.join(parent, 'LEFT',
|
||||
condition=parent_column == parent.id)
|
||||
.select(path_column,
|
||||
Concat(Concat(
|
||||
Coalesce(parent_path_column, ''), table.id), '/')))
|
||||
for sub_ids in grouped_slice(ids):
|
||||
query.where = reduce_ids(table.id, sub_ids)
|
||||
cursor.execute(*query)
|
||||
for old_path, new_path in cursor:
|
||||
if old_path == new_path:
|
||||
continue
|
||||
update.execute(*table.update(
|
||||
[path_column],
|
||||
[Concat(new_path,
|
||||
Substring(table.path, len(old_path) + 1))],
|
||||
where=table.path.like(old_path + '%')))
|
||||
|
||||
@classmethod
|
||||
def _update_mptt(cls, field_names, list_ids, values=None):
|
||||
for field_name, ids in zip(field_names, list_ids):
|
||||
|
||||
@@ -147,11 +147,14 @@ class ModelStorage(Model):
|
||||
super().__post_setup__()
|
||||
|
||||
cls._mptt_fields = set()
|
||||
cls._path_fields = set()
|
||||
for name, field in cls._fields.items():
|
||||
if (isinstance(field, fields.Many2One)
|
||||
and field.model_name == cls.__name__
|
||||
and field.left and field.right):
|
||||
cls._mptt_fields.add(name)
|
||||
and field.model_name == cls.__name__):
|
||||
if field.path:
|
||||
cls._path_fields.add(name)
|
||||
if field.left and field.right:
|
||||
cls._mptt_fields.add(name)
|
||||
|
||||
@staticmethod
|
||||
def default_create_uid():
|
||||
|
||||
@@ -41,6 +41,7 @@ def register():
|
||||
from . import modelview
|
||||
from . import mptt
|
||||
from . import multivalue
|
||||
from . import path
|
||||
from . import resource
|
||||
from . import rule
|
||||
from . import tree
|
||||
@@ -81,6 +82,7 @@ def register():
|
||||
modelview.register('tests')
|
||||
mptt.register('tests')
|
||||
multivalue.register('tests')
|
||||
path.register('tests')
|
||||
resource.register('tests')
|
||||
rule.register('tests')
|
||||
tree.register('tests')
|
||||
|
||||
@@ -65,6 +65,13 @@ class Many2OneMPTT(ModelSQL):
|
||||
return 0
|
||||
|
||||
|
||||
class Many2OnePath(ModelSQL):
|
||||
"Many2One Path"
|
||||
__name__ = 'test.many2one_path'
|
||||
many2one = fields.Many2One('test.many2one_path', 'many2one', path='path')
|
||||
path = fields.Char("Path")
|
||||
|
||||
|
||||
class Many2OneContext(ModelSQL):
|
||||
"Many2One Context"
|
||||
__name__ = 'test.many2one_context'
|
||||
@@ -92,6 +99,7 @@ def register(module):
|
||||
Many2OneTargetStorage,
|
||||
Many2OneTree,
|
||||
Many2OneMPTT,
|
||||
Many2OnePath,
|
||||
Many2OneContext,
|
||||
Many2OneTargetContext,
|
||||
module=module, type_='model')
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# 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 trytond.model import ModelSQL, DeactivableMixin, tree, fields
|
||||
from trytond.pool import Pool
|
||||
|
||||
|
||||
class Path(DeactivableMixin, tree(), ModelSQL):
|
||||
"Path"
|
||||
__name__ = 'test.path'
|
||||
name = fields.Char("Name", required=True)
|
||||
parent = fields.Many2One('test.path', "Parent", select=True, path='path')
|
||||
path = fields.Char("Path")
|
||||
children = fields.One2Many('test.path', 'parent', "Children")
|
||||
|
||||
|
||||
def register(module):
|
||||
Pool.register(
|
||||
Path,
|
||||
module=module, type_='model')
|
||||
@@ -176,297 +176,6 @@ class FieldMany2OneTestCase(unittest.TestCase):
|
||||
"Test search by many2one subquery"
|
||||
self._test_search_join('subquery')
|
||||
|
||||
def create_tree(self, Many2One):
|
||||
self.root1, self.root2 = Many2One.create([{}, {}])
|
||||
self.first1, self.first2, self.first3, self.first4 = Many2One.create([
|
||||
{'many2one': self.root1},
|
||||
{'many2one': self.root1},
|
||||
{'many2one': self.root2},
|
||||
{'many2one': self.root2},
|
||||
])
|
||||
self.second1, self.second2, self.second3, self.second4 = (
|
||||
Many2One.create([
|
||||
{'many2one': self.first1},
|
||||
{'many2one': self.first1},
|
||||
{'many2one': self.first2},
|
||||
{'many2one': self.first2},
|
||||
]))
|
||||
|
||||
@with_transaction()
|
||||
def _test_search_child_of_root1(self, model_name):
|
||||
Many2One = Pool().get(model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'child_of', [self.root1.id]),
|
||||
])
|
||||
|
||||
self.assertListEqual(
|
||||
sorted(result),
|
||||
sorted([self.root1,
|
||||
self.first1, self.first2,
|
||||
self.second1, self.second2, self.second3, self.second4]))
|
||||
|
||||
def test_search_tree_child_of_root1(self):
|
||||
"Test search many2one tree child of root1"
|
||||
self._test_search_child_of_root1('test.many2one_tree')
|
||||
|
||||
def test_search_mptt_child_of_root1(self):
|
||||
"Test search many2one mptt child of root1"
|
||||
self._test_search_child_of_root1('test.many2one_mptt')
|
||||
|
||||
@with_transaction()
|
||||
def _test_search_not_child_of_root1(self, model_name):
|
||||
Many2One = Pool().get(model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'not child_of', [self.root1.id]),
|
||||
])
|
||||
|
||||
self.assertListEqual(
|
||||
sorted(result),
|
||||
sorted([self.root2, self.first3, self.first4]))
|
||||
|
||||
def test_search_tree_not_child_of_root1(self):
|
||||
"Test search many2one tree not child of root1"
|
||||
self._test_search_not_child_of_root1('test.many2one_tree')
|
||||
|
||||
def test_search_mptt_not_child_of_root1(self):
|
||||
"Test search many2one mptt not child of root1"
|
||||
self._test_search_not_child_of_root1('test.many2one_mptt')
|
||||
|
||||
@with_transaction()
|
||||
def _test_search_child_of_second1(self, model_name):
|
||||
Many2One = Pool().get(model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'child_of', [self.second1.id]),
|
||||
])
|
||||
|
||||
self.assertListEqual(result, [self.second1])
|
||||
|
||||
def test_search_tree_child_of_second1(self):
|
||||
"Test search many2one tree child of second1"
|
||||
self._test_search_child_of_second1('test.many2one_tree')
|
||||
|
||||
def test_search_mptt_child_of_second1(self):
|
||||
"Test search many2one mptt child of second1"
|
||||
self._test_search_child_of_second1('test.many2one_mptt')
|
||||
|
||||
@with_transaction()
|
||||
def _test_search_not_child_of_second1(self, model_name):
|
||||
Many2One = Pool().get(model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'not child_of', [self.second1.id]),
|
||||
])
|
||||
|
||||
self.assertListEqual(
|
||||
sorted(result),
|
||||
sorted([self.root1, self.root2,
|
||||
self.first1, self.first2, self.first3, self.first4,
|
||||
self.second2, self.second3, self.second4]))
|
||||
|
||||
def test_search_tree_not_child_of_second1(self):
|
||||
"Test search many2one tree not child of second1"
|
||||
self._test_search_not_child_of_second1('test.many2one_tree')
|
||||
|
||||
def test_search_mptt_not_child_of_second1(self):
|
||||
"Test search many2one mptt not child of second1"
|
||||
self._test_search_not_child_of_second1('test.many2one_mptt')
|
||||
|
||||
@with_transaction()
|
||||
def _test_search_child_of_empty(self, model_name):
|
||||
Many2One = Pool().get(model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'child_of', []),
|
||||
])
|
||||
|
||||
self.assertListEqual(result, [])
|
||||
|
||||
def test_search_tree_child_of_empty(self):
|
||||
"Test search many2one tree child of empty"
|
||||
self._test_search_child_of_empty('test.many2one_tree')
|
||||
|
||||
def test_search_mptt_child_of_empty(self):
|
||||
"Test search many2one mptt child of empty"
|
||||
self._test_search_child_of_empty('test.many2one_mptt')
|
||||
|
||||
@with_transaction()
|
||||
def _test_search_not_child_of_empty(self, model_name):
|
||||
Many2One = Pool().get(model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'not child_of', []),
|
||||
])
|
||||
|
||||
self.assertListEqual(
|
||||
sorted(result),
|
||||
sorted([self.root1, self.root2,
|
||||
self.first1, self.first2, self.first3, self.first4,
|
||||
self.second1, self.second2, self.second3, self.second4]))
|
||||
|
||||
def test_search_tree_not_child_of_empty(self):
|
||||
"Test search many2one tree not child of empty"
|
||||
self._test_search_not_child_of_empty('test.many2one_tree')
|
||||
|
||||
def test_search_mptt_not_child_of_empty(self):
|
||||
"Test search many2one mptt not child of empty"
|
||||
self._test_search_not_child_of_empty('test.many2one_mptt')
|
||||
|
||||
@with_transaction()
|
||||
def _test_search_child_of_none(self, model_name):
|
||||
Many2One = Pool().get(model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'child_of', [None]),
|
||||
])
|
||||
|
||||
self.assertListEqual(result, [])
|
||||
|
||||
def test_search_tree_child_of_none(self):
|
||||
"Test search many2one tree child of None"
|
||||
self._test_search_child_of_none('test.many2one_tree')
|
||||
|
||||
def test_search_mptt_child_of_none(self):
|
||||
"Test search many2one mptt child of None"
|
||||
self._test_search_child_of_none('test.many2one_mptt')
|
||||
|
||||
@with_transaction()
|
||||
def _test_search_parent_of_root1(self, model_name):
|
||||
Many2One = Pool().get(model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'parent_of', [self.root1.id]),
|
||||
])
|
||||
|
||||
self.assertListEqual(result, [self.root1])
|
||||
|
||||
def test_search_tree_parent_of_root1(self):
|
||||
"Test search many2one tree parent of root1"
|
||||
self._test_search_parent_of_root1('test.many2one_tree')
|
||||
|
||||
def test_search_mptt_parent_of_root1(self):
|
||||
"Test search many2one mptt parent of root1"
|
||||
self._test_search_parent_of_root1('test.many2one_mptt')
|
||||
|
||||
@with_transaction()
|
||||
def _test_search_not_parent_of_root1(self, model_name):
|
||||
Many2One = Pool().get(model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'not parent_of', [self.root1.id]),
|
||||
])
|
||||
|
||||
self.assertListEqual(
|
||||
sorted(result),
|
||||
sorted([self.root2,
|
||||
self.first1, self.first2, self.first3, self.first4,
|
||||
self.second1, self.second2, self.second3, self.second4]))
|
||||
|
||||
def test_search_tree_not_parent_of_root1(self):
|
||||
"Test search many2one tree not parent of root1"
|
||||
self._test_search_not_parent_of_root1('test.many2one_tree')
|
||||
|
||||
def test_search_mptt_not_parent_of_root1(self):
|
||||
"Test search many2one mptt not parent of root1"
|
||||
self._test_search_not_parent_of_root1('test.many2one_mptt')
|
||||
|
||||
@with_transaction()
|
||||
def _test_search_parent_of_second4(self, model_name):
|
||||
Many2One = Pool().get(model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'parent_of', [self.second4.id]),
|
||||
])
|
||||
|
||||
self.assertListEqual(
|
||||
sorted(result),
|
||||
sorted([self.root1, self.first2, self.second4]))
|
||||
|
||||
def test_search_tree_parent_of_second4(self):
|
||||
"Test search many2one tree parent of second4"
|
||||
self._test_search_parent_of_second4('test.many2one_tree')
|
||||
|
||||
def test_search_mptt_parent_of_second4(self):
|
||||
"Test search many2one mptt parent of second4"
|
||||
self._test_search_parent_of_second4('test.many2one_mptt')
|
||||
|
||||
@with_transaction()
|
||||
def _test_search_not_parent_of_second4(self, model_name):
|
||||
Many2One = Pool().get(model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'not parent_of', [self.second4.id]),
|
||||
])
|
||||
|
||||
self.assertListEqual(
|
||||
sorted(result),
|
||||
sorted([self.root2,
|
||||
self.first1, self.first3, self.first4,
|
||||
self.second1, self.second2, self.second3]))
|
||||
|
||||
def test_search_tree_not_parent_of_second4(self):
|
||||
"Test search many2one tree not parent of second4"
|
||||
self._test_search_not_parent_of_second4('test.many2one_tree')
|
||||
|
||||
def test_search_mptt_not_parent_of_second4(self):
|
||||
"Test search many2one mptt not parent of second4"
|
||||
self._test_search_not_parent_of_second4('test.many2one_mptt')
|
||||
|
||||
@with_transaction()
|
||||
def _test_search_parent_of_empty(self, model_name):
|
||||
Many2One = Pool().get(model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'parent_of', []),
|
||||
])
|
||||
|
||||
self.assertListEqual(result, [])
|
||||
|
||||
def test_search_tree_parent_of_empty(self):
|
||||
"Test search many2one tree parent of empty"
|
||||
self._test_search_parent_of_empty('test.many2one_tree')
|
||||
|
||||
def test_search_mptt_parent_of_empty(self):
|
||||
"Test search many2one mptt parent of empty"
|
||||
self._test_search_parent_of_empty('test.many2one_mptt')
|
||||
|
||||
@with_transaction()
|
||||
def _test_search_not_parent_of_empty(self, model_name):
|
||||
Many2One = Pool().get(model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'not parent_of', []),
|
||||
])
|
||||
|
||||
self.assertListEqual(
|
||||
sorted(result),
|
||||
sorted([self.root1, self.root2,
|
||||
self.first1, self.first2, self.first3, self.first4,
|
||||
self.second1, self.second2, self.second3, self.second4]))
|
||||
|
||||
def test_search_tree_not_parent_of_empty(self):
|
||||
"Test search many2one tree not parent of empty"
|
||||
self._test_search_not_parent_of_empty('test.many2one_tree')
|
||||
|
||||
def test_search_mptt_not_parent_of_empty(self):
|
||||
"Test search many2one mptt not parent of empty"
|
||||
self._test_search_not_parent_of_empty('test.many2one_mptt')
|
||||
|
||||
@with_transaction()
|
||||
def test_context_attribute(self):
|
||||
"Test context on many2one attribute"
|
||||
@@ -509,5 +218,226 @@ class FieldMany2OneTestCase(unittest.TestCase):
|
||||
self.assertEqual(record.target.context, 'foo')
|
||||
|
||||
|
||||
class FieldMany2OneTreeTestCase(unittest.TestCase):
|
||||
"Test Field Many2One Tree"
|
||||
model_name = 'test.many2one_tree'
|
||||
|
||||
def create_tree(self, Many2One):
|
||||
self.root1, self.root2 = Many2One.create([{}, {}])
|
||||
self.first1, self.first2, self.first3, self.first4 = Many2One.create([
|
||||
{'many2one': self.root1},
|
||||
{'many2one': self.root1},
|
||||
{'many2one': self.root2},
|
||||
{'many2one': self.root2},
|
||||
])
|
||||
self.second1, self.second2, self.second3, self.second4 = (
|
||||
Many2One.create([
|
||||
{'many2one': self.first1},
|
||||
{'many2one': self.first1},
|
||||
{'many2one': self.first2},
|
||||
{'many2one': self.first2},
|
||||
]))
|
||||
|
||||
@with_transaction()
|
||||
def test_search_child_of_root1(self):
|
||||
"Test search many2one child of root1"
|
||||
Many2One = Pool().get(self.model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'child_of', [self.root1.id]),
|
||||
])
|
||||
|
||||
self.assertListEqual(
|
||||
sorted(result),
|
||||
sorted([self.root1,
|
||||
self.first1, self.first2,
|
||||
self.second1, self.second2, self.second3, self.second4]))
|
||||
|
||||
@with_transaction()
|
||||
def test_search_not_child_of_root1(self):
|
||||
"Test search many2one not child of root1"
|
||||
Many2One = Pool().get(self.model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'not child_of', [self.root1.id]),
|
||||
])
|
||||
|
||||
self.assertListEqual(
|
||||
sorted(result),
|
||||
sorted([self.root2, self.first3, self.first4]))
|
||||
|
||||
@with_transaction()
|
||||
def test_search_child_of_second1(self):
|
||||
"Test search many2one child of second1"
|
||||
Many2One = Pool().get(self.model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'child_of', [self.second1.id]),
|
||||
])
|
||||
|
||||
self.assertListEqual(result, [self.second1])
|
||||
|
||||
@with_transaction()
|
||||
def test_search_not_child_of_second1(self):
|
||||
"Test search many2one not child of second1"
|
||||
Many2One = Pool().get(self.model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'not child_of', [self.second1.id]),
|
||||
])
|
||||
|
||||
self.assertListEqual(
|
||||
sorted(result),
|
||||
sorted([self.root1, self.root2,
|
||||
self.first1, self.first2, self.first3, self.first4,
|
||||
self.second2, self.second3, self.second4]))
|
||||
|
||||
@with_transaction()
|
||||
def test_search_child_of_empty(self):
|
||||
"Test search many2one child of empty"
|
||||
Many2One = Pool().get(self.model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'child_of', []),
|
||||
])
|
||||
|
||||
self.assertListEqual(result, [])
|
||||
|
||||
@with_transaction()
|
||||
def test_search_not_child_of_empty(self):
|
||||
"Test search many2one not child of empty"
|
||||
Many2One = Pool().get(self.model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'not child_of', []),
|
||||
])
|
||||
|
||||
self.assertListEqual(
|
||||
sorted(result),
|
||||
sorted([self.root1, self.root2,
|
||||
self.first1, self.first2, self.first3, self.first4,
|
||||
self.second1, self.second2, self.second3, self.second4]))
|
||||
|
||||
@with_transaction()
|
||||
def test_search_child_of_none(self):
|
||||
"Test search many2one child of None"
|
||||
Many2One = Pool().get(self.model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'child_of', [None]),
|
||||
])
|
||||
|
||||
self.assertListEqual(result, [])
|
||||
|
||||
@with_transaction()
|
||||
def test_search_parent_of_root1(self):
|
||||
"Test search many2one parent of root1"
|
||||
Many2One = Pool().get(self.model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'parent_of', [self.root1.id]),
|
||||
])
|
||||
|
||||
self.assertListEqual(result, [self.root1])
|
||||
|
||||
@with_transaction()
|
||||
def test_search_not_parent_of_root1(self):
|
||||
"Test search many2one not parent of root1"
|
||||
Many2One = Pool().get(self.model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'not parent_of', [self.root1.id]),
|
||||
])
|
||||
|
||||
self.assertListEqual(
|
||||
sorted(result),
|
||||
sorted([self.root2,
|
||||
self.first1, self.first2, self.first3, self.first4,
|
||||
self.second1, self.second2, self.second3, self.second4]))
|
||||
|
||||
@with_transaction()
|
||||
def test_search_parent_of_second4(self):
|
||||
"Test search many2one parent of second4"
|
||||
Many2One = Pool().get(self.model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'parent_of', [self.second4.id]),
|
||||
])
|
||||
|
||||
self.assertListEqual(
|
||||
sorted(result),
|
||||
sorted([self.root1, self.first2, self.second4]))
|
||||
|
||||
@with_transaction()
|
||||
def test_search_not_parent_of_second4(self):
|
||||
"Test search many2one not parent of second4"
|
||||
Many2One = Pool().get(self.model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'not parent_of', [self.second4.id]),
|
||||
])
|
||||
|
||||
self.assertListEqual(
|
||||
sorted(result),
|
||||
sorted([self.root2,
|
||||
self.first1, self.first3, self.first4,
|
||||
self.second1, self.second2, self.second3]))
|
||||
|
||||
@with_transaction()
|
||||
def test_search_parent_of_empty(self):
|
||||
"Test search many2one parent of empty"
|
||||
Many2One = Pool().get(self.model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'parent_of', []),
|
||||
])
|
||||
|
||||
self.assertListEqual(result, [])
|
||||
|
||||
@with_transaction()
|
||||
def test_search_not_parent_of_empty(self):
|
||||
"Test search many2one not parent of empty"
|
||||
Many2One = Pool().get(self.model_name)
|
||||
self.create_tree(Many2One)
|
||||
|
||||
result = Many2One.search([
|
||||
('many2one', 'not parent_of', []),
|
||||
])
|
||||
|
||||
self.assertListEqual(
|
||||
sorted(result),
|
||||
sorted([self.root1, self.root2,
|
||||
self.first1, self.first2, self.first3, self.first4,
|
||||
self.second1, self.second2, self.second3, self.second4]))
|
||||
|
||||
|
||||
class FieldMany2OneMPTTTestCase(FieldMany2OneTreeTestCase):
|
||||
"Test Field Many2One MPTT"
|
||||
model_name = 'test.many2one_mptt'
|
||||
|
||||
|
||||
class FieldMany2OnePathTestCase(FieldMany2OneTreeTestCase):
|
||||
"Test Field Many2One Path"
|
||||
model_name = 'test.many2one_path'
|
||||
|
||||
|
||||
def suite():
|
||||
return unittest.TestLoader().loadTestsFromTestCase(FieldMany2OneTestCase)
|
||||
suite_ = unittest.TestSuite()
|
||||
loader = unittest.TestLoader()
|
||||
suite_.addTests(loader.loadTestsFromTestCase(FieldMany2OneTestCase))
|
||||
suite_.addTests(loader.loadTestsFromTestCase(FieldMany2OneTreeTestCase))
|
||||
suite_.addTests(loader.loadTestsFromTestCase(FieldMany2OneMPTTTestCase))
|
||||
suite_.addTests(loader.loadTestsFromTestCase(FieldMany2OnePathTestCase))
|
||||
return suite_
|
||||
|
||||
+8
-114
@@ -9,9 +9,12 @@ from trytond.tests.test_tryton import activate_module, with_transaction
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.pool import Pool
|
||||
|
||||
from .test_tree import TreeTestCaseMixin
|
||||
|
||||
class MPTTTestCase(unittest.TestCase):
|
||||
|
||||
class MPTTTestCase(TreeTestCaseMixin, unittest.TestCase):
|
||||
'Test Modified Preorder Tree Traversal'
|
||||
model_name = 'test.mptt'
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@@ -19,7 +22,7 @@ class MPTTTestCase(unittest.TestCase):
|
||||
|
||||
def check_tree(self, parent_id=None, left=-1, right=sys.maxsize):
|
||||
pool = Pool()
|
||||
Mptt = pool.get('test.mptt')
|
||||
Mptt = pool.get(self.model_name)
|
||||
|
||||
with Transaction().set_context(active_test=False):
|
||||
childs = Mptt.search([
|
||||
@@ -51,119 +54,10 @@ class MPTTTestCase(unittest.TestCase):
|
||||
child, child.right, previous_right))
|
||||
previous_right = child.left
|
||||
|
||||
def reparent(self, parent=None):
|
||||
def rebuild(self):
|
||||
pool = Pool()
|
||||
Mptt = pool.get('test.mptt')
|
||||
|
||||
records = Mptt.search([
|
||||
('parent', '=', parent),
|
||||
])
|
||||
if not records:
|
||||
return
|
||||
for record in records:
|
||||
for record2 in records:
|
||||
if record != record2:
|
||||
record.parent = record2
|
||||
record.save()
|
||||
record.parent = parent
|
||||
record.save()
|
||||
for record in records:
|
||||
self.reparent(record)
|
||||
|
||||
def create(self):
|
||||
pool = Pool()
|
||||
Mptt = pool.get('test.mptt')
|
||||
|
||||
new_records = [None, None, None]
|
||||
for j in range(3):
|
||||
parent_records = new_records
|
||||
new_records = []
|
||||
k = 0
|
||||
to_create = []
|
||||
for parent_record in parent_records:
|
||||
to_create.extend([{
|
||||
'name': 'Test %d %d %d' % (j, k, i),
|
||||
'parent': (parent_record.id
|
||||
if parent_record else None),
|
||||
} for i in range(3)])
|
||||
k += 1
|
||||
new_records = Mptt.create(to_create)
|
||||
|
||||
@with_transaction()
|
||||
def test_create(self):
|
||||
'Test create tree'
|
||||
self.create()
|
||||
self.check_tree()
|
||||
|
||||
@with_transaction()
|
||||
def test_reparent(self):
|
||||
'Test re-parent'
|
||||
self.create()
|
||||
self.reparent()
|
||||
|
||||
@with_transaction()
|
||||
def test_active(self):
|
||||
'Test active'
|
||||
pool = Pool()
|
||||
Mptt = pool.get('test.mptt')
|
||||
|
||||
self.create()
|
||||
|
||||
records = Mptt.search([])
|
||||
for record in records:
|
||||
if record.id % 2:
|
||||
record.active = False
|
||||
record.save()
|
||||
self.check_tree()
|
||||
self.reparent()
|
||||
self.check_tree()
|
||||
|
||||
records = Mptt.search([])
|
||||
Mptt.write(records, {
|
||||
'active': True,
|
||||
})
|
||||
Mptt.write(records[::2], {
|
||||
'active': False
|
||||
})
|
||||
self.check_tree()
|
||||
|
||||
records = Mptt.search([])
|
||||
Mptt.write(records, {
|
||||
'active': True,
|
||||
})
|
||||
Mptt.write(records[:len(records) // 2], {
|
||||
'active': False
|
||||
})
|
||||
self.check_tree()
|
||||
|
||||
records = Mptt.search([])
|
||||
Mptt.write(records, {
|
||||
'active': False
|
||||
})
|
||||
self.reparent()
|
||||
self.check_tree()
|
||||
|
||||
@with_transaction()
|
||||
def test_delete(self):
|
||||
'Test delete'
|
||||
pool = Pool()
|
||||
Mptt = pool.get('test.mptt')
|
||||
|
||||
self.create()
|
||||
|
||||
records = Mptt.search([])
|
||||
for record in records:
|
||||
if record.id % 2:
|
||||
Mptt.delete([record])
|
||||
self.check_tree()
|
||||
|
||||
records = Mptt.search([])
|
||||
Mptt.delete(records[:len(records) // 2])
|
||||
self.check_tree()
|
||||
|
||||
records = Mptt.search([])
|
||||
Mptt.delete(records)
|
||||
self.check_tree()
|
||||
Mptt = pool.get(self.model_name)
|
||||
Mptt._rebuild_tree('parent', None, 0)
|
||||
|
||||
@with_transaction()
|
||||
def test_update_only_if_parent_is_modified(self):
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import unittest
|
||||
|
||||
from trytond.pool import Pool
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
from .test_tree import TreeTestCaseMixin
|
||||
|
||||
|
||||
class PathTestCase(TreeTestCaseMixin, unittest.TestCase):
|
||||
"Test Path"
|
||||
model_name = 'test.path'
|
||||
|
||||
def check_tree(self, parent_id=None):
|
||||
pool = Pool()
|
||||
Path = pool.get(self.model_name)
|
||||
|
||||
with Transaction().set_context(active_test=False):
|
||||
children = Path.search([
|
||||
('parent', '=', parent_id),
|
||||
])
|
||||
for child in children:
|
||||
if child.parent:
|
||||
self.assertEqual(
|
||||
child.path, '%s%s/' % (child.parent.path, child.id))
|
||||
else:
|
||||
self.assertEqual(child.path, '%s/' % child.id)
|
||||
self.check_tree(child.id)
|
||||
|
||||
def rebuild(self):
|
||||
pool = Pool()
|
||||
Path = pool.get(self.model_name)
|
||||
Path._rebuild_path('parent')
|
||||
|
||||
|
||||
def suite():
|
||||
return unittest.TestLoader().loadTestsFromTestCase(PathTestCase)
|
||||
@@ -8,6 +8,151 @@ from trytond.tests.test_tryton import activate_module, with_transaction
|
||||
from trytond.pool import Pool
|
||||
|
||||
|
||||
class TreeTestCaseMixin:
|
||||
model_name = None
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
activate_module('tests')
|
||||
|
||||
def create(self):
|
||||
pool = Pool()
|
||||
Model = pool.get(self.model_name)
|
||||
|
||||
new_records = [None, None, None]
|
||||
for j in range(3):
|
||||
parent_records = new_records
|
||||
new_records = []
|
||||
k = 0
|
||||
to_create = []
|
||||
for parent_record in parent_records:
|
||||
to_create.extend([{
|
||||
'name': 'Test %d %d %d' % (j, k, i),
|
||||
'parent': (parent_record.id
|
||||
if parent_record else None),
|
||||
} for i in range(3)])
|
||||
k += 1
|
||||
new_records = Model.create(to_create)
|
||||
|
||||
def check_tree(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def change_parent(self, parent=None, restore=False):
|
||||
pool = Pool()
|
||||
Model = pool.get(self.model_name)
|
||||
|
||||
records = Model.search([
|
||||
('parent', '=', parent),
|
||||
])
|
||||
if not records:
|
||||
return
|
||||
for record in records[::2]:
|
||||
for record2 in records[1::2]:
|
||||
record.parent = record2
|
||||
record.save()
|
||||
if restore:
|
||||
record.parent = parent
|
||||
record.save()
|
||||
for record in records:
|
||||
self.change_parent(record, restore=restore)
|
||||
|
||||
@with_transaction()
|
||||
def test_create(self):
|
||||
"Test create tree"
|
||||
self.create()
|
||||
self.check_tree()
|
||||
|
||||
@with_transaction()
|
||||
def test_reparent(self):
|
||||
"Test re-parent"
|
||||
self.create()
|
||||
self.change_parent(restore=True)
|
||||
self.check_tree()
|
||||
|
||||
@with_transaction()
|
||||
def test_move(self):
|
||||
"Test move"
|
||||
self.create()
|
||||
self.change_parent()
|
||||
self.check_tree()
|
||||
|
||||
@with_transaction()
|
||||
def test_active(self):
|
||||
"Test active"
|
||||
pool = Pool()
|
||||
Model = pool.get(self.model_name)
|
||||
|
||||
self.create()
|
||||
|
||||
records = Model.search([])
|
||||
for record in records:
|
||||
if record.id % 2:
|
||||
record.active = False
|
||||
record.save()
|
||||
self.check_tree()
|
||||
self.change_parent()
|
||||
self.check_tree()
|
||||
|
||||
records = Model.search([])
|
||||
Model.write(records, {
|
||||
'active': True,
|
||||
})
|
||||
Model.write(records[::2], {
|
||||
'active': False
|
||||
})
|
||||
self.check_tree()
|
||||
|
||||
records = Model.search([])
|
||||
Model.write(records, {
|
||||
'active': True,
|
||||
})
|
||||
Model.write(records[:len(records) // 2], {
|
||||
'active': False
|
||||
})
|
||||
self.check_tree()
|
||||
|
||||
records = Model.search([])
|
||||
Model.write(records, {
|
||||
'active': False
|
||||
})
|
||||
self.change_parent()
|
||||
self.check_tree()
|
||||
|
||||
@with_transaction()
|
||||
def test_delete(self):
|
||||
"Test delete"
|
||||
pool = Pool()
|
||||
Model = pool.get(self.model_name)
|
||||
|
||||
self.create()
|
||||
|
||||
records = Model.search([])
|
||||
for record in records:
|
||||
if record.id % 2:
|
||||
Model.delete([record])
|
||||
self.check_tree()
|
||||
|
||||
records = Model.search([])
|
||||
Model.delete(records[:len(records) // 2])
|
||||
self.check_tree()
|
||||
|
||||
records = Model.search([])
|
||||
Model.delete(records)
|
||||
self.check_tree()
|
||||
|
||||
def rebuild(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@with_transaction()
|
||||
def test_rebuild(self):
|
||||
"Test rebuild"
|
||||
self.create()
|
||||
|
||||
self.rebuild()
|
||||
|
||||
self.check_tree()
|
||||
|
||||
|
||||
class TreeMixinTestCase(unittest.TestCase):
|
||||
"Test TreeMixin"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user