django-fast-treenode 2.1.4__py3-none-any.whl → 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_fast_treenode-3.0.0.dist-info/METADATA +203 -0
- django_fast_treenode-3.0.0.dist-info/RECORD +90 -0
- {django_fast_treenode-2.1.4.dist-info → django_fast_treenode-3.0.0.dist-info}/WHEEL +1 -1
- treenode/admin/__init__.py +2 -7
- treenode/admin/admin.py +138 -209
- treenode/admin/changelist.py +21 -39
- treenode/admin/exporter.py +170 -0
- treenode/admin/importer.py +171 -0
- treenode/admin/mixin.py +291 -0
- treenode/apps.py +42 -20
- treenode/cache.py +192 -303
- treenode/forms.py +45 -65
- treenode/managers/__init__.py +4 -20
- treenode/managers/managers.py +216 -0
- treenode/managers/queries.py +233 -0
- treenode/managers/tasks.py +167 -0
- treenode/models/__init__.py +8 -5
- treenode/models/decorators.py +54 -0
- treenode/models/factory.py +44 -68
- treenode/models/mixins/__init__.py +2 -1
- treenode/models/mixins/ancestors.py +44 -20
- treenode/models/mixins/children.py +33 -26
- treenode/models/mixins/descendants.py +33 -22
- treenode/models/mixins/family.py +25 -15
- treenode/models/mixins/logical.py +23 -21
- treenode/models/mixins/node.py +162 -104
- treenode/models/mixins/properties.py +22 -16
- treenode/models/mixins/roots.py +59 -15
- treenode/models/mixins/siblings.py +46 -43
- treenode/models/mixins/tree.py +212 -153
- treenode/models/mixins/update.py +154 -0
- treenode/models/models.py +365 -0
- treenode/settings.py +28 -0
- treenode/static/{treenode/css → css}/tree_widget.css +1 -1
- treenode/static/{treenode/css → css}/treenode_admin.css +43 -2
- treenode/static/css/treenode_tabs.css +51 -0
- treenode/static/js/lz-string.min.js +1 -0
- treenode/static/{treenode/js → js}/tree_widget.js +9 -23
- treenode/static/js/treenode_admin.js +531 -0
- treenode/static/vendors/jquery-ui/AUTHORS.txt +384 -0
- treenode/static/vendors/jquery-ui/LICENSE.txt +43 -0
- treenode/static/vendors/jquery-ui/external/jquery/jquery.js +10716 -0
- treenode/static/vendors/jquery-ui/images/ui-icons_444444_256x240.png +0 -0
- treenode/static/vendors/jquery-ui/images/ui-icons_555555_256x240.png +0 -0
- treenode/static/vendors/jquery-ui/images/ui-icons_777620_256x240.png +0 -0
- treenode/static/vendors/jquery-ui/images/ui-icons_777777_256x240.png +0 -0
- treenode/static/vendors/jquery-ui/images/ui-icons_cc0000_256x240.png +0 -0
- treenode/static/vendors/jquery-ui/images/ui-icons_ffffff_256x240.png +0 -0
- treenode/static/vendors/jquery-ui/index.html +297 -0
- treenode/static/vendors/jquery-ui/jquery-ui.css +438 -0
- treenode/static/vendors/jquery-ui/jquery-ui.js +5223 -0
- treenode/static/vendors/jquery-ui/jquery-ui.min.css +7 -0
- treenode/static/vendors/jquery-ui/jquery-ui.min.js +6 -0
- treenode/static/vendors/jquery-ui/jquery-ui.structure.css +16 -0
- treenode/static/vendors/jquery-ui/jquery-ui.structure.min.css +5 -0
- treenode/static/vendors/jquery-ui/jquery-ui.theme.css +439 -0
- treenode/static/vendors/jquery-ui/jquery-ui.theme.min.css +5 -0
- treenode/static/vendors/jquery-ui/package.json +82 -0
- treenode/templates/admin/treenode_changelist.html +25 -0
- treenode/templates/admin/treenode_import_export.html +85 -0
- treenode/templates/admin/treenode_rows.html +57 -0
- treenode/tests.py +3 -0
- treenode/urls.py +6 -27
- treenode/utils/__init__.py +0 -15
- treenode/utils/db/__init__.py +7 -0
- treenode/utils/db/compiler.py +114 -0
- treenode/utils/db/db_vendor.py +50 -0
- treenode/utils/db/service.py +84 -0
- treenode/utils/db/sqlcompat.py +60 -0
- treenode/utils/db/sqlquery.py +70 -0
- treenode/version.py +2 -2
- treenode/views/__init__.py +5 -0
- treenode/views/autoapi.py +91 -0
- treenode/views/autocomplete.py +52 -0
- treenode/views/children.py +41 -0
- treenode/views/common.py +23 -0
- treenode/views/crud.py +209 -0
- treenode/views/search.py +48 -0
- treenode/widgets.py +27 -44
- django_fast_treenode-2.1.4.dist-info/METADATA +0 -166
- django_fast_treenode-2.1.4.dist-info/RECORD +0 -63
- treenode/admin/mixins.py +0 -302
- treenode/managers/adjacency.py +0 -205
- treenode/managers/closure.py +0 -278
- treenode/models/adjacency.py +0 -342
- treenode/models/classproperty.py +0 -27
- treenode/models/closure.py +0 -122
- treenode/static/treenode/js/.gitkeep +0 -1
- treenode/static/treenode/js/treenode_admin.js +0 -131
- treenode/templates/admin/export_success.html +0 -26
- treenode/templates/admin/tree_node_changelist.html +0 -19
- treenode/templates/admin/tree_node_export.html +0 -27
- treenode/templates/admin/tree_node_import.html +0 -45
- treenode/templates/admin/tree_node_import_report.html +0 -32
- treenode/templates/widgets/tree_widget.css +0 -23
- treenode/utils/aid.py +0 -46
- treenode/utils/base16.py +0 -38
- treenode/utils/base36.py +0 -37
- treenode/utils/db.py +0 -116
- treenode/utils/exporter.py +0 -196
- treenode/utils/importer.py +0 -328
- treenode/utils/radix.py +0 -61
- treenode/views.py +0 -184
- {django_fast_treenode-2.1.4.dist-info → django_fast_treenode-3.0.0.dist-info/licenses}/LICENSE +0 -0
- {django_fast_treenode-2.1.4.dist-info → django_fast_treenode-3.0.0.dist-info}/top_level.txt +0 -0
- /treenode/static/{treenode → css}/.gitkeep +0 -0
- /treenode/static/{treenode/css → js}/.gitkeep +0 -0
@@ -0,0 +1,167 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode TaskQuery manager
|
4
|
+
|
5
|
+
Version: 3.0.0
|
6
|
+
Author: Timur Kady
|
7
|
+
Email: timurkady@yandex.com
|
8
|
+
"""
|
9
|
+
|
10
|
+
from django.db import connection
|
11
|
+
|
12
|
+
from ..utils.db import TreePathCompiler
|
13
|
+
|
14
|
+
'''
|
15
|
+
try:
|
16
|
+
profile
|
17
|
+
except NameError:
|
18
|
+
def profile(func):
|
19
|
+
"""Profile."""
|
20
|
+
return func
|
21
|
+
'''
|
22
|
+
|
23
|
+
|
24
|
+
class TreeTaskQueue:
|
25
|
+
"""TreeTaskQueue Class."""
|
26
|
+
|
27
|
+
def __init__(self, model):
|
28
|
+
"""Init the task query."""
|
29
|
+
self.model = model
|
30
|
+
self.queue = []
|
31
|
+
|
32
|
+
def add(self, mode, parent_id):
|
33
|
+
"""Add task to the query."""
|
34
|
+
self.queue.append({"mode": mode, "parent_id": parent_id})
|
35
|
+
|
36
|
+
def run(self):
|
37
|
+
"""Run task queue."""
|
38
|
+
if len(self.queue) == 0:
|
39
|
+
return
|
40
|
+
try:
|
41
|
+
optimized = self._optimize()
|
42
|
+
for task in optimized:
|
43
|
+
if task["mode"] == "update":
|
44
|
+
parent_id = task["parent_id"]
|
45
|
+
TreePathCompiler.update_path(
|
46
|
+
model=self.model,
|
47
|
+
parent_id=parent_id
|
48
|
+
)
|
49
|
+
finally:
|
50
|
+
self.queue.clear()
|
51
|
+
self._running = False
|
52
|
+
|
53
|
+
# @profile
|
54
|
+
def _optimize(self):
|
55
|
+
"""Return optimized task queue (ID-only logic)."""
|
56
|
+
result_set = set()
|
57
|
+
id_set = set()
|
58
|
+
|
59
|
+
for task in self.queue:
|
60
|
+
if task["mode"] == "update":
|
61
|
+
parent_id = task["parent_id"]
|
62
|
+
if parent_id is None:
|
63
|
+
# If we are already updating the entire tree, then
|
64
|
+
# the remaining tasks are meaningless # noqa: D501
|
65
|
+
return [{"mode": "update", "parent_id": None}]
|
66
|
+
else:
|
67
|
+
id_set.add(parent_id)
|
68
|
+
|
69
|
+
id_list = list(id_set)
|
70
|
+
|
71
|
+
while id_list:
|
72
|
+
current = id_list.pop(0)
|
73
|
+
merged = False
|
74
|
+
for other in id_list[:]:
|
75
|
+
ancestor = self._get_common_ancestor(current, other)
|
76
|
+
if ancestor is not None:
|
77
|
+
# If the common ancestor is the root, then we update
|
78
|
+
# the entire tree
|
79
|
+
if ancestor in self._get_root_ids():
|
80
|
+
return [{"mode": "update", "parent_id": None}]
|
81
|
+
if ancestor not in id_set:
|
82
|
+
id_list.append(ancestor)
|
83
|
+
id_set.add(ancestor)
|
84
|
+
id_list.remove(other)
|
85
|
+
merged = True
|
86
|
+
break
|
87
|
+
if not merged:
|
88
|
+
result_set.add(current)
|
89
|
+
|
90
|
+
return [{"mode": "update", "parent_id": pk} for pk in sorted(result_set)] # noqa: D501
|
91
|
+
|
92
|
+
def _get_root_ids(self):
|
93
|
+
"""Return root node IDs."""
|
94
|
+
with connection.cursor() as cursor:
|
95
|
+
cursor.execute(
|
96
|
+
f"SELECT id FROM {self.model._meta.db_table} WHERE parent_id IS NULL") # noqa: D501
|
97
|
+
return [row[0] for row in cursor.fetchall()]
|
98
|
+
|
99
|
+
def _get_parent_id(self, node_id):
|
100
|
+
"""Return parent ID for a given node."""
|
101
|
+
with connection.cursor() as cursor:
|
102
|
+
cursor.execute(
|
103
|
+
f"SELECT parent_id FROM {self.model._meta.db_table} WHERE id = %s", [node_id]) # noqa: D501
|
104
|
+
row = cursor.fetchone()
|
105
|
+
return row[0] if row else None
|
106
|
+
|
107
|
+
'''
|
108
|
+
def _get_ancestor_path(self, node_id):
|
109
|
+
"""Return list of ancestor IDs including the node itself."""
|
110
|
+
path = []
|
111
|
+
while node_id is not None:
|
112
|
+
path.append(node_id)
|
113
|
+
node_id = self._get_parent_id(node_id)
|
114
|
+
return path[::-1] # root to leaf
|
115
|
+
'''
|
116
|
+
|
117
|
+
# @profile
|
118
|
+
def _get_ancestor_path(self, node_id):
|
119
|
+
"""Return list of ancestor IDs including the node itself, using recursive SQL.""" # noqa: D501
|
120
|
+
table = self.model._meta.db_table
|
121
|
+
|
122
|
+
sql = f"""
|
123
|
+
WITH RECURSIVE ancestor_cte AS (
|
124
|
+
SELECT id, parent_id, 0 AS depth
|
125
|
+
FROM {table}
|
126
|
+
WHERE id = %s
|
127
|
+
|
128
|
+
UNION ALL
|
129
|
+
|
130
|
+
SELECT t.id, t.parent_id, a.depth + 1
|
131
|
+
FROM {table} t
|
132
|
+
JOIN ancestor_cte a ON t.id = a.parent_id
|
133
|
+
)
|
134
|
+
SELECT id FROM ancestor_cte ORDER BY depth DESC
|
135
|
+
"""
|
136
|
+
|
137
|
+
with connection.cursor() as cursor:
|
138
|
+
cursor.execute(sql, [node_id])
|
139
|
+
rows = cursor.fetchall()
|
140
|
+
|
141
|
+
return [row[0] for row in rows]
|
142
|
+
|
143
|
+
# @profile
|
144
|
+
def _get_common_ancestor(self, id1, id2):
|
145
|
+
"""Return common ancestor ID between two nodes."""
|
146
|
+
path1 = self._get_ancestor_path(id1)
|
147
|
+
path2 = self._get_ancestor_path(id2)
|
148
|
+
common = None
|
149
|
+
for a, b in zip(path1, path2):
|
150
|
+
if a == b:
|
151
|
+
common = a
|
152
|
+
else:
|
153
|
+
break
|
154
|
+
return common
|
155
|
+
|
156
|
+
|
157
|
+
class TreeTaskManager:
|
158
|
+
"""Handle to TreeTaskQueue."""
|
159
|
+
|
160
|
+
def __get__(self, instance, owner):
|
161
|
+
"""Get query for instance."""
|
162
|
+
if not hasattr(owner, "_task_queue"):
|
163
|
+
owner._task_queue = TreeTaskQueue(owner)
|
164
|
+
return owner._task_queue
|
165
|
+
|
166
|
+
|
167
|
+
# The End
|
treenode/models/__init__.py
CHANGED
@@ -1,8 +1,11 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
|
-
|
2
|
+
"""
|
3
|
+
The TreeNode Model
|
3
4
|
|
5
|
+
Version: 3.0.0
|
6
|
+
Author: Timur Kady
|
7
|
+
Email: timurkady@yandex.com
|
8
|
+
"""
|
9
|
+
from .models import TreeNodeModel
|
4
10
|
|
5
|
-
__all__ = ["TreeNodeModel"
|
6
|
-
|
7
|
-
|
8
|
-
# The End
|
11
|
+
__all__ = ["TreeNodeModel"]
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeeNodeModel Class Decorators
|
4
|
+
|
5
|
+
- Decorator `@cached_method` for caching method results.
|
6
|
+
|
7
|
+
Version: 3.0.0
|
8
|
+
Author: Timur Kady
|
9
|
+
Email: timurkady@yandex.com
|
10
|
+
"""
|
11
|
+
|
12
|
+
|
13
|
+
_TIMEOUT = 10
|
14
|
+
_INTERVAL = 0.2
|
15
|
+
|
16
|
+
_UNSET = object()
|
17
|
+
|
18
|
+
|
19
|
+
class classproperty(object):
|
20
|
+
"""Classproperty class."""
|
21
|
+
|
22
|
+
def __init__(self, getter):
|
23
|
+
"""Init."""
|
24
|
+
self.getter = getter
|
25
|
+
|
26
|
+
def __get__(self, instance, owner):
|
27
|
+
"""Get."""
|
28
|
+
return self.getter(owner)
|
29
|
+
|
30
|
+
|
31
|
+
def lazy_property(source, default=_UNSET):
|
32
|
+
"""
|
33
|
+
Декоратор ленивого свойства, которое берёт значение из другого поля.
|
34
|
+
|
35
|
+
source — имя поля (например, 'parent_id')
|
36
|
+
default — значение по умолчанию. Если не задан, берётся self.source
|
37
|
+
"""
|
38
|
+
def decorator(func):
|
39
|
+
def getter(self):
|
40
|
+
attr_name = '_' + func.__name__
|
41
|
+
if not hasattr(self, attr_name):
|
42
|
+
if default is _UNSET:
|
43
|
+
value = getattr(self, source)
|
44
|
+
else:
|
45
|
+
value = default
|
46
|
+
setattr(self, attr_name, value)
|
47
|
+
return getattr(self, attr_name)
|
48
|
+
|
49
|
+
def setter(self, value):
|
50
|
+
attr_name = '_' + func.__name__
|
51
|
+
setattr(self, attr_name, value)
|
52
|
+
|
53
|
+
return property(getter, setter)
|
54
|
+
return decorator
|
treenode/models/factory.py
CHANGED
@@ -1,81 +1,57 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
"""
|
3
|
-
TreeNode Factory
|
3
|
+
TreeNode Factory
|
4
4
|
|
5
|
-
This module provides a metaclass
|
6
|
-
a
|
5
|
+
This module provides a metaclass that automatically associates a model with
|
6
|
+
a service table and creates a set of indexes for the database
|
7
7
|
|
8
8
|
Features:
|
9
|
-
-
|
10
|
-
-
|
11
|
-
- Facilitates the management of hierarchical relationships.
|
9
|
+
- Dynamically creates and assigns a service model.
|
10
|
+
- Facilitates the formation of indexes taking into account the DB vendor.
|
12
11
|
|
13
|
-
Version:
|
12
|
+
Version: 3.0.0
|
14
13
|
Author: Timur Kady
|
15
14
|
Email: timurkady@yandex.com
|
16
15
|
"""
|
17
16
|
|
18
17
|
|
19
|
-
import
|
20
|
-
from django.db import
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
"child": models.ForeignKey(
|
59
|
-
cls._meta.model,
|
60
|
-
related_name="parents_set",
|
61
|
-
on_delete=models.CASCADE,
|
62
|
-
),
|
63
|
-
|
64
|
-
"node": models.OneToOneField(
|
65
|
-
cls._meta.model,
|
66
|
-
related_name="tn_closure",
|
67
|
-
on_delete=models.CASCADE,
|
68
|
-
null=True,
|
69
|
-
blank=True,
|
70
|
-
),
|
71
|
-
|
72
|
-
"__module__": cls.__module__
|
73
|
-
}
|
74
|
-
|
75
|
-
closure_model = type(closure_name, (ClosureModel,), fields)
|
76
|
-
setattr(sys.modules[cls.__module__], closure_name, closure_model)
|
77
|
-
|
78
|
-
cls.closure_model = closure_model
|
79
|
-
|
18
|
+
from django.db import models, connection
|
19
|
+
from django.db.models import Func, F
|
20
|
+
|
21
|
+
|
22
|
+
class TreeNodeModelBase(models.base.ModelBase):
|
23
|
+
"""Base Class for TreeNodeModel."""
|
24
|
+
|
25
|
+
def __new__(mcls, name, bases, attrs, **kwargs):
|
26
|
+
"""Create a New Class."""
|
27
|
+
new_class = super().__new__(mcls, name, bases, attrs, **kwargs)
|
28
|
+
if not new_class._meta.abstract:
|
29
|
+
class_name = name.lower()
|
30
|
+
# Create an index with the desired name
|
31
|
+
"""Set DB Indexes with unique names per model."""
|
32
|
+
vendor = connection.vendor
|
33
|
+
indexes = []
|
34
|
+
|
35
|
+
if vendor == 'postgresql':
|
36
|
+
indexes.append(models.Index(
|
37
|
+
fields=['_path'],
|
38
|
+
name=f'idx_{class_name}_path_ops',
|
39
|
+
opclasses=['text_pattern_ops']
|
40
|
+
))
|
41
|
+
elif vendor in {'mysql'}:
|
42
|
+
indexes.append(models.Index(
|
43
|
+
Func(F('_path'), function='md5'),
|
44
|
+
name=f'idx_{class_name}_path_hash'
|
45
|
+
))
|
46
|
+
else:
|
47
|
+
indexes.append(models.Index(
|
48
|
+
fields=['_path'],
|
49
|
+
name=f'idx_{class_name}_path'
|
50
|
+
))
|
51
|
+
|
52
|
+
# Update the list of indexes
|
53
|
+
new_class._meta.indexes += indexes
|
54
|
+
|
55
|
+
return new_class
|
80
56
|
|
81
57
|
# The End
|
@@ -10,13 +10,14 @@ from .properties import TreeNodePropertiesMixin
|
|
10
10
|
from .roots import TreeNodeRootsMixin
|
11
11
|
from .siblings import TreeNodeSiblingsMixin
|
12
12
|
from .tree import TreeNodeTreeMixin
|
13
|
+
from .update import RawSQLMixin
|
13
14
|
|
14
15
|
|
15
16
|
__all__ = [
|
16
17
|
"TreeNodeAncestorsMixin", "TreeNodeChildrenMixin", "TreeNodeFamilyMixin",
|
17
18
|
"TreeNodeDescendantsMixin", "TreeNodeLogicalMixin", "TreeNodeNodeMixin",
|
18
19
|
"TreeNodePropertiesMixin", "TreeNodeRootsMixin", "TreeNodeSiblingsMixin",
|
19
|
-
"TreeNodeTreeMixin"
|
20
|
+
"TreeNodeTreeMixin", "RawSQLMixin"
|
20
21
|
]
|
21
22
|
|
22
23
|
|
@@ -2,14 +2,13 @@
|
|
2
2
|
"""
|
3
3
|
TreeNode Ancestors Mixin
|
4
4
|
|
5
|
-
Version:
|
5
|
+
Version: 3.0.0
|
6
6
|
Author: Timur Kady
|
7
7
|
Email: timurkady@yandex.com
|
8
8
|
"""
|
9
9
|
|
10
10
|
from django.db import models
|
11
|
-
from
|
12
|
-
from ...cache import treenode_cache, cached_method
|
11
|
+
from ...cache import cached_method
|
13
12
|
|
14
13
|
|
15
14
|
class TreeNodeAncestorsMixin(models.Model):
|
@@ -20,29 +19,54 @@ class TreeNodeAncestorsMixin(models.Model):
|
|
20
19
|
|
21
20
|
abstract = True
|
22
21
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
if depth:
|
28
|
-
options.update({'depth__lte': depth})
|
29
|
-
|
30
|
-
return self.closure_model.objects\
|
31
|
-
.filter(**options)\
|
32
|
-
.order_by('-depth')
|
22
|
+
def get_ancestors_queryset(self, include_self=True):
|
23
|
+
"""Get all ancestors of a node."""
|
24
|
+
pks = self.query("ancestors", include_self)
|
25
|
+
return self._meta.model.objects.filter(pk__in=pks)
|
33
26
|
|
34
27
|
@cached_method
|
35
28
|
def get_ancestors_pks(self, include_self=True, depth=None):
|
36
29
|
"""Get the ancestors pks list."""
|
37
|
-
return self.
|
38
|
-
.values_list('id', flat=True)
|
30
|
+
return self.query("ancestors", include_self)
|
39
31
|
|
40
|
-
|
41
|
-
|
42
|
-
|
32
|
+
@cached_method
|
33
|
+
def get_ancestors(self, include_self=True):
|
34
|
+
"""Get a list of all ancestors of a node."""
|
35
|
+
node = self if include_self else self.parent
|
36
|
+
ancestors = []
|
37
|
+
while node:
|
38
|
+
ancestors.append(node)
|
39
|
+
node = node.parent
|
40
|
+
return ancestors[::-1]
|
43
41
|
|
44
|
-
|
42
|
+
@cached_method
|
43
|
+
def get_ancestors_count(self, include_self=True):
|
45
44
|
"""Get the ancestors count."""
|
46
|
-
return
|
45
|
+
return self.query(
|
46
|
+
objects="ancestors",
|
47
|
+
include_self=include_self,
|
48
|
+
mode='count'
|
49
|
+
)
|
50
|
+
|
51
|
+
def get_common_ancestor(self, target):
|
52
|
+
"""Find lowest common ancestor between self and other node."""
|
53
|
+
if self._path == target._path:
|
54
|
+
return self
|
55
|
+
|
56
|
+
self_path_pks = self.query("ancestors")
|
57
|
+
target_path_pks = target.query("ancestors")
|
58
|
+
common = []
|
59
|
+
|
60
|
+
for a, b in zip(self_path_pks, target_path_pks):
|
61
|
+
if a == b:
|
62
|
+
common.append(a)
|
63
|
+
else:
|
64
|
+
break
|
65
|
+
|
66
|
+
if not common:
|
67
|
+
return None
|
68
|
+
|
69
|
+
ancestor_id = common[-1]
|
70
|
+
return self._meta.model.objects.get(pk=ancestor_id)
|
47
71
|
|
48
72
|
# The End
|
@@ -2,13 +2,23 @@
|
|
2
2
|
"""
|
3
3
|
TreeNode Children Mixin
|
4
4
|
|
5
|
-
Version:
|
5
|
+
Version: 3.0.0
|
6
6
|
Author: Timur Kady
|
7
7
|
Email: timurkady@yandex.com
|
8
8
|
"""
|
9
9
|
|
10
10
|
from django.db import models
|
11
|
-
from
|
11
|
+
from ...cache import cached_method
|
12
|
+
|
13
|
+
|
14
|
+
'''
|
15
|
+
try:
|
16
|
+
profile
|
17
|
+
except NameError:
|
18
|
+
def profile(func):
|
19
|
+
"""Profile."""
|
20
|
+
return func
|
21
|
+
'''
|
12
22
|
|
13
23
|
|
14
24
|
class TreeNodeChildrenMixin(models.Model):
|
@@ -36,47 +46,44 @@ class TreeNodeChildrenMixin(models.Model):
|
|
36
46
|
Returns:
|
37
47
|
The created node object. It will be save()d by this method.
|
38
48
|
"""
|
39
|
-
if isinstance(position, int):
|
40
|
-
priority = position
|
41
|
-
parent = self
|
42
|
-
else:
|
43
|
-
if position not in ['first-child', 'last-child', 'sorted-child']:
|
44
|
-
raise ValueError(f"Invalid position format: {position}")
|
45
|
-
parent, priority = self._meta.model._get_place(self, position)
|
46
|
-
|
47
49
|
instance = kwargs.get("instance")
|
48
50
|
if instance is None:
|
49
51
|
instance = self._meta.model(**kwargs)
|
50
|
-
instance.tn_parent = parent
|
51
|
-
instance.tn_priority = priority
|
52
|
-
instance.save()
|
53
|
-
return instance
|
54
52
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
53
|
+
parent, priority = self._meta.model._get_place(self, position)
|
54
|
+
|
55
|
+
instance.parent = self
|
56
|
+
instance.priority = priority
|
57
|
+
instance.save()
|
59
58
|
|
60
|
-
@cached_method
|
61
59
|
def get_children_queryset(self):
|
62
|
-
"""Get the children queryset
|
63
|
-
|
64
|
-
return self._meta.model.objects.filter(tn_parent__pk=self.id)
|
60
|
+
"""Get the children queryset."""
|
61
|
+
return self._meta.model.objects.filter(parent_id=self.id)
|
65
62
|
|
63
|
+
@cached_method
|
66
64
|
def get_children(self):
|
67
65
|
"""Get a list containing all children."""
|
68
|
-
|
66
|
+
queryset = self._meta.model.objects.filter(parent_id=self.id)
|
67
|
+
return list(queryset)
|
69
68
|
|
69
|
+
@cached_method
|
70
|
+
def get_children_pks(self):
|
71
|
+
"""Get the children pks list."""
|
72
|
+
return self.query("children")
|
73
|
+
|
74
|
+
@cached_method
|
70
75
|
def get_children_count(self):
|
71
76
|
"""Get the children count."""
|
72
|
-
return
|
77
|
+
return self.query(objects="children", mode='count')
|
73
78
|
|
79
|
+
@cached_method
|
74
80
|
def get_first_child(self):
|
75
81
|
"""Get the first child node or None if it has no children."""
|
76
|
-
return self.get_children_queryset().first()
|
82
|
+
return self.get_children_queryset().first()
|
77
83
|
|
84
|
+
@cached_method
|
78
85
|
def get_last_child(self):
|
79
86
|
"""Get the last child node or None if it has no children."""
|
80
|
-
return self.get_children_queryset().last()
|
87
|
+
return self.get_children_queryset().last()
|
81
88
|
|
82
89
|
# The End
|
@@ -2,15 +2,23 @@
|
|
2
2
|
"""
|
3
3
|
TreeNode Descendants Mixin
|
4
4
|
|
5
|
-
Version:
|
5
|
+
Version: 3.0.0
|
6
6
|
Author: Timur Kady
|
7
7
|
Email: timurkady@yandex.com
|
8
8
|
"""
|
9
9
|
|
10
10
|
from django.db import models
|
11
|
-
from
|
11
|
+
from ...cache import cached_method
|
12
12
|
|
13
|
-
|
13
|
+
|
14
|
+
'''
|
15
|
+
try:
|
16
|
+
profile
|
17
|
+
except NameError:
|
18
|
+
def profile(func):
|
19
|
+
"""Profile."""
|
20
|
+
return func
|
21
|
+
'''
|
14
22
|
|
15
23
|
|
16
24
|
class TreeNodeDescendantsMixin(models.Model):
|
@@ -21,40 +29,43 @@ class TreeNodeDescendantsMixin(models.Model):
|
|
21
29
|
|
22
30
|
abstract = True
|
23
31
|
|
24
|
-
@cached_method
|
25
32
|
def get_descendants_queryset(self, include_self=False, depth=None):
|
26
33
|
"""Get the descendants queryset."""
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
).filter(min_depth__isnull=False)
|
35
|
-
|
36
|
-
if depth is not None:
|
37
|
-
queryset = queryset.filter(min_depth__lte=depth)
|
34
|
+
path = self.get_order() # calls refresh and gets the current _path
|
35
|
+
from_path = path + '.'
|
36
|
+
to_path = path + '/'
|
37
|
+
options = {'_path__gte': from_path, '_path__lt': to_path}
|
38
|
+
if depth:
|
39
|
+
options["_depth__lt"] = depth
|
40
|
+
queryset = self._meta.model.objects.filter(**options)
|
38
41
|
|
39
|
-
# add self if needed
|
40
42
|
if include_self:
|
41
|
-
|
42
|
-
|
43
|
-
|
43
|
+
return self._meta.model.objects.filter(pk=self.pk) | queryset
|
44
|
+
else:
|
45
|
+
return queryset
|
44
46
|
|
45
47
|
@cached_method
|
46
48
|
def get_descendants_pks(self, include_self=False, depth=None):
|
47
49
|
"""Get the descendants pks list."""
|
48
|
-
return self.
|
49
|
-
.values_list("id", flat=True)
|
50
|
+
return self.query("descendants", include_self)
|
50
51
|
|
52
|
+
# @profile
|
53
|
+
@cached_method
|
51
54
|
def get_descendants(self, include_self=False, depth=None):
|
52
55
|
"""Get a list containing all descendants."""
|
56
|
+
# descendants_pks = self.query("descendants", include_self)
|
57
|
+
# queryset = self._meta.model.objects.filter(pk__in=descendants_pks)
|
53
58
|
queryset = self.get_descendants_queryset(include_self, depth)
|
54
59
|
return list(queryset)
|
55
60
|
|
61
|
+
@cached_method
|
56
62
|
def get_descendants_count(self, include_self=False, depth=None):
|
57
63
|
"""Get the descendants count."""
|
58
|
-
return
|
64
|
+
return self.query(
|
65
|
+
objects="descendants",
|
66
|
+
include_self=include_self,
|
67
|
+
mode='count'
|
68
|
+
)
|
69
|
+
|
59
70
|
|
60
71
|
# The End
|