django-fast-treenode 2.0.9__py3-none-any.whl → 2.0.11__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-2.0.9.dist-info → django_fast_treenode-2.0.11.dist-info}/METADATA +4 -3
- {django_fast_treenode-2.0.9.dist-info → django_fast_treenode-2.0.11.dist-info}/RECORD +19 -18
- treenode/admin.py +95 -71
- treenode/docs/Documentation +7 -35
- treenode/forms.py +64 -23
- treenode/managers.py +306 -168
- treenode/models/closure.py +22 -46
- treenode/models/proxy.py +43 -24
- treenode/templates/admin/tree_node_changelist.html +2 -0
- treenode/templates/admin/tree_node_import.html +27 -9
- treenode/templates/admin/tree_node_import_report.html +32 -0
- treenode/utils/__init__.py +4 -5
- treenode/utils/exporter.py +80 -27
- treenode/utils/importer.py +185 -152
- treenode/views.py +18 -12
- treenode/widgets.py +21 -5
- {django_fast_treenode-2.0.9.dist-info → django_fast_treenode-2.0.11.dist-info}/LICENSE +0 -0
- {django_fast_treenode-2.0.9.dist-info → django_fast_treenode-2.0.11.dist-info}/WHEEL +0 -0
- {django_fast_treenode-2.0.9.dist-info → django_fast_treenode-2.0.11.dist-info}/top_level.txt +0 -0
treenode/managers.py
CHANGED
@@ -11,13 +11,15 @@ Features:
|
|
11
11
|
- `TreeNodeQuerySet` and `TreeNodeModelManager` for adjacency model operations.
|
12
12
|
- Optimized `bulk_create` and `bulk_update` methods with atomic transactions.
|
13
13
|
|
14
|
-
Version: 2.0.
|
14
|
+
Version: 2.0.11
|
15
15
|
Author: Timur Kady
|
16
16
|
Email: timurkady@yandex.com
|
17
17
|
"""
|
18
18
|
|
19
|
-
|
19
|
+
from collections import deque, defaultdict
|
20
20
|
from django.db import models, transaction
|
21
|
+
from django.db.models import F
|
22
|
+
from django.db import connection
|
21
23
|
|
22
24
|
|
23
25
|
# ----------------------------------------------------------------------------
|
@@ -28,173 +30,237 @@ from django.db import models, transaction
|
|
28
30
|
class ClosureQuerySet(models.QuerySet):
|
29
31
|
"""QuerySet для ClosureModel."""
|
30
32
|
|
31
|
-
|
32
|
-
def bulk_create(self, objs, batch_size=1000):
|
33
|
+
def sort_nodes(self, node_list):
|
33
34
|
"""
|
34
|
-
|
35
|
+
Sort nodes topologically.
|
35
36
|
|
36
|
-
|
37
|
-
|
38
|
-
|
37
|
+
Возвращает список узлов, отсортированных от корней к листьям.
|
38
|
+
Узел считается корневым, если его tn_parent равен None или его
|
39
|
+
родитель отсутствует в node_list.
|
39
40
|
"""
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
41
|
+
visited = set() # будем хранить id уже обработанных узлов
|
42
|
+
result = []
|
43
|
+
# Множество id узлов, входящих в исходный список
|
44
|
+
node_ids = {node.id for node in node_list}
|
45
|
+
|
46
|
+
def dfs(node):
|
47
|
+
if node.id in visited:
|
48
|
+
return
|
49
|
+
# Если родитель есть и он входит в node_list – обрабатываем его
|
50
|
+
# первым
|
51
|
+
if node.tn_parent and node.tn_parent_id in node_ids:
|
52
|
+
dfs(node.tn_parent)
|
53
|
+
visited.add(node.id)
|
54
|
+
result.append(node)
|
55
|
+
|
56
|
+
for n in node_list:
|
57
|
+
dfs(n)
|
47
58
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
59
|
+
return result
|
60
|
+
|
61
|
+
@transaction.atomic
|
62
|
+
def bulk_create(self, objs, batch_size=1000, *args, **kwargs):
|
63
|
+
"""Insert new nodes in bulk."""
|
91
64
|
result = []
|
92
|
-
|
93
|
-
|
65
|
+
|
66
|
+
# 1. Топологическая сортировка узлов
|
67
|
+
objs = self.sort_nodes(objs)
|
68
|
+
|
69
|
+
# 1. Создаем self-ссылки для всех узлов: (node, node, 0).
|
70
|
+
self_links = [
|
71
|
+
self.model(parent=obj, child=obj, depth=0)
|
72
|
+
for obj in objs
|
73
|
+
]
|
74
|
+
result.extend(
|
75
|
+
super().bulk_create(self_links, batch_size, *args, **kwargs)
|
76
|
+
)
|
77
|
+
|
78
|
+
# 2. Формируем отображение: id родителя -> список его детей.
|
79
|
+
children_map = defaultdict(list)
|
80
|
+
for obj in objs:
|
81
|
+
if obj.tn_parent_id:
|
82
|
+
children_map[obj.tn_parent_id].append(obj)
|
83
|
+
|
84
|
+
# 3. Пробуем определить корневые узлы (с tn_parent == None).
|
85
|
+
root_nodes = [obj for obj in objs if obj.tn_parent is None]
|
86
|
+
|
87
|
+
# Если корневых узлов нет, значит вставляем поддерево.
|
88
|
+
if not root_nodes:
|
89
|
+
# Определяем "верхние" узлы поддерева:
|
90
|
+
# те, чей родитель не входит в список вставляемых объектов.
|
91
|
+
objs_ids = {obj.id for obj in objs if obj.id is not None}
|
92
|
+
top_nodes = [
|
93
|
+
obj for obj in objs if obj.tn_parent_id not in objs_ids
|
94
|
+
]
|
95
|
+
|
96
|
+
# Для каждого такого узла, если родитель существует, получаем
|
97
|
+
# записи замыкания для родителя и добавляем новые записи для
|
98
|
+
# (ancestor -> node) с depth = ancestor.depth + 1.
|
99
|
+
new_entries = []
|
100
|
+
for node in top_nodes:
|
101
|
+
if node.tn_parent_id:
|
102
|
+
parent_closures = self.model.objects.filter(
|
103
|
+
child_id=node.tn_parent_id
|
104
|
+
)
|
105
|
+
for ancestor in parent_closures:
|
106
|
+
new_entries.append(
|
107
|
+
self.model(
|
108
|
+
parent=ancestor.parent,
|
109
|
+
child=node,
|
110
|
+
depth=ancestor.depth + 1
|
111
|
+
)
|
112
|
+
)
|
113
|
+
if new_entries:
|
114
|
+
result.extend(
|
115
|
+
super().bulk_create(
|
116
|
+
new_entries, batch_size, *args, **kwargs
|
117
|
+
)
|
118
|
+
)
|
119
|
+
|
120
|
+
# Устанавливаем узлы верхнего уровня поддерева как начальные
|
121
|
+
# для обхода.
|
122
|
+
current_nodes = top_nodes
|
123
|
+
else:
|
124
|
+
current_nodes = root_nodes
|
125
|
+
|
126
|
+
# 4. Рекурсивная функция для обхода уровней.
|
127
|
+
def process_level(current_nodes):
|
128
|
+
next_level = []
|
129
|
+
new_entries = []
|
130
|
+
for node in current_nodes:
|
131
|
+
# Для текущего узла получаем все записи замыкания (его предков).
|
132
|
+
ancestors = self.model.objects.filter(child=node)
|
133
|
+
for child in children_map.get(node.id, []):
|
134
|
+
for ancestor in ancestors:
|
135
|
+
new_entries.append(
|
136
|
+
self.model(
|
137
|
+
parent=ancestor.parent,
|
138
|
+
child=child,
|
139
|
+
depth=ancestor.depth + 1
|
140
|
+
)
|
141
|
+
)
|
142
|
+
next_level.append(child)
|
143
|
+
if new_entries:
|
144
|
+
result.extend(
|
145
|
+
super().bulk_create(
|
146
|
+
new_entries, batch_size, *args, **kwargs
|
147
|
+
)
|
148
|
+
)
|
149
|
+
if next_level:
|
150
|
+
process_level(next_level)
|
151
|
+
|
152
|
+
process_level(current_nodes)
|
153
|
+
|
94
154
|
self.model.clear_cache()
|
95
155
|
return result
|
96
156
|
|
97
157
|
@transaction.atomic
|
98
158
|
def bulk_update(self, objs, fields=None, batch_size=1000):
|
99
159
|
"""
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
160
|
+
Обновляет таблицу замыкания для объектов, у которых изменился tn_parent.
|
161
|
+
|
162
|
+
Предполагается, что все объекты из списка objs уже есть в таблице
|
163
|
+
замыкания, но их связи (как для родителей, так и для детей) могли
|
164
|
+
измениться.
|
165
|
+
|
166
|
+
Алгоритм:
|
167
|
+
1. Формируем отображение: id родителя → список его детей.
|
168
|
+
2. Определяем корневые узлы обновляемого поддерева:
|
169
|
+
– Узел считается корневым, если его tn_parent равен None или его
|
170
|
+
родитель не входит в objs.
|
171
|
+
3. Для каждого корневого узла, если есть внешний родитель, получаем его
|
172
|
+
замыкание из базы.
|
173
|
+
Затем формируем записи замыкания для узла (все внешние связи с
|
174
|
+
увеличенным depth и self-ссылка).
|
175
|
+
4. С помощью BFS обходим поддерево: для каждого узла для каждого его
|
176
|
+
ребёнка создаём записи, используя родительские записи (увеличенные
|
177
|
+
на 1) и добавляем self-ссылку для ребёнка.
|
178
|
+
5. Удаляем старые записи замыкания для объектов из objs и сохраняем
|
179
|
+
новые пакетно.
|
111
180
|
"""
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
#
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
#
|
125
|
-
#
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
)
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
#
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
181
|
+
# 1. Топологическая сортировка узлов
|
182
|
+
objs = self.sort_nodes(objs)
|
183
|
+
|
184
|
+
# 2. Построим отображение: id родителя → список детей
|
185
|
+
children_map = defaultdict(list)
|
186
|
+
for obj in objs:
|
187
|
+
if obj.tn_parent_id:
|
188
|
+
children_map[obj.tn_parent_id].append(obj)
|
189
|
+
|
190
|
+
# Множество id обновляемых объектов
|
191
|
+
objs_ids = {obj.id for obj in objs}
|
192
|
+
|
193
|
+
# 3. Определяем корневые узлы обновляемого поддерева:
|
194
|
+
# Узел считается корневым, если его tn_parent либо равен None, либо
|
195
|
+
# его родитель не входит в objs.
|
196
|
+
roots = [
|
197
|
+
obj for obj in objs
|
198
|
+
if (obj.tn_parent is None) or (obj.tn_parent_id not in objs_ids)
|
199
|
+
]
|
200
|
+
|
201
|
+
# Список для накопления новых записей замыкания
|
202
|
+
new_closure_entries = []
|
203
|
+
|
204
|
+
# Очередь для BFS: каждый элемент — кортеж (node, node_closure),
|
205
|
+
# где node_closure — список записей замыкания для этого узла.
|
206
|
+
queue = deque()
|
207
|
+
for node in roots:
|
208
|
+
if node.tn_parent_id:
|
209
|
+
# Получаем замыкание внешнего родителя из базы
|
210
|
+
external_ancestors = list(
|
211
|
+
self.model.objects.filter(child_id=node.tn_parent_id)
|
212
|
+
.values('parent_id', 'depth')
|
213
|
+
)
|
214
|
+
# Для каждого найденного предка создаём запись для node с
|
215
|
+
# depth+1
|
216
|
+
node_closure = [
|
217
|
+
self.model(
|
218
|
+
parent_id=entry['parent_id'],
|
219
|
+
child=node,
|
220
|
+
depth=entry['depth'] + 1
|
221
|
+
)
|
222
|
+
for entry in external_ancestors
|
223
|
+
]
|
224
|
+
else:
|
225
|
+
node_closure = []
|
226
|
+
# Добавляем self-ссылку (node → node, depth 0)
|
227
|
+
node_closure.append(self.model(parent=node, child=node, depth=0))
|
228
|
+
|
229
|
+
# Сохраняем записи для текущего узла и кладем в очередь для
|
230
|
+
# обработки его поддерева
|
231
|
+
new_closure_entries.extend(node_closure)
|
232
|
+
queue.append((node, node_closure))
|
233
|
+
|
234
|
+
# 4. BFS-обход поддерева: для каждого узла создаём замыкание для его
|
235
|
+
# детей
|
236
|
+
while queue:
|
237
|
+
parent_node, parent_closure = queue.popleft()
|
238
|
+
for child in children_map.get(parent_node.id, []):
|
239
|
+
# Для ребенка новые записи замыкания:
|
240
|
+
# для каждого записи родителя создаем (ancestor -> child)
|
241
|
+
# с depth+1
|
242
|
+
child_closure = [
|
243
|
+
self.model(
|
244
|
+
parent_id=entry.parent_id,
|
245
|
+
child=child,
|
246
|
+
depth=entry.depth + 1
|
176
247
|
)
|
248
|
+
for entry in parent_closure
|
249
|
+
]
|
250
|
+
# Добавляем self-ссылку для ребенка
|
251
|
+
child_closure.append(
|
252
|
+
self.model(parent=child, child=child, depth=0)
|
253
|
+
)
|
177
254
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
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)
|
255
|
+
new_closure_entries.extend(child_closure)
|
256
|
+
queue.append((child, child_closure))
|
257
|
+
|
258
|
+
# 5. Удаляем старые записи замыкания для обновляемых объектов
|
259
|
+
self.model.objects.filter(child_id__in=objs_ids).delete()
|
260
|
+
|
261
|
+
# 6. Сохраняем новые записи пакетно
|
262
|
+
super().bulk_create(new_closure_entries)
|
196
263
|
self.model.clear_cache()
|
197
|
-
return new_records
|
198
264
|
|
199
265
|
|
200
266
|
class ClosureModelManager(models.Manager):
|
@@ -230,30 +296,38 @@ class TreeNodeQuerySet(models.QuerySet):
|
|
230
296
|
super().__init__(model, query, using, hints)
|
231
297
|
|
232
298
|
@transaction.atomic
|
233
|
-
def bulk_create(self, objs, batch_size=1000,
|
299
|
+
def bulk_create(self, objs, batch_size=1000, *args, **kwargs):
|
234
300
|
"""
|
235
301
|
Bulk create.
|
236
302
|
|
237
303
|
Method of bulk creation objects with updating and processing of
|
238
304
|
the Closuse Model.
|
239
305
|
"""
|
240
|
-
#
|
241
|
-
objs = super().bulk_create(objs, batch_size,
|
242
|
-
|
243
|
-
|
244
|
-
|
306
|
+
# 1. Массовая вставка узлов в Модели Смежности
|
307
|
+
objs = super().bulk_create(objs, batch_size, *args, **kwargs)
|
308
|
+
|
309
|
+
# 2. Синхронизация Модели Закрытия
|
310
|
+
self.closure_model.objects.bulk_create(objs)
|
311
|
+
|
312
|
+
# 3. Очиска кэша и возрат результата
|
245
313
|
self.model.clear_cache()
|
246
|
-
return
|
314
|
+
return objs
|
247
315
|
|
248
316
|
@transaction.atomic
|
249
317
|
def bulk_update(self, objs, fields, batch_size=1000, **kwargs):
|
250
|
-
"""."""
|
251
|
-
|
318
|
+
"""Bulk update."""
|
319
|
+
# 1. Выполняем обновление Модели Смежности
|
320
|
+
result = super().bulk_update(objs, fields, batch_size, **kwargs)
|
321
|
+
|
322
|
+
# 2. Синхронизируем данные в Модели Закрытия
|
252
323
|
if 'tn_parent' in fields:
|
253
324
|
# Попросим ClosureModel обработать move
|
254
|
-
closure_model.objects.bulk_update(
|
255
|
-
|
256
|
-
|
325
|
+
self.closure_model.objects.bulk_update(
|
326
|
+
objs, ["tn_parent",], batch_size
|
327
|
+
)
|
328
|
+
|
329
|
+
# 3. Очиска кэша и возрат результата
|
330
|
+
self.model.clear_cache()
|
257
331
|
return result
|
258
332
|
|
259
333
|
|
@@ -269,13 +343,77 @@ class TreeNodeModelManager(models.Manager):
|
|
269
343
|
custom QuerySet.
|
270
344
|
"""
|
271
345
|
self.model.clear_cache()
|
272
|
-
|
346
|
+
result = self.get_queryset().bulk_create(
|
273
347
|
objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts
|
274
348
|
)
|
349
|
+
transaction.on_commit(lambda: self.update_auto_increment())
|
350
|
+
return result
|
351
|
+
|
352
|
+
def bulk_update(self, objs, fields=None, batch_size=1000):
|
353
|
+
"""Bulk Update."""
|
354
|
+
self.model.clear_cache()
|
355
|
+
result = self.get_queryset().bulk_update(objs, fields, batch_size)
|
356
|
+
return result
|
275
357
|
|
276
358
|
def get_queryset(self):
|
277
359
|
"""Return a QuerySet that sorts by 'tn_parent' and 'tn_priority'."""
|
278
360
|
queryset = TreeNodeQuerySet(self.model, using=self._db)
|
279
|
-
return queryset.order_by(
|
361
|
+
return queryset.order_by(
|
362
|
+
F('tn_parent').asc(nulls_first=True),
|
363
|
+
'tn_parent',
|
364
|
+
'tn_priority'
|
365
|
+
)
|
366
|
+
|
367
|
+
def get_auto_increment_sequence(self):
|
368
|
+
"""Get auto increment sequence."""
|
369
|
+
table_name = self.model._meta.db_table
|
370
|
+
pk_column = self.model._meta.pk.column
|
371
|
+
with connection.cursor() as cursor:
|
372
|
+
query = "SELECT pg_get_serial_sequence(%s, %s)"
|
373
|
+
cursor.execute(query, [table_name, pk_column])
|
374
|
+
result = cursor.fetchone()
|
375
|
+
return result[0] if result else None
|
376
|
+
|
377
|
+
def update_auto_increment(self):
|
378
|
+
"""Update auto increment."""
|
379
|
+
table_name = self.model._meta.db_table
|
380
|
+
with connection.cursor() as cursor:
|
381
|
+
db_engine = connection.vendor
|
382
|
+
|
383
|
+
if db_engine == "postgresql":
|
384
|
+
sequence_name = self.get_auto_increment_sequence()
|
385
|
+
# Получаем максимальный id из таблицы
|
386
|
+
cursor.execute(
|
387
|
+
f"SELECT COALESCE(MAX(id), 0) FROM {table_name};"
|
388
|
+
)
|
389
|
+
max_id = cursor.fetchone()[0]
|
390
|
+
next_id = max_id + 1
|
391
|
+
# Прямо указываем следующее значение последовательности
|
392
|
+
cursor.execute(
|
393
|
+
f"ALTER SEQUENCE {sequence_name} RESTART WITH {next_id};"
|
394
|
+
)
|
395
|
+
elif db_engine == "mysql":
|
396
|
+
cursor.execute(f"SELECT MAX(id) FROM {table_name};")
|
397
|
+
max_id = cursor.fetchone()[0] or 0
|
398
|
+
next_id = max_id + 1
|
399
|
+
cursor.execute(
|
400
|
+
f"ALTER TABLE {table_name} AUTO_INCREMENT = {next_id};"
|
401
|
+
)
|
402
|
+
elif db_engine == "sqlite":
|
403
|
+
cursor.execute(
|
404
|
+
f"UPDATE sqlite_sequence SET seq = (SELECT MAX(id) \
|
405
|
+
FROM {table_name}) WHERE name='{table_name}';"
|
406
|
+
)
|
407
|
+
elif db_engine == "mssql":
|
408
|
+
cursor.execute(f"SELECT MAX(id) FROM {table_name};")
|
409
|
+
max_id = cursor.fetchone()[0] or 0
|
410
|
+
cursor.execute(
|
411
|
+
f"DBCC CHECKIDENT ('{table_name}', RESEED, {max_id});"
|
412
|
+
)
|
413
|
+
else:
|
414
|
+
raise NotImplementedError(
|
415
|
+
f"Autoincrement for {db_engine} is not supported."
|
416
|
+
)
|
417
|
+
|
280
418
|
|
281
419
|
# The End
|
treenode/models/closure.py
CHANGED
@@ -11,7 +11,7 @@ Features:
|
|
11
11
|
- Implements cached queries for improved performance.
|
12
12
|
- Provides bulk operations for inserting, moving, and deleting nodes.
|
13
13
|
|
14
|
-
Version: 2.0.
|
14
|
+
Version: 2.0.11
|
15
15
|
Author: Timur Kady
|
16
16
|
Email: timurkady@yandex.com
|
17
17
|
"""
|
@@ -53,6 +53,7 @@ class ClosureModel(models.Model):
|
|
53
53
|
unique_together = (("parent", "child"),)
|
54
54
|
indexes = [
|
55
55
|
models.Index(fields=["parent", "child"]),
|
56
|
+
models.Index(fields=["parent", "child", "depth"]),
|
56
57
|
]
|
57
58
|
|
58
59
|
def __str__(self):
|
@@ -68,46 +69,30 @@ class ClosureModel(models.Model):
|
|
68
69
|
|
69
70
|
@classmethod
|
70
71
|
@cached_method
|
71
|
-
def
|
72
|
-
"""Get the ancestors
|
73
|
-
|
74
|
-
if depth
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
qs = qs.exclude(parent=node, child=node)
|
79
|
-
return qs
|
72
|
+
def get_ancestors_pks(cls, node, include_self=True, depth=None):
|
73
|
+
"""Get the ancestors pks list."""
|
74
|
+
options = dict(child_id=node.pk, depth__gte=0 if include_self else 1)
|
75
|
+
if depth:
|
76
|
+
options["depth__lte"] = depth
|
77
|
+
queryset = cls.objects.filter(**options).order_by('depth')
|
78
|
+
return list(queryset.values_list("parent_id", flat=True))
|
80
79
|
|
81
80
|
@classmethod
|
82
81
|
@cached_method
|
83
|
-
def
|
84
|
-
"""Get
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
82
|
+
def get_descendants_pks(cls, node, include_self=False, depth=None):
|
83
|
+
"""Get a list containing all descendants."""
|
84
|
+
options = dict(parent_id=node.pk, depth__gte=0 if include_self else 1)
|
85
|
+
if depth:
|
86
|
+
options.update({'depth__lte': depth})
|
87
|
+
queryset = cls.objects.filter(**options)
|
88
|
+
return list(queryset.values_list("child_id", flat=True))
|
104
89
|
|
105
90
|
@classmethod
|
106
91
|
@cached_method
|
107
92
|
def get_root(cls, node):
|
108
|
-
"""Get the root node for the
|
109
|
-
|
110
|
-
|
93
|
+
"""Get the root node pk for the current node."""
|
94
|
+
queryset = cls.objects.filter(child=node).order_by('-depth')
|
95
|
+
return queryset.firts().parent if queryset.count() > 0 else None
|
111
96
|
|
112
97
|
@classmethod
|
113
98
|
@cached_method
|
@@ -125,15 +110,6 @@ class ClosureModel(models.Model):
|
|
125
110
|
return cls.objects.filter(child__pk=node.pk).aggregate(
|
126
111
|
models.Max("depth"))["depth__max"] + 1
|
127
112
|
|
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
113
|
@classmethod
|
138
114
|
@transaction.atomic
|
139
115
|
def insert_node(cls, node):
|
@@ -145,10 +121,10 @@ class ClosureModel(models.Model):
|
|
145
121
|
|
146
122
|
@classmethod
|
147
123
|
@transaction.atomic
|
148
|
-
def move_node(cls,
|
149
|
-
"""Move a node
|
124
|
+
def move_node(cls, nodes):
|
125
|
+
"""Move a nodes (node and its subtree) to a new parent."""
|
150
126
|
# Call bulk_update passing a single object
|
151
|
-
cls.objects.bulk_update(
|
127
|
+
cls.objects.bulk_update(nodes, batch_size=1000)
|
152
128
|
# Clear cache
|
153
129
|
cls.clear_cache()
|
154
130
|
|