Add stored path as alternative to MPTT

issue7177
review352501002
This commit is contained in:
Cédric Krier
2021-10-03 01:14:08 +02:00
parent 6bba066883
commit 66a0dcf2b5
12 changed files with 600 additions and 427 deletions
+1
View File
@@ -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
+16 -1
View File
@@ -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.
+27 -1
View File
@@ -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
View File
@@ -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):
+6 -3
View File
@@ -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():
+2
View File
@@ -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')
+8
View File
@@ -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')
+19
View File
@@ -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')
+222 -292
View File
@@ -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
View File
@@ -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):
+38
View File
@@ -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)
+145
View File
@@ -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"