django-fast-treenode 2.0.11__py3-none-any.whl → 2.1.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-2.0.11.dist-info → django_fast_treenode-2.1.0.dist-info}/LICENSE +2 -2
- django_fast_treenode-2.1.0.dist-info/METADATA +161 -0
- django_fast_treenode-2.1.0.dist-info/RECORD +75 -0
- {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.0.dist-info}/WHEEL +1 -1
- treenode/admin/__init__.py +9 -0
- treenode/admin/admin.py +295 -0
- treenode/admin/changelist.py +65 -0
- treenode/admin/mixins.py +302 -0
- treenode/apps.py +12 -1
- treenode/cache.py +2 -2
- treenode/docs/.gitignore +0 -0
- treenode/docs/about.md +36 -0
- treenode/docs/admin.md +104 -0
- treenode/docs/api.md +739 -0
- treenode/docs/cache.md +187 -0
- treenode/docs/import_export.md +35 -0
- treenode/docs/index.md +30 -0
- treenode/docs/installation.md +74 -0
- treenode/docs/migration.md +145 -0
- treenode/docs/models.md +128 -0
- treenode/docs/roadmap.md +45 -0
- treenode/forms.py +8 -10
- treenode/managers/__init__.py +21 -0
- treenode/managers/adjacency.py +203 -0
- treenode/managers/closure.py +278 -0
- treenode/models/__init__.py +2 -1
- treenode/models/adjacency.py +343 -0
- treenode/models/classproperty.py +3 -0
- treenode/models/closure.py +23 -24
- treenode/models/factory.py +12 -2
- treenode/models/mixins/__init__.py +23 -0
- treenode/models/mixins/ancestors.py +65 -0
- treenode/models/mixins/children.py +81 -0
- treenode/models/mixins/descendants.py +66 -0
- treenode/models/mixins/family.py +63 -0
- treenode/models/mixins/logical.py +68 -0
- treenode/models/mixins/node.py +210 -0
- treenode/models/mixins/properties.py +156 -0
- treenode/models/mixins/roots.py +96 -0
- treenode/models/mixins/siblings.py +99 -0
- treenode/models/mixins/tree.py +344 -0
- treenode/signals.py +26 -0
- treenode/static/treenode/css/tree_widget.css +201 -31
- treenode/static/treenode/css/treenode_admin.css +48 -41
- treenode/static/treenode/js/tree_widget.js +269 -131
- treenode/static/treenode/js/treenode_admin.js +131 -171
- treenode/templates/admin/tree_node_changelist.html +6 -0
- treenode/templates/admin/treenode_ajax_rows.html +7 -0
- treenode/tests/tests.py +488 -0
- treenode/urls.py +10 -6
- treenode/utils/__init__.py +2 -0
- treenode/utils/aid.py +46 -0
- treenode/utils/base16.py +38 -0
- treenode/utils/base36.py +3 -1
- treenode/utils/db.py +116 -0
- treenode/utils/exporter.py +2 -0
- treenode/utils/importer.py +0 -1
- treenode/utils/radix.py +61 -0
- treenode/version.py +2 -2
- treenode/views.py +118 -43
- treenode/widgets.py +91 -43
- django_fast_treenode-2.0.11.dist-info/METADATA +0 -698
- django_fast_treenode-2.0.11.dist-info/RECORD +0 -42
- treenode/admin.py +0 -439
- treenode/docs/Documentation +0 -636
- treenode/managers.py +0 -419
- treenode/models/proxy.py +0 -669
- {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.0.dist-info}/top_level.txt +0 -0
treenode/managers.py
DELETED
@@ -1,419 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
"""
|
3
|
-
Managers and QuerySets
|
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.11
|
15
|
-
Author: Timur Kady
|
16
|
-
Email: timurkady@yandex.com
|
17
|
-
"""
|
18
|
-
|
19
|
-
from collections import deque, defaultdict
|
20
|
-
from django.db import models, transaction
|
21
|
-
from django.db.models import F
|
22
|
-
from django.db import connection
|
23
|
-
|
24
|
-
|
25
|
-
# ----------------------------------------------------------------------------
|
26
|
-
# Closere Model
|
27
|
-
# ----------------------------------------------------------------------------
|
28
|
-
|
29
|
-
|
30
|
-
class ClosureQuerySet(models.QuerySet):
|
31
|
-
"""QuerySet для ClosureModel."""
|
32
|
-
|
33
|
-
def sort_nodes(self, node_list):
|
34
|
-
"""
|
35
|
-
Sort nodes topologically.
|
36
|
-
|
37
|
-
Возвращает список узлов, отсортированных от корней к листьям.
|
38
|
-
Узел считается корневым, если его tn_parent равен None или его
|
39
|
-
родитель отсутствует в node_list.
|
40
|
-
"""
|
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)
|
58
|
-
|
59
|
-
return result
|
60
|
-
|
61
|
-
@transaction.atomic
|
62
|
-
def bulk_create(self, objs, batch_size=1000, *args, **kwargs):
|
63
|
-
"""Insert new nodes in bulk."""
|
64
|
-
result = []
|
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
|
-
|
154
|
-
self.model.clear_cache()
|
155
|
-
return result
|
156
|
-
|
157
|
-
@transaction.atomic
|
158
|
-
def bulk_update(self, objs, fields=None, batch_size=1000):
|
159
|
-
"""
|
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
|
-
новые пакетно.
|
180
|
-
"""
|
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
|
247
|
-
)
|
248
|
-
for entry in parent_closure
|
249
|
-
]
|
250
|
-
# Добавляем self-ссылку для ребенка
|
251
|
-
child_closure.append(
|
252
|
-
self.model(parent=child, child=child, depth=0)
|
253
|
-
)
|
254
|
-
|
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)
|
263
|
-
self.model.clear_cache()
|
264
|
-
|
265
|
-
|
266
|
-
class ClosureModelManager(models.Manager):
|
267
|
-
"""ClosureModel Manager."""
|
268
|
-
|
269
|
-
def get_queryset(self):
|
270
|
-
"""get_queryset method."""
|
271
|
-
return ClosureQuerySet(self.model, using=self._db)
|
272
|
-
|
273
|
-
def bulk_create(self, objs, batch_size=1000):
|
274
|
-
"""Create objects in bulk."""
|
275
|
-
self.model.clear_cache()
|
276
|
-
return self.get_queryset().bulk_create(objs, batch_size=batch_size)
|
277
|
-
|
278
|
-
def bulk_update(self, objs, fields=None, batch_size=1000):
|
279
|
-
"""Move nodes in ClosureModel."""
|
280
|
-
self.model.clear_cache()
|
281
|
-
return self.get_queryset().bulk_update(
|
282
|
-
objs, fields, batch_size=batch_size
|
283
|
-
)
|
284
|
-
|
285
|
-
# ----------------------------------------------------------------------------
|
286
|
-
# TreeNode Model
|
287
|
-
# ----------------------------------------------------------------------------
|
288
|
-
|
289
|
-
|
290
|
-
class TreeNodeQuerySet(models.QuerySet):
|
291
|
-
"""TreeNodeModel QuerySet."""
|
292
|
-
|
293
|
-
def __init__(self, model=None, query=None, using=None, hints=None):
|
294
|
-
"""Init."""
|
295
|
-
self.closure_model = model.closure_model
|
296
|
-
super().__init__(model, query, using, hints)
|
297
|
-
|
298
|
-
@transaction.atomic
|
299
|
-
def bulk_create(self, objs, batch_size=1000, *args, **kwargs):
|
300
|
-
"""
|
301
|
-
Bulk create.
|
302
|
-
|
303
|
-
Method of bulk creation objects with updating and processing of
|
304
|
-
the Closuse Model.
|
305
|
-
"""
|
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. Очиска кэша и возрат результата
|
313
|
-
self.model.clear_cache()
|
314
|
-
return objs
|
315
|
-
|
316
|
-
@transaction.atomic
|
317
|
-
def bulk_update(self, objs, fields, batch_size=1000, **kwargs):
|
318
|
-
"""Bulk update."""
|
319
|
-
# 1. Выполняем обновление Модели Смежности
|
320
|
-
result = super().bulk_update(objs, fields, batch_size, **kwargs)
|
321
|
-
|
322
|
-
# 2. Синхронизируем данные в Модели Закрытия
|
323
|
-
if 'tn_parent' in fields:
|
324
|
-
# Попросим ClosureModel обработать move
|
325
|
-
self.closure_model.objects.bulk_update(
|
326
|
-
objs, ["tn_parent",], batch_size
|
327
|
-
)
|
328
|
-
|
329
|
-
# 3. Очиска кэша и возрат результата
|
330
|
-
self.model.clear_cache()
|
331
|
-
return result
|
332
|
-
|
333
|
-
|
334
|
-
class TreeNodeModelManager(models.Manager):
|
335
|
-
"""TreeNodeModel Manager."""
|
336
|
-
|
337
|
-
def bulk_create(self, objs, batch_size=1000, ignore_conflicts=False):
|
338
|
-
"""
|
339
|
-
Bulk Create.
|
340
|
-
|
341
|
-
Override bulk_create for the adjacency model.
|
342
|
-
Here we first clear the cache, then delegate the creation via our
|
343
|
-
custom QuerySet.
|
344
|
-
"""
|
345
|
-
self.model.clear_cache()
|
346
|
-
result = self.get_queryset().bulk_create(
|
347
|
-
objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts
|
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
|
357
|
-
|
358
|
-
def get_queryset(self):
|
359
|
-
"""Return a QuerySet that sorts by 'tn_parent' and 'tn_priority'."""
|
360
|
-
queryset = TreeNodeQuerySet(self.model, using=self._db)
|
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
|
-
|
418
|
-
|
419
|
-
# The End
|