django-fast-treenode 1.1.3__py3-none-any.whl → 2.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-1.1.3.dist-info → django_fast_treenode-2.0.0.dist-info}/METADATA +156 -46
- django_fast_treenode-2.0.0.dist-info/RECORD +41 -0
- {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.0.dist-info}/WHEEL +1 -1
- treenode/__init__.py +0 -7
- treenode/admin.py +327 -82
- treenode/apps.py +20 -3
- treenode/cache.py +231 -0
- treenode/docs/Documentation +130 -54
- treenode/forms.py +75 -19
- treenode/managers.py +260 -48
- treenode/models/__init__.py +7 -0
- treenode/models/classproperty.py +24 -0
- treenode/models/closure.py +168 -0
- treenode/models/factory.py +71 -0
- treenode/models/proxy.py +650 -0
- treenode/static/treenode/css/tree_widget.css +62 -0
- treenode/static/treenode/css/treenode_admin.css +106 -0
- treenode/static/treenode/js/tree_widget.js +161 -0
- treenode/static/treenode/js/treenode_admin.js +171 -0
- treenode/templates/admin/export_success.html +26 -0
- treenode/templates/admin/tree_node_changelist.html +11 -0
- treenode/templates/admin/tree_node_export.html +27 -0
- treenode/templates/admin/tree_node_import.html +27 -0
- treenode/templates/widgets/tree_widget.css +23 -0
- treenode/templates/widgets/tree_widget.html +21 -0
- treenode/urls.py +34 -0
- treenode/utils/__init__.py +4 -0
- treenode/utils/base36.py +35 -0
- treenode/utils/exporter.py +141 -0
- treenode/utils/importer.py +296 -0
- treenode/version.py +11 -1
- treenode/views.py +102 -2
- treenode/widgets.py +49 -27
- django_fast_treenode-1.1.3.dist-info/RECORD +0 -33
- treenode/compat.py +0 -8
- treenode/factory.py +0 -68
- treenode/models.py +0 -668
- treenode/static/select2tree/.gitkeep +0 -1
- treenode/static/select2tree/select2tree.css +0 -176
- treenode/static/select2tree/select2tree.js +0 -181
- treenode/static/treenode/css/treenode.css +0 -85
- treenode/static/treenode/js/treenode.js +0 -201
- treenode/templates/widgets/.gitkeep +0 -1
- treenode/templates/widgets/attrs.html +0 -7
- treenode/templates/widgets/options.html +0 -1
- treenode/templates/widgets/select2tree.html +0 -22
- treenode/tests.py +0 -3
- {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.0.dist-info}/LICENSE +0 -0
- {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.0.dist-info}/top_level.txt +0 -0
- /treenode/{docs → templates/admin}/.gitkeep +0 -0
treenode/managers.py
CHANGED
@@ -1,69 +1,281 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
"""
|
3
|
-
|
3
|
+
Managers and QuerySets
|
4
4
|
|
5
|
+
This module defines custom managers and query sets for the TreeNode model.
|
6
|
+
It includes optimized bulk operations for handling hierarchical data
|
7
|
+
using the Closure Table approach.
|
8
|
+
|
9
|
+
Features:
|
10
|
+
- `ClosureQuerySet` and `ClosureModelManager` for managing closure records.
|
11
|
+
- `TreeNodeQuerySet` and `TreeNodeModelManager` for adjacency model operations.
|
12
|
+
- Optimized `bulk_create` and `bulk_update` methods with atomic transactions.
|
13
|
+
|
14
|
+
Version: 2.0.0
|
15
|
+
Author: Timur Kady
|
16
|
+
Email: timurkady@yandex.com
|
5
17
|
"""
|
6
18
|
|
7
|
-
|
8
|
-
from django.db
|
19
|
+
|
20
|
+
from django.db import models, transaction
|
21
|
+
|
22
|
+
|
23
|
+
# ----------------------------------------------------------------------------
|
24
|
+
# Closere Model
|
25
|
+
# ----------------------------------------------------------------------------
|
26
|
+
|
27
|
+
|
28
|
+
class ClosureQuerySet(models.QuerySet):
|
29
|
+
"""QuerySet для ClosureModel."""
|
30
|
+
|
31
|
+
@transaction.atomic
|
32
|
+
def bulk_create(self, objs, batch_size=1000):
|
33
|
+
"""
|
34
|
+
Insert new nodes in bulk.
|
35
|
+
|
36
|
+
For newly created AdjacencyModel objects:
|
37
|
+
1. Create self-referential records (parent=child, depth=0).
|
38
|
+
2. Build ancestors for each node based on tn_parent.
|
39
|
+
"""
|
40
|
+
# --- 1. Create self-referential closure records for each object.
|
41
|
+
self_closure_records = []
|
42
|
+
for item in objs:
|
43
|
+
self_closure_records.append(
|
44
|
+
self.model(parent=item, child=item, depth=0)
|
45
|
+
)
|
46
|
+
super().bulk_create(self_closure_records, batch_size=batch_size)
|
47
|
+
|
48
|
+
# --- 2. Preparing closure for parents
|
49
|
+
parent_ids = {node.tn_parent.pk for node in objs if node.tn_parent}
|
50
|
+
parent_closures = list(
|
51
|
+
self.filter(child__in=parent_ids)
|
52
|
+
.values('child', 'parent', 'depth')
|
53
|
+
)
|
54
|
+
# Pack them into a dict
|
55
|
+
parent_closures_dict = {}
|
56
|
+
for pc in parent_closures:
|
57
|
+
parent_id = pc['child']
|
58
|
+
parent_closures_dict.setdefault(parent_id, []).append(pc)
|
59
|
+
|
60
|
+
# --- 3. Get closure records for the objs themselves
|
61
|
+
node_ids = [node.pk for node in objs]
|
62
|
+
child_closures = list(
|
63
|
+
self.filter(parent__in=node_ids)
|
64
|
+
.values('parent', 'child', 'depth')
|
65
|
+
)
|
66
|
+
child_closures_dict = {}
|
67
|
+
for cc in child_closures:
|
68
|
+
parent_id = cc['parent']
|
69
|
+
child_closures_dict.setdefault(parent_id, []).append(cc)
|
70
|
+
|
71
|
+
# --- 4. Collecting new links
|
72
|
+
new_records = []
|
73
|
+
for node in objs:
|
74
|
+
if not node.tn_parent:
|
75
|
+
continue
|
76
|
+
# parent closure
|
77
|
+
parents = parent_closures_dict.get(node.tn_parent.pk, [])
|
78
|
+
# closure of descendants
|
79
|
+
# (often this is only the node itself, depth=0, but there can be
|
80
|
+
# nested ones)
|
81
|
+
children = child_closures_dict.get(node.pk, [])
|
82
|
+
# Combine parents x children
|
83
|
+
for p in parents:
|
84
|
+
for c in children:
|
85
|
+
new_records.append(self.model(
|
86
|
+
parent_id=p['parent'],
|
87
|
+
child_id=c['child'],
|
88
|
+
depth=p['depth'] + c['depth'] + 1
|
89
|
+
))
|
90
|
+
# --- 5. bulk_create
|
91
|
+
result = []
|
92
|
+
if new_records:
|
93
|
+
result = super().bulk_create(new_records, batch_size=batch_size)
|
94
|
+
self.model.clear_cache()
|
95
|
+
return result
|
96
|
+
|
97
|
+
@transaction.atomic
|
98
|
+
def bulk_update(self, objs, fields=None, batch_size=1000):
|
99
|
+
"""
|
100
|
+
Update the records in the closure table for the list of updated nodes.
|
101
|
+
|
102
|
+
For each node whose tn_parent has changed, the closure records
|
103
|
+
for its entire subtree are recalculated:
|
104
|
+
1. The ancestor chain of the new parent is selected.
|
105
|
+
2. The subtree (with closure records) of the updated node is selected.
|
106
|
+
3. For each combination (ancestor, descendant), a new depth is
|
107
|
+
calculated.
|
108
|
+
4. Old "dangling" records (those where the descendant has a link to
|
109
|
+
a non-subtree) are removed.
|
110
|
+
5. New records are inserted using the bulk_create method.
|
111
|
+
"""
|
112
|
+
if not objs:
|
113
|
+
return
|
114
|
+
|
115
|
+
# --- 1. We obtain chains of ancestors for new parents.
|
116
|
+
parent_ids = {node.tn_parent.pk for node in objs if node.tn_parent}
|
117
|
+
parent_closures = list(
|
118
|
+
self.filter(child__in=parent_ids).values(
|
119
|
+
'child',
|
120
|
+
'parent',
|
121
|
+
'depth'
|
122
|
+
)
|
123
|
+
)
|
124
|
+
# We collect in a dictionary: key is the parent ID (tn_parent),
|
125
|
+
# value is the list of records.
|
126
|
+
parent_closures_dict = {}
|
127
|
+
for pc in parent_closures:
|
128
|
+
parent_closures_dict.setdefault(pc['child'], []).append(pc)
|
129
|
+
|
130
|
+
# --- 2. Obtain closing records for the subtrees of the
|
131
|
+
# nodes being updated.
|
132
|
+
updated_ids = [node.pk for node in objs]
|
133
|
+
subtree_closures = list(self.filter(parent__in=updated_ids).values(
|
134
|
+
'parent',
|
135
|
+
'child',
|
136
|
+
'depth'
|
137
|
+
))
|
138
|
+
# Group by ID of the node being updated (i.e. by parent in the
|
139
|
+
# closing record)
|
140
|
+
subtree_closures_dict = {}
|
141
|
+
for sc in subtree_closures:
|
142
|
+
subtree_closures_dict.setdefault(sc['parent'], []).append(sc)
|
143
|
+
|
144
|
+
# --- 3. Construct new close records for each updated node with
|
145
|
+
# a new parent.
|
146
|
+
new_records = []
|
147
|
+
for node in objs:
|
148
|
+
# If the node has become root (tn_parent=None), then there are
|
149
|
+
# no additional connections with ancestors.
|
150
|
+
if not node.tn_parent:
|
151
|
+
continue
|
152
|
+
# From the closing chain of the new parent we get a list of its
|
153
|
+
# ancestors
|
154
|
+
p_closures = parent_closures_dict.get(node.tn_parent.pk, [])
|
155
|
+
# From the node subtree we get the closing records
|
156
|
+
s_closures = subtree_closures_dict.get(node.pk, [])
|
157
|
+
# If for some reason the subtree entries are missing, we will
|
158
|
+
# ensure that there is a custom entry.
|
159
|
+
if not s_closures:
|
160
|
+
s_closures = [{
|
161
|
+
'parent': node.pk,
|
162
|
+
'child': node.pk,
|
163
|
+
'depth': 0
|
164
|
+
}]
|
165
|
+
# Combine: for each ancestor of the new parent and for each
|
166
|
+
# descendant from the subtree
|
167
|
+
for p in p_closures:
|
168
|
+
for s in s_closures:
|
169
|
+
new_depth = p['depth'] + s['depth'] + 1
|
170
|
+
new_records.append(
|
171
|
+
self.model(
|
172
|
+
parent_id=p['parent'],
|
173
|
+
child_id=s['child'],
|
174
|
+
depth=new_depth
|
175
|
+
)
|
176
|
+
)
|
177
|
+
|
178
|
+
# --- 4. Remove old closing records so that there are no "tails".
|
179
|
+
# For each updated node, calculate a subset of IDs related to its
|
180
|
+
# subtree
|
181
|
+
for node in objs:
|
182
|
+
subtree_ids = set()
|
183
|
+
# Be sure to include the node itself (its self-link should
|
184
|
+
# already be there)
|
185
|
+
subtree_ids.add(node.pk)
|
186
|
+
for sc in subtree_closures_dict.get(node.pk, []):
|
187
|
+
subtree_ids.add(sc['child'])
|
188
|
+
# Remove records where the child belongs to the subtree, but the
|
189
|
+
# parent is not included in it.
|
190
|
+
self.filter(child_id__in=subtree_ids).exclude(
|
191
|
+
parent_id__in=subtree_ids).delete()
|
192
|
+
|
193
|
+
# --- 5. Insert new closing records in bulk.
|
194
|
+
if new_records:
|
195
|
+
super().bulk_create(new_records, batch_size=batch_size)
|
196
|
+
self.model.clear_cache()
|
197
|
+
return new_records
|
198
|
+
|
199
|
+
|
200
|
+
class ClosureModelManager(models.Manager):
|
201
|
+
"""ClosureModel Manager."""
|
202
|
+
|
203
|
+
def get_queryset(self):
|
204
|
+
"""get_queryset method."""
|
205
|
+
return ClosureQuerySet(self.model, using=self._db)
|
206
|
+
|
207
|
+
def bulk_create(self, objs, batch_size=1000):
|
208
|
+
"""Create objects in bulk."""
|
209
|
+
self.model.clear_cache()
|
210
|
+
return self.get_queryset().bulk_create(objs, batch_size=batch_size)
|
211
|
+
|
212
|
+
def bulk_update(self, objs, fields=None, batch_size=1000):
|
213
|
+
"""Move nodes in ClosureModel."""
|
214
|
+
self.model.clear_cache()
|
215
|
+
return self.get_queryset().bulk_update(
|
216
|
+
objs, fields, batch_size=batch_size
|
217
|
+
)
|
218
|
+
|
219
|
+
# ----------------------------------------------------------------------------
|
220
|
+
# TreeNode Model
|
221
|
+
# ----------------------------------------------------------------------------
|
9
222
|
|
10
223
|
|
11
224
|
class TreeNodeQuerySet(models.QuerySet):
|
12
|
-
"""
|
225
|
+
"""TreeNodeModel QuerySet."""
|
13
226
|
|
14
227
|
def __init__(self, model=None, query=None, using=None, hints=None):
|
228
|
+
"""Init."""
|
15
229
|
self.closure_model = model.closure_model
|
16
230
|
super().__init__(model, query, using, hints)
|
17
231
|
|
18
|
-
|
232
|
+
@transaction.atomic
|
233
|
+
def bulk_create(self, objs, batch_size=1000, ignore_conflicts=False):
|
234
|
+
"""
|
235
|
+
Bulk create.
|
236
|
+
|
237
|
+
Method of bulk creation objects with updating and processing of
|
238
|
+
the Closuse Model.
|
239
|
+
"""
|
240
|
+
# Regular bulk_create for TreeNodeModel
|
19
241
|
objs = super().bulk_create(objs, batch_size, ignore_conflicts)
|
242
|
+
# Call ClosureModel to insert closure records
|
243
|
+
self.closure_model.objects.bulk_create(objs, batch_size=batch_size)
|
244
|
+
# Возвращаем результат
|
245
|
+
self.model.clear_cache()
|
246
|
+
return self.filter(pk__in=[obj.pk for obj in objs])
|
20
247
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
248
|
+
@transaction.atomic
|
249
|
+
def bulk_update(self, objs, fields, batch_size=1000, **kwargs):
|
250
|
+
"""."""
|
251
|
+
closure_model = self.model.closure_model
|
252
|
+
if 'tn_parent' in fields:
|
253
|
+
# Попросим ClosureModel обработать move
|
254
|
+
closure_model.objects.bulk_update(objs)
|
255
|
+
result = super().bulk_update(objs, fields, batch_size, **kwargs)
|
256
|
+
closure_model.clear_cache()
|
257
|
+
return result
|
28
258
|
|
29
|
-
for node in objs:
|
30
|
-
qs = self.model.closure_model.objects.all()
|
31
|
-
parents = qs.filter(child=node.tn_parent).values('parent', 'depth')
|
32
|
-
children = qs.filter(parent=node).values('child', 'depth')
|
33
|
-
objects = [
|
34
|
-
self.model.closure_model(
|
35
|
-
parent_id=p['parent'],
|
36
|
-
child_id=c['child'],
|
37
|
-
depth=p['depth'] + c['depth'] + 1
|
38
|
-
)
|
39
|
-
for p in parents
|
40
|
-
for c in children
|
41
|
-
]
|
42
|
-
node._closure_model.objects.bulk_create(objects)
|
43
|
-
|
44
|
-
self.model._update_orders()
|
45
|
-
return objs
|
46
|
-
|
47
|
-
|
48
|
-
class TreeNodeManager(models.Manager):
|
49
|
-
"""TreeNode Manager Class"""
|
50
259
|
|
51
|
-
|
52
|
-
|
53
|
-
|
260
|
+
class TreeNodeModelManager(models.Manager):
|
261
|
+
"""TreeNodeModel Manager."""
|
262
|
+
|
263
|
+
def bulk_create(self, objs, batch_size=1000, ignore_conflicts=False):
|
54
264
|
"""
|
265
|
+
Bulk Create.
|
55
266
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
return
|
62
|
-
|
63
|
-
for ordering, pk in enumerate(pk_list)],
|
64
|
-
default=Value(len(pk_list)),
|
65
|
-
output_field=models.IntegerField(),
|
66
|
-
)
|
267
|
+
Override bulk_create for the adjacency model.
|
268
|
+
Here we first clear the cache, then delegate the creation via our
|
269
|
+
custom QuerySet.
|
270
|
+
"""
|
271
|
+
self.model.clear_cache()
|
272
|
+
return self.get_queryset().bulk_create(
|
273
|
+
objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts
|
67
274
|
)
|
68
275
|
|
69
|
-
|
276
|
+
def get_queryset(self):
|
277
|
+
"""Return a QuerySet that sorts by 'tn_parent' and 'tn_priority'."""
|
278
|
+
queryset = TreeNodeQuerySet(self.model, using=self._db)
|
279
|
+
return queryset.order_by('tn_parent', 'tn_priority')
|
280
|
+
|
281
|
+
# The End
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
Class Property Decorator
|
4
|
+
|
5
|
+
This module provides a `classproperty` decorator that allows defining
|
6
|
+
read-only class-level properties.
|
7
|
+
|
8
|
+
Features:
|
9
|
+
- Enables class-level properties similar to instance properties.
|
10
|
+
- Uses a custom descriptor for property-like behavior.
|
11
|
+
|
12
|
+
"""
|
13
|
+
|
14
|
+
|
15
|
+
class classproperty(object):
|
16
|
+
"""Classproperty class."""
|
17
|
+
|
18
|
+
def __init__(self, getter):
|
19
|
+
"""Init."""
|
20
|
+
self.getter = getter
|
21
|
+
|
22
|
+
def __get__(self, instance, owner):
|
23
|
+
"""Get."""
|
24
|
+
return self.getter(owner)
|
@@ -0,0 +1,168 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Closure Model
|
4
|
+
|
5
|
+
This module defines the Closure Table implementation for hierarchical
|
6
|
+
data storage in the TreeNode model. It supports efficient queries for
|
7
|
+
retrieving ancestors, descendants, breadcrumbs, and tree depth.
|
8
|
+
|
9
|
+
Features:
|
10
|
+
- Uses a Closure Table for efficient tree operations.
|
11
|
+
- Implements cached queries for improved performance.
|
12
|
+
- Provides bulk operations for inserting, moving, and deleting nodes.
|
13
|
+
|
14
|
+
Version: 2.0.0
|
15
|
+
Author: Timur Kady
|
16
|
+
Email: timurkady@yandex.com
|
17
|
+
"""
|
18
|
+
|
19
|
+
|
20
|
+
from django.db import models, transaction
|
21
|
+
|
22
|
+
from ..managers import ClosureModelManager
|
23
|
+
from ..cache import cached_method, treenode_cache
|
24
|
+
|
25
|
+
|
26
|
+
class ClosureModel(models.Model):
|
27
|
+
"""
|
28
|
+
Model for Closure Table.
|
29
|
+
|
30
|
+
Implements hierarchy storage using the Closure Table method.
|
31
|
+
"""
|
32
|
+
|
33
|
+
parent = models.ForeignKey(
|
34
|
+
'TreeNodeModel',
|
35
|
+
related_name='children_set',
|
36
|
+
on_delete=models.CASCADE,
|
37
|
+
)
|
38
|
+
|
39
|
+
child = models.ForeignKey(
|
40
|
+
'TreeNodeModel',
|
41
|
+
related_name='parents_set',
|
42
|
+
on_delete=models.CASCADE,
|
43
|
+
)
|
44
|
+
|
45
|
+
depth = models.PositiveIntegerField()
|
46
|
+
|
47
|
+
objects = ClosureModelManager()
|
48
|
+
|
49
|
+
class Meta:
|
50
|
+
"""Meta Class."""
|
51
|
+
|
52
|
+
abstract = True
|
53
|
+
unique_together = (("parent", "child"),)
|
54
|
+
indexes = [
|
55
|
+
models.Index(fields=["parent", "child"]),
|
56
|
+
]
|
57
|
+
|
58
|
+
def __str__(self):
|
59
|
+
"""Display information about a class object."""
|
60
|
+
return f"{self.parent} — {self.child} — {self.depth}"
|
61
|
+
|
62
|
+
# ----------- Methods of working with tree structure ----------- #
|
63
|
+
|
64
|
+
@classmethod
|
65
|
+
def clear_cache(cls):
|
66
|
+
"""Clear cache for this model only."""
|
67
|
+
treenode_cache.invalidate(cls._meta.label)
|
68
|
+
|
69
|
+
@classmethod
|
70
|
+
@cached_method
|
71
|
+
def get_ancestors_queryset(cls, node, include_self=True, depth=None):
|
72
|
+
"""Get the ancestors QuerySet (ordered from root to parent)."""
|
73
|
+
filters = {"child__pk": node.pk}
|
74
|
+
if depth is not None:
|
75
|
+
filters["depth__lte"] = depth
|
76
|
+
qs = cls.objects.all().filter(**filters)
|
77
|
+
if not include_self:
|
78
|
+
qs = qs.exclude(parent=node, child=node)
|
79
|
+
return qs
|
80
|
+
|
81
|
+
@classmethod
|
82
|
+
@cached_method
|
83
|
+
def get_breadcrumbs(cls, node, attr=None):
|
84
|
+
"""Get the breadcrumbs to current node (included)."""
|
85
|
+
qs = cls.get_ancestors_queryset(
|
86
|
+
node, include_self=True).order_by('-depth')
|
87
|
+
if attr:
|
88
|
+
return [getattr(item.parent, attr)
|
89
|
+
if hasattr(item.parent, attr) else None for item in qs]
|
90
|
+
else:
|
91
|
+
return [item.parent.pk for item in qs]
|
92
|
+
|
93
|
+
@classmethod
|
94
|
+
@cached_method
|
95
|
+
def get_descendants_queryset(cls, node, include_self=False, depth=None):
|
96
|
+
"""Get the descendants QuerySet (ordered from parent to leaf)."""
|
97
|
+
filters = {"parent__pk": node.pk}
|
98
|
+
if depth is not None:
|
99
|
+
filters["depth__lte"] = depth
|
100
|
+
qs = cls.objects.all().filter(**filters).select_related('parent')
|
101
|
+
if not include_self:
|
102
|
+
qs = qs.exclude(parent=node, child=node)
|
103
|
+
return qs
|
104
|
+
|
105
|
+
@classmethod
|
106
|
+
@cached_method
|
107
|
+
def get_root(cls, node):
|
108
|
+
"""Get the root node for the given node."""
|
109
|
+
return cls.objects.filter(child=node).values_list(
|
110
|
+
"parent", flat=True).order_by('-depth').first()
|
111
|
+
|
112
|
+
@classmethod
|
113
|
+
@cached_method
|
114
|
+
def get_depth(cls, node):
|
115
|
+
"""Get the node depth (how deep the node is in the tree)."""
|
116
|
+
result = cls.objects.filter(child__pk=node.pk).aggregate(
|
117
|
+
models.Max("depth")
|
118
|
+
)["depth__max"]
|
119
|
+
return result if result is not None else 0
|
120
|
+
|
121
|
+
@classmethod
|
122
|
+
@cached_method
|
123
|
+
def get_level(cls, node):
|
124
|
+
"""Get the node level (starting from 1)."""
|
125
|
+
return cls.objects.filter(child__pk=node.pk).aggregate(
|
126
|
+
models.Max("depth"))["depth__max"] + 1
|
127
|
+
|
128
|
+
@classmethod
|
129
|
+
@cached_method
|
130
|
+
def get_path(cls, node, delimiter='.', format_str=""):
|
131
|
+
"""Return Materialized Path of node."""
|
132
|
+
str_ = "{%s}" % format_str
|
133
|
+
priorities = cls.get_breadcrumbs(node, attr='tn_priority')
|
134
|
+
path = delimiter.join([str_.format(p) for p in priorities])
|
135
|
+
return path
|
136
|
+
|
137
|
+
@classmethod
|
138
|
+
@transaction.atomic
|
139
|
+
def insert_node(cls, node):
|
140
|
+
"""Add a node to a Closure table."""
|
141
|
+
# Call bulk_create passing a single object
|
142
|
+
cls.objects.bulk_create([node], batch_size=1000)
|
143
|
+
# Clear cache
|
144
|
+
cls.clear_cache()
|
145
|
+
|
146
|
+
@classmethod
|
147
|
+
@transaction.atomic
|
148
|
+
def move_node(cls, node):
|
149
|
+
"""Move a node (and its subtree) to a new parent."""
|
150
|
+
# Call bulk_update passing a single object
|
151
|
+
cls.objects.bulk_update([node], batch_size=1000)
|
152
|
+
# Clear cache
|
153
|
+
cls.clear_cache()
|
154
|
+
|
155
|
+
@classmethod
|
156
|
+
@transaction.atomic
|
157
|
+
def delete_all(cls):
|
158
|
+
"""Clear the Closure Table."""
|
159
|
+
# Clear cache
|
160
|
+
cls.clear_cache()
|
161
|
+
cls.objects.all().delete()
|
162
|
+
|
163
|
+
def save(self, force_insert=False, *args, **kwargs):
|
164
|
+
"""Save method."""
|
165
|
+
super().save(force_insert, *args, **kwargs)
|
166
|
+
self._meta.model.clear_cache()
|
167
|
+
|
168
|
+
# The End
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Factory for Closure Table
|
4
|
+
|
5
|
+
This module provides a metaclass `TreeFactory` that automatically binds
|
6
|
+
a model to a Closure Table for hierarchical data storage.
|
7
|
+
|
8
|
+
Features:
|
9
|
+
- Ensures non-abstract, non-proxy models get a corresponding Closure Table.
|
10
|
+
- Dynamically creates and assigns a Closure Model for each TreeNodeModel.
|
11
|
+
- Facilitates the management of hierarchical relationships.
|
12
|
+
|
13
|
+
Version: 2.0.0
|
14
|
+
Author: Timur Kady
|
15
|
+
Email: timurkady@yandex.com
|
16
|
+
"""
|
17
|
+
|
18
|
+
|
19
|
+
import sys
|
20
|
+
from django.db import models
|
21
|
+
from .closure import ClosureModel # Используем готовый ClosureModel
|
22
|
+
|
23
|
+
|
24
|
+
class TreeFactory(models.base.ModelBase):
|
25
|
+
"""
|
26
|
+
Metaclass for binding a model to a Closure Table.
|
27
|
+
|
28
|
+
For each non-abstract, non-proxy, and "top" (without parents) model,
|
29
|
+
assigns the `ClosureModel` as the closure table.
|
30
|
+
"""
|
31
|
+
|
32
|
+
def __init__(cls, name, bases, dct):
|
33
|
+
"""Class initialization.
|
34
|
+
|
35
|
+
We check that the model:
|
36
|
+
- is not abstract
|
37
|
+
- is not a proxy
|
38
|
+
- is not a child
|
39
|
+
and only then assign the ClosureModel.
|
40
|
+
"""
|
41
|
+
super().__init__(name, bases, dct)
|
42
|
+
|
43
|
+
if (cls._meta.abstract or cls._meta.proxy or
|
44
|
+
cls._meta.get_parent_list()):
|
45
|
+
return
|
46
|
+
|
47
|
+
closure_name = f"{cls._meta.object_name}ClosureModel"
|
48
|
+
if getattr(cls, "closure_model", None) is not None:
|
49
|
+
return
|
50
|
+
|
51
|
+
fields = {
|
52
|
+
"parent": models.ForeignKey(
|
53
|
+
cls._meta.model,
|
54
|
+
related_name="children_set",
|
55
|
+
on_delete=models.CASCADE
|
56
|
+
),
|
57
|
+
|
58
|
+
"child": models.ForeignKey(
|
59
|
+
cls._meta.model,
|
60
|
+
related_name="parents_set",
|
61
|
+
on_delete=models.CASCADE,
|
62
|
+
),
|
63
|
+
|
64
|
+
"__module__": cls.__module__
|
65
|
+
}
|
66
|
+
closure_model = type(closure_name, (ClosureModel,), fields)
|
67
|
+
setattr(sys.modules[cls.__module__], closure_name, closure_model)
|
68
|
+
|
69
|
+
cls.closure_model = closure_model
|
70
|
+
|
71
|
+
# The End
|