django-fast-treenode 1.1.2__py3-none-any.whl → 2.0.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- {django_fast_treenode-1.1.2.dist-info → django_fast_treenode-2.0.0.dist-info}/METADATA +156 -44
- django_fast_treenode-2.0.0.dist-info/RECORD +41 -0
- {django_fast_treenode-1.1.2.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.2.dist-info/RECORD +0 -33
- treenode/compat.py +0 -8
- treenode/factory.py +0 -68
- treenode/models.py +0 -669
- treenode/static/select2tree/.gitkeep +0 -1
- treenode/static/select2tree/select2tree.css +0 -176
- treenode/static/select2tree/select2tree.js +0 -171
- 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.2.dist-info → django_fast_treenode-2.0.0.dist-info}/LICENSE +0 -0
- {django_fast_treenode-1.1.2.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
|