django-fast-treenode 2.0.10__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.10.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.10.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 +33 -22
- 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 +39 -65
- 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/tree_node_import.html +27 -9
- treenode/templates/admin/tree_node_import_report.html +32 -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 +63 -36
- treenode/utils/importer.py +168 -161
- treenode/utils/radix.py +61 -0
- treenode/version.py +2 -2
- treenode/views.py +119 -38
- treenode/widgets.py +104 -40
- django_fast_treenode-2.0.10.dist-info/METADATA +0 -698
- django_fast_treenode-2.0.10.dist-info/RECORD +0 -41
- treenode/admin.py +0 -396
- treenode/docs/Documentation +0 -664
- treenode/managers.py +0 -281
- treenode/models/proxy.py +0 -650
- {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/top_level.txt +0 -0
treenode/managers.py
DELETED
@@ -1,281 +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.0
|
15
|
-
Author: Timur Kady
|
16
|
-
Email: timurkady@yandex.com
|
17
|
-
"""
|
18
|
-
|
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
|
-
# ----------------------------------------------------------------------------
|
222
|
-
|
223
|
-
|
224
|
-
class TreeNodeQuerySet(models.QuerySet):
|
225
|
-
"""TreeNodeModel QuerySet."""
|
226
|
-
|
227
|
-
def __init__(self, model=None, query=None, using=None, hints=None):
|
228
|
-
"""Init."""
|
229
|
-
self.closure_model = model.closure_model
|
230
|
-
super().__init__(model, query, using, hints)
|
231
|
-
|
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
|
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])
|
247
|
-
|
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
|
258
|
-
|
259
|
-
|
260
|
-
class TreeNodeModelManager(models.Manager):
|
261
|
-
"""TreeNodeModel Manager."""
|
262
|
-
|
263
|
-
def bulk_create(self, objs, batch_size=1000, ignore_conflicts=False):
|
264
|
-
"""
|
265
|
-
Bulk Create.
|
266
|
-
|
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
|
274
|
-
)
|
275
|
-
|
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
|