django-fast-treenode 2.0.11__py3-none-any.whl → 2.1.1__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.
Files changed (57) hide show
  1. {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.dist-info}/LICENSE +2 -2
  2. django_fast_treenode-2.1.1.dist-info/METADATA +158 -0
  3. django_fast_treenode-2.1.1.dist-info/RECORD +64 -0
  4. {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.dist-info}/WHEEL +1 -1
  5. treenode/admin/__init__.py +9 -0
  6. treenode/admin/admin.py +295 -0
  7. treenode/admin/changelist.py +65 -0
  8. treenode/admin/mixins.py +302 -0
  9. treenode/apps.py +12 -1
  10. treenode/cache.py +2 -2
  11. treenode/forms.py +8 -10
  12. treenode/managers/__init__.py +21 -0
  13. treenode/managers/adjacency.py +203 -0
  14. treenode/managers/closure.py +278 -0
  15. treenode/models/__init__.py +2 -1
  16. treenode/models/adjacency.py +343 -0
  17. treenode/models/classproperty.py +3 -0
  18. treenode/models/closure.py +23 -24
  19. treenode/models/factory.py +12 -2
  20. treenode/models/mixins/__init__.py +23 -0
  21. treenode/models/mixins/ancestors.py +65 -0
  22. treenode/models/mixins/children.py +81 -0
  23. treenode/models/mixins/descendants.py +66 -0
  24. treenode/models/mixins/family.py +63 -0
  25. treenode/models/mixins/logical.py +68 -0
  26. treenode/models/mixins/node.py +210 -0
  27. treenode/models/mixins/properties.py +156 -0
  28. treenode/models/mixins/roots.py +96 -0
  29. treenode/models/mixins/siblings.py +99 -0
  30. treenode/models/mixins/tree.py +344 -0
  31. treenode/signals.py +26 -0
  32. treenode/static/treenode/css/tree_widget.css +201 -31
  33. treenode/static/treenode/css/treenode_admin.css +48 -41
  34. treenode/static/treenode/js/tree_widget.js +269 -131
  35. treenode/static/treenode/js/treenode_admin.js +131 -171
  36. treenode/templates/admin/tree_node_changelist.html +6 -0
  37. treenode/templates/admin/treenode_ajax_rows.html +7 -0
  38. treenode/tests/tests.py +488 -0
  39. treenode/urls.py +10 -6
  40. treenode/utils/__init__.py +2 -0
  41. treenode/utils/aid.py +46 -0
  42. treenode/utils/base16.py +38 -0
  43. treenode/utils/base36.py +3 -1
  44. treenode/utils/db.py +116 -0
  45. treenode/utils/exporter.py +2 -0
  46. treenode/utils/importer.py +0 -1
  47. treenode/utils/radix.py +61 -0
  48. treenode/version.py +2 -2
  49. treenode/views.py +118 -43
  50. treenode/widgets.py +91 -43
  51. django_fast_treenode-2.0.11.dist-info/METADATA +0 -698
  52. django_fast_treenode-2.0.11.dist-info/RECORD +0 -42
  53. treenode/admin.py +0 -439
  54. treenode/docs/Documentation +0 -636
  55. treenode/managers.py +0 -419
  56. treenode/models/proxy.py +0 -669
  57. {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.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