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
treenode/models/mixins/tree.py
CHANGED
@@ -2,17 +2,18 @@
|
|
2
2
|
"""
|
3
3
|
TreeNode Tree 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
|
import json
|
11
|
-
|
11
|
+
import inspect
|
12
|
+
from django.db import models, transaction, connection
|
12
13
|
from collections import OrderedDict
|
13
14
|
from django.core.serializers.json import DjangoJSONEncoder
|
14
15
|
|
15
|
-
from ...cache import
|
16
|
+
from ...cache import treenode_cache as cache
|
16
17
|
|
17
18
|
|
18
19
|
class TreeNodeTreeMixin(models.Model):
|
@@ -23,17 +24,41 @@ class TreeNodeTreeMixin(models.Model):
|
|
23
24
|
|
24
25
|
abstract = True
|
25
26
|
|
26
|
-
|
27
|
-
def dump_tree(cls, instance=None):
|
27
|
+
def clone_subtree(self, parent=None):
|
28
28
|
"""
|
29
|
-
|
29
|
+
Clone self and entire subtree under given parent.
|
30
30
|
|
31
|
-
|
31
|
+
Returns new root of the cloned subtree.
|
32
32
|
"""
|
33
|
-
|
33
|
+
model = self._meta.model
|
34
|
+
|
35
|
+
def _clone_node(node, parent):
|
36
|
+
# Copy all regular fields (including ForeignKey via attname)
|
37
|
+
data = {
|
38
|
+
f.attname: getattr(node, f.attname)
|
39
|
+
for f in node._meta.concrete_fields
|
40
|
+
if not f.primary_key and f.name != "_path"
|
41
|
+
}
|
42
|
+
data['parent'] = parent
|
43
|
+
|
44
|
+
# Create a new node
|
45
|
+
new_node = model.objects.create(**data)
|
46
|
+
|
47
|
+
# Copy ManyToMany fields
|
48
|
+
for m2m_field in node._meta.many_to_many:
|
49
|
+
related_ids = getattr(
|
50
|
+
node, m2m_field.name).values_list('pk', flat=True)
|
51
|
+
getattr(new_node, m2m_field.name).set(related_ids)
|
52
|
+
|
53
|
+
# Clone descendants recursively
|
54
|
+
for child in node.get_children():
|
55
|
+
_clone_node(child, new_node)
|
56
|
+
|
57
|
+
return new_node
|
58
|
+
|
59
|
+
return _clone_node(self, parent)
|
34
60
|
|
35
61
|
@classmethod
|
36
|
-
@cached_method
|
37
62
|
def get_tree(cls, instance=None):
|
38
63
|
"""
|
39
64
|
Return an n-dimensional dictionary representing the model tree.
|
@@ -44,39 +69,39 @@ class TreeNodeTreeMixin(models.Model):
|
|
44
69
|
"""
|
45
70
|
# If instance is passed, we get all its descendants (including itself)
|
46
71
|
if instance:
|
47
|
-
queryset = instance.get_descendants_queryset(include_self=True)
|
48
|
-
.annotate(depth=models.Max("parents_set__depth"))
|
72
|
+
queryset = instance.get_descendants_queryset(include_self=True)
|
49
73
|
else:
|
50
74
|
# Load all records at once
|
51
|
-
queryset = cls.objects.
|
75
|
+
queryset = cls.objects.get_queryset()
|
52
76
|
|
53
77
|
# Dictionary for quick access to nodes by id and list for iteration
|
54
78
|
nodes_by_id = {}
|
55
79
|
nodes_list = []
|
56
80
|
|
57
81
|
# Loop through all nodes using an iterator
|
58
|
-
for node in queryset.iterator(chunk_size=1000):
|
82
|
+
for node in queryset.order_by("_path").iterator(chunk_size=1000):
|
59
83
|
# Create a dictionary for the node.
|
60
|
-
# In Python 3.7+, the standard dict preserves insertion order.
|
61
|
-
# We'll stick to the order:
|
62
|
-
# id, 'tn_parent', 'tn_priority', 'level', then the rest of
|
63
|
-
# the fields. Сlose the dictionary with the fields 'level', 'path',
|
64
|
-
# 'children'
|
65
84
|
node_dict = OrderedDict()
|
66
85
|
node_dict['id'] = node.id
|
67
|
-
node_dict['
|
68
|
-
node_dict['
|
69
|
-
node_dict['
|
70
|
-
node_dict['path'] = node.
|
86
|
+
node_dict['parent'] = node.parent_id
|
87
|
+
node_dict['priority'] = node.priority
|
88
|
+
node_dict['depth'] = node.get_depth()
|
89
|
+
node_dict['path'] = node.get_order()
|
90
|
+
node_dict['children'] = [] # node.get_children_pks()
|
71
91
|
|
72
92
|
# Add the rest of the model fields.
|
73
93
|
# Iterate over all the fields obtained via _meta.get_fields()
|
74
|
-
|
94
|
+
fields = [
|
95
|
+
f for f in node._meta.get_fields()
|
96
|
+
if f.concrete and not f.auto_created and
|
97
|
+
not f.name.startswith('_')
|
98
|
+
]
|
99
|
+
|
100
|
+
for field in fields:
|
75
101
|
# Skipping fields that are already added or not required
|
76
102
|
# (e.g. tn_closure or virtual links)
|
77
103
|
if field.name in [
|
78
|
-
'id', '
|
79
|
-
'children']:
|
104
|
+
'id', 'parent', 'priority', '_depth', 'children']:
|
80
105
|
continue
|
81
106
|
|
82
107
|
try:
|
@@ -91,14 +116,6 @@ class TreeNodeTreeMixin(models.Model):
|
|
91
116
|
|
92
117
|
node_dict[field.name] = value
|
93
118
|
|
94
|
-
# Adding a materialized path
|
95
|
-
node_dict['path'] = None
|
96
|
-
# Adding a nesting level
|
97
|
-
node_dict['level'] = None
|
98
|
-
# We initialize the list of children, which we will then fill
|
99
|
-
# when assembling the tree
|
100
|
-
node_dict['children'] = []
|
101
|
-
|
102
119
|
# Save the node both in the list and in the dictionary by id
|
103
120
|
# for quick access
|
104
121
|
nodes_by_id[node.id] = node_dict
|
@@ -107,7 +124,7 @@ class TreeNodeTreeMixin(models.Model):
|
|
107
124
|
# Build a tree: assign each node a list of its children
|
108
125
|
tree = []
|
109
126
|
for node_dict in nodes_list:
|
110
|
-
parent_id = node_dict['
|
127
|
+
parent_id = node_dict['parent']
|
111
128
|
# If there is a parent and it is present in nodes_by_id, then
|
112
129
|
# add the current node to the list of its children
|
113
130
|
if parent_id and parent_id in nodes_by_id:
|
@@ -122,7 +139,7 @@ class TreeNodeTreeMixin(models.Model):
|
|
122
139
|
@classmethod
|
123
140
|
def get_tree_json(cls, instance=None):
|
124
141
|
"""Represent the tree as a JSON-compatible string."""
|
125
|
-
tree = cls.
|
142
|
+
tree = cls.get_tree(instance)
|
126
143
|
return DjangoJSONEncoder().encode(tree)
|
127
144
|
|
128
145
|
@classmethod
|
@@ -130,86 +147,132 @@ class TreeNodeTreeMixin(models.Model):
|
|
130
147
|
"""
|
131
148
|
Load a tree from a list of dictionaries.
|
132
149
|
|
133
|
-
|
134
|
-
|
135
|
-
|
150
|
+
- Ignores visual/path metadata ('path', 'depth', '_path', '_depth')
|
151
|
+
- Accepts 'id', 'name', 'priority', 'parent'
|
152
|
+
- Automatically recalculates _path, _depth, priority after loading
|
136
153
|
"""
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
Recursively traverse the tree and generate lists of nodes.
|
141
|
-
|
142
|
-
Each node in the list is a copy of the dictionary without
|
143
|
-
the service keys 'children', 'level', 'path'.
|
144
|
-
"""
|
154
|
+
# Step 1: Flatten nested tree into list of nodes with temporary
|
155
|
+
# parent references
|
156
|
+
def flatten_tree(tree, parent_temp_id=None):
|
145
157
|
flat = []
|
146
|
-
for node in
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
+
for node in tree:
|
159
|
+
node = node.copy()
|
160
|
+
children = node.pop('children', [])
|
161
|
+
node_id = node.pop('id', None)
|
162
|
+
node.pop('path', None)
|
163
|
+
node.pop('depth', None)
|
164
|
+
node.pop('parent', None)
|
165
|
+
flat_node = {
|
166
|
+
**node,
|
167
|
+
'_id': int(node_id) if node_id is not None else None,
|
168
|
+
'_parent': int(parent_temp_id) if parent_temp_id is not None else None # noqa: D501
|
169
|
+
}
|
170
|
+
flat.append(flat_node)
|
171
|
+
flat.extend(flatten_tree(children, parent_temp_id=node_id))
|
158
172
|
return flat
|
159
173
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
174
|
+
raw_data = flatten_tree(tree_data)
|
175
|
+
|
176
|
+
# Step 2: Identify which objects should be updated
|
177
|
+
pks = [r["_id"] for r in raw_data if r["_id"] is not None]
|
178
|
+
existing_ids = set(cls.objects.filter(
|
179
|
+
pk__in=pks).values_list('id', flat=True))
|
180
|
+
to_update_ids = {r["_id"] for r in raw_data if r["_id"] in existing_ids}
|
181
|
+
|
182
|
+
existing_objs = {
|
183
|
+
obj.pk: obj for obj in cls.objects.filter(pk__in=to_update_ids)}
|
184
|
+
|
185
|
+
# Step 3: Determine tree levels for correct creation order
|
186
|
+
levels = {}
|
187
|
+
for record in raw_data:
|
188
|
+
level = 0
|
189
|
+
p = record.get('_parent')
|
190
|
+
while p:
|
191
|
+
level += 1
|
192
|
+
p = next((r['_parent']
|
193
|
+
for r in raw_data if r['_id'] == p), None)
|
194
|
+
levels.setdefault(level, []).append(record)
|
195
|
+
|
196
|
+
records_by_level = [
|
197
|
+
sorted(levels[level], key=lambda x: x.get('_parent') or -1)
|
198
|
+
for level in sorted(levels)
|
199
|
+
]
|
200
|
+
|
201
|
+
# Step 4: Map old temporary IDs to actual DB IDs
|
202
|
+
created_objects = []
|
203
|
+
updated_objects = []
|
204
|
+
# ID mapping for existing objects
|
205
|
+
id_map = {pk: pk for pk in to_update_ids}
|
206
|
+
|
207
|
+
model_fields = {
|
208
|
+
f.name for f in cls._meta.get_fields()
|
209
|
+
if f.concrete and not f.auto_created and not f.name.startswith('_')
|
210
|
+
}
|
211
|
+
|
212
|
+
# Step 5: Process records level by level
|
213
|
+
for level_records in records_by_level:
|
214
|
+
objs_to_create = []
|
215
|
+
objs_to_update = []
|
216
|
+
fields_to_update_set = set()
|
217
|
+
|
218
|
+
for record in level_records:
|
219
|
+
temp_id = record['_id']
|
220
|
+
temp_parent = record.get('_parent')
|
221
|
+
data = {k: v for k, v in record.items() if not k.startswith(
|
222
|
+
'_') and k in model_fields}
|
223
|
+
|
224
|
+
if temp_parent and temp_parent in id_map:
|
225
|
+
data['parent_id'] = id_map[temp_parent]
|
226
|
+
|
227
|
+
if temp_id in to_update_ids:
|
228
|
+
obj = existing_objs[temp_id]
|
229
|
+
for field, value in data.items():
|
230
|
+
setattr(obj, field, value)
|
231
|
+
setattr(obj, '_temp_parent', temp_parent)
|
232
|
+
objs_to_update.append(obj)
|
233
|
+
fields_to_update_set.update(data.keys())
|
234
|
+
else:
|
235
|
+
obj = cls(**data)
|
236
|
+
obj.full_clean()
|
237
|
+
setattr(obj, '_temp_id', temp_id)
|
238
|
+
setattr(obj, '_temp_parent', temp_parent)
|
239
|
+
objs_to_create.append(obj)
|
240
|
+
|
241
|
+
# Step 6: Bulk create and track real IDs
|
242
|
+
if objs_to_create:
|
243
|
+
created = cls.objects.bulk_create(objs_to_create)
|
244
|
+
for obj in created:
|
245
|
+
temp_id = getattr(obj, '_temp_id')
|
246
|
+
id_map[temp_id] = obj.id
|
247
|
+
delattr(obj, '_temp_id')
|
248
|
+
created_objects.append(obj)
|
249
|
+
|
250
|
+
# Step 7: Bulk update existing objects
|
251
|
+
if objs_to_update:
|
252
|
+
for obj in objs_to_update:
|
253
|
+
obj.full_clean()
|
207
254
|
cls.objects.bulk_update(
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
255
|
+
objs_to_update, list(fields_to_update_set))
|
256
|
+
updated_objects.extend(objs_to_update)
|
257
|
+
|
258
|
+
all_objects = created_objects + updated_objects
|
259
|
+
|
260
|
+
# Step 8: Finalize parent assignments using ID map
|
261
|
+
for obj in all_objects:
|
262
|
+
temp_parent = getattr(obj, '_temp_parent', None)
|
263
|
+
if temp_parent is not None and temp_parent in id_map:
|
264
|
+
obj.parent_id = id_map[temp_parent]
|
265
|
+
for attr in ['_temp_id', '_temp_parent']:
|
266
|
+
if hasattr(obj, attr):
|
267
|
+
delattr(obj, attr)
|
268
|
+
|
269
|
+
cls.objects.bulk_update(all_objects, ['parent_id'])
|
270
|
+
|
271
|
+
# Step 9: Rebuild tree structure (path, depth, priority)
|
272
|
+
cls.tasks.run()
|
273
|
+
|
274
|
+
# Step 10: Invalidate tree-related caches
|
275
|
+
cache.invalidate(cls._meta.label)
|
213
276
|
|
214
277
|
@classmethod
|
215
278
|
def load_tree_json(cls, json_str):
|
@@ -222,36 +285,9 @@ class TreeNodeTreeMixin(models.Model):
|
|
222
285
|
tree_data = json.loads(json_str)
|
223
286
|
except json.JSONDecodeError as e:
|
224
287
|
raise ValueError(f"Error decoding JSON: {e}")
|
225
|
-
|
226
|
-
cls.load_tree(tree_data)
|
227
|
-
|
228
|
-
@classmethod
|
229
|
-
@cached_method
|
230
|
-
def get_tree_display(cls, instance=None, symbol="—"):
|
231
|
-
"""Get a multiline string representing the model tree."""
|
232
|
-
# If instance is passed, we get all its descendants (including itself)
|
233
|
-
if instance:
|
234
|
-
queryset = instance.get_descendants_queryset(include_self=True)\
|
235
|
-
.prefetch_related("tn_children")\
|
236
|
-
.annotate(depth=models.Max("parents_set__depth"))\
|
237
|
-
.order_by("depth", "tn_parent", "tn_priority")
|
238
|
-
else:
|
239
|
-
queryset = cls.objects.all()\
|
240
|
-
.prefetch_related("tn_children")\
|
241
|
-
.annotate(depth=models.Max("parents_set__depth"))\
|
242
|
-
.order_by("depth", "tn_parent", "tn_priority")
|
243
|
-
# Convert queryset to list for indexed access
|
244
|
-
nodes = list(queryset)
|
245
|
-
sorted_nodes = cls._sort_node_list(nodes)
|
246
|
-
result = []
|
247
|
-
for node in sorted_nodes:
|
248
|
-
# Insert an indent proportional to the depth of the node
|
249
|
-
indent = symbol * node.depth
|
250
|
-
result.append(indent + str(node))
|
251
|
-
return result
|
288
|
+
return tree_data
|
252
289
|
|
253
290
|
@classmethod
|
254
|
-
@cached_method
|
255
291
|
def get_tree_annotated(cls):
|
256
292
|
"""
|
257
293
|
Get an annotated list from a tree branch.
|
@@ -285,10 +321,7 @@ class TreeNodeTreeMixin(models.Model):
|
|
285
321
|
|
286
322
|
"""
|
287
323
|
# Load the tree with the required preloads and depth annotation.
|
288
|
-
queryset = cls.objects.all()
|
289
|
-
.prefetch_related("tn_children")\
|
290
|
-
.annotate(depth=models.Max("parents_set__depth"))\
|
291
|
-
.order_by("depth", "tn_parent", "tn_priority")
|
324
|
+
queryset = cls.objects.all()
|
292
325
|
# Convert queryset to list for indexed access
|
293
326
|
nodes = list(queryset)
|
294
327
|
total_nodes = len(nodes)
|
@@ -301,7 +334,7 @@ class TreeNodeTreeMixin(models.Model):
|
|
301
334
|
value = str(node)
|
302
335
|
# Determine if there are descendants (use prefetch_related to avoid
|
303
336
|
# additional queries)
|
304
|
-
value_open = len(node.
|
337
|
+
value_open = len(node.children.all()) > 0
|
305
338
|
level = node.depth
|
306
339
|
|
307
340
|
# Calculate the "close" field
|
@@ -326,19 +359,45 @@ class TreeNodeTreeMixin(models.Model):
|
|
326
359
|
return result
|
327
360
|
|
328
361
|
@classmethod
|
329
|
-
@transaction.atomic
|
330
362
|
def update_tree(cls):
|
331
|
-
"""
|
332
|
-
|
333
|
-
cls.
|
334
|
-
|
335
|
-
|
336
|
-
|
363
|
+
"""Rebuld the tree."""
|
364
|
+
cls.tasks.add("update", None)
|
365
|
+
cls.tasks.run()
|
366
|
+
|
367
|
+
def delete_tree(self, include_self=True):
|
368
|
+
"""
|
369
|
+
Delete current node and all descendants.
|
370
|
+
|
371
|
+
If include_self=False, only descendants will be deleted.
|
372
|
+
"""
|
373
|
+
table = self._meta.db_table
|
374
|
+
path = self._path
|
375
|
+
like_pattern = f"{path}.%"
|
376
|
+
|
377
|
+
if include_self:
|
378
|
+
sql = f"""
|
379
|
+
DELETE FROM {table}
|
380
|
+
WHERE _path = %s OR _path LIKE %s
|
381
|
+
"""
|
382
|
+
params = [path, like_pattern]
|
383
|
+
else:
|
384
|
+
sql = f"""
|
385
|
+
DELETE FROM {table}
|
386
|
+
WHERE _path LIKE %s
|
387
|
+
"""
|
388
|
+
params = [like_pattern]
|
389
|
+
|
390
|
+
with connection.cursor() as cursor:
|
391
|
+
cursor.execute(sql, params)
|
392
|
+
self.clear_cache()
|
337
393
|
|
338
394
|
@classmethod
|
339
|
-
def
|
395
|
+
def delete_forest(cls):
|
340
396
|
"""Delete the whole tree for the current node class."""
|
341
|
-
cls.
|
342
|
-
cls.
|
397
|
+
# cls.objects.all()._raw_delete(cls._base_manager.db)
|
398
|
+
table = cls._meta.db_table
|
399
|
+
with connection.cursor() as cursor:
|
400
|
+
cursor.execute(f"DELETE FROM {table}")
|
401
|
+
cache.invalidate(cls._meta.label)
|
343
402
|
|
344
403
|
# The end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Raw SQL Mixin
|
4
|
+
|
5
|
+
Version: 3.0.0
|
6
|
+
Author: Timur Kady
|
7
|
+
Email: timurkady@yandex.com
|
8
|
+
"""
|
9
|
+
|
10
|
+
from django.db import models, connection
|
11
|
+
|
12
|
+
from ...settings import SEGMENT_LENGTH, BASE
|
13
|
+
from ...utils.db.sqlcompat import SQLCompat
|
14
|
+
|
15
|
+
|
16
|
+
class RawSQLMixin(models.Model):
|
17
|
+
"""Raw SQL Mixin."""
|
18
|
+
|
19
|
+
class Meta:
|
20
|
+
"""Meta Class."""
|
21
|
+
|
22
|
+
abstract = True
|
23
|
+
|
24
|
+
def refresh(self):
|
25
|
+
"""Refresh key fields from DB."""
|
26
|
+
task_query = self._meta.model.tasks
|
27
|
+
if len(task_query.queue) > 0:
|
28
|
+
task_query.run()
|
29
|
+
|
30
|
+
table = self._meta.db_table
|
31
|
+
sql = f"SELECT priority, _path, _depth FROM {table} WHERE id = %s"
|
32
|
+
with connection.cursor() as cursor:
|
33
|
+
cursor.execute(sql, [self.pk])
|
34
|
+
row = cursor.fetchone()
|
35
|
+
self.priority, self._path, self._depth = row
|
36
|
+
|
37
|
+
self._parent_id = self.parent_pk
|
38
|
+
self._priority = self.priority
|
39
|
+
|
40
|
+
def _shift_siblings_forward(self):
|
41
|
+
"""
|
42
|
+
Shift the priority of all siblings starting from self.priority.
|
43
|
+
|
44
|
+
Uses direct SQL for maximum speed.
|
45
|
+
"""
|
46
|
+
if (self.priority is None) or (self.priority >= BASE - 1):
|
47
|
+
return
|
48
|
+
|
49
|
+
db_table = self._meta.db_table
|
50
|
+
|
51
|
+
if self.parent_id is None:
|
52
|
+
where_clause = "parent_id IS NULL"
|
53
|
+
params = [self.priority]
|
54
|
+
else:
|
55
|
+
where_clause = "parent_id = %s"
|
56
|
+
params = [self.parent_id, self.priority]
|
57
|
+
|
58
|
+
sql = f"""
|
59
|
+
UPDATE {db_table}
|
60
|
+
SET priority = priority + 1
|
61
|
+
WHERE {where_clause} AND priority >= %s
|
62
|
+
"""
|
63
|
+
|
64
|
+
with connection.cursor() as cursor:
|
65
|
+
cursor.execute(sql, params)
|
66
|
+
|
67
|
+
def _update_path(self, parent_id):
|
68
|
+
"""
|
69
|
+
Rebuild subtree starting from parent_id.
|
70
|
+
|
71
|
+
If parent_id=None, then the whole tree is rebuilt.
|
72
|
+
Only fields are used: parent_id and id. All others (priority, _path,
|
73
|
+
_depth) are recalculated.
|
74
|
+
"""
|
75
|
+
db_table = self._meta.db_table
|
76
|
+
|
77
|
+
sorting_field = self.sorting_field
|
78
|
+
sorting_fields = ["priority", "id"] if sorting_field == "priority" else [sorting_field] # noqa: D501
|
79
|
+
sort_expr = ", ".join([
|
80
|
+
field if "." in field else f"c.{field}"
|
81
|
+
for field in sorting_fields
|
82
|
+
])
|
83
|
+
|
84
|
+
cte_header = "(id, parent_id, new_priority, new_path, new_depth)"
|
85
|
+
|
86
|
+
row_number_expr = "ROW_NUMBER() OVER (ORDER BY {sort_expr}) - 1"
|
87
|
+
hex_expr = SQLCompat.to_hex(row_number_expr)
|
88
|
+
lpad_expr = SQLCompat.lpad(hex_expr, SEGMENT_LENGTH, "'0'")
|
89
|
+
|
90
|
+
if parent_id is None:
|
91
|
+
new_path_expr = lpad_expr
|
92
|
+
base_sql = f"""
|
93
|
+
SELECT
|
94
|
+
c.id,
|
95
|
+
c.parent_id,
|
96
|
+
{row_number_expr} AS new_priority,
|
97
|
+
{new_path_expr} AS new_path,
|
98
|
+
0 AS new_depth
|
99
|
+
FROM {db_table} AS c
|
100
|
+
WHERE c.parent_id IS NULL
|
101
|
+
"""
|
102
|
+
params = []
|
103
|
+
else:
|
104
|
+
path_expr = SQLCompat.concat("p._path", "'.'", lpad_expr)
|
105
|
+
base_sql = f"""
|
106
|
+
SELECT
|
107
|
+
c.id,
|
108
|
+
c.parent_id,
|
109
|
+
{row_number_expr} AS new_priority,
|
110
|
+
{path_expr} AS new_path,
|
111
|
+
p._depth + 1 AS new_depth
|
112
|
+
FROM {db_table} c
|
113
|
+
JOIN {db_table} p ON c.parent_id = p.id
|
114
|
+
WHERE p.id = %s
|
115
|
+
"""
|
116
|
+
params = [parent_id]
|
117
|
+
|
118
|
+
recursive_row_number_expr = "ROW_NUMBER() OVER (PARTITION BY c.parent_id ORDER BY {sort_expr}) - 1" # noqa: D501
|
119
|
+
recursive_hex_expr = SQLCompat.to_hex(recursive_row_number_expr)
|
120
|
+
recursive_lpad_expr = SQLCompat.lpad(
|
121
|
+
recursive_hex_expr, SEGMENT_LENGTH, "'0'")
|
122
|
+
recursive_path_expr = SQLCompat.concat(
|
123
|
+
"t.new_path", "'.'", recursive_lpad_expr)
|
124
|
+
|
125
|
+
recursive_sql = f"""
|
126
|
+
SELECT
|
127
|
+
c.id,
|
128
|
+
c.parent_id,
|
129
|
+
{recursive_row_number_expr} AS new_priority,
|
130
|
+
{recursive_path_expr} AS new_path,
|
131
|
+
t.new_depth + 1 AS new_depth
|
132
|
+
FROM {db_table} c
|
133
|
+
JOIN tree_cte t ON c.parent_id = t.id
|
134
|
+
"""
|
135
|
+
|
136
|
+
final_sql = f"""
|
137
|
+
WITH RECURSIVE tree_cte {cte_header} AS (
|
138
|
+
{base_sql}
|
139
|
+
UNION ALL
|
140
|
+
{recursive_sql}
|
141
|
+
)
|
142
|
+
UPDATE {db_table} AS orig
|
143
|
+
SET
|
144
|
+
priority = t.new_priority,
|
145
|
+
_path = t.new_path,
|
146
|
+
_depth = t.new_depth
|
147
|
+
FROM tree_cte t
|
148
|
+
WHERE orig.id = t.id;
|
149
|
+
"""
|
150
|
+
|
151
|
+
self.sqlq.append((final_sql.format(sort_expr=sort_expr), params))
|
152
|
+
|
153
|
+
|
154
|
+
# The End
|