django-fast-treenode 1.1.3__py3-none-any.whl → 2.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-1.1.3.dist-info → django_fast_treenode-2.0.0.dist-info}/METADATA +156 -46
- django_fast_treenode-2.0.0.dist-info/RECORD +41 -0
- {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.0.dist-info}/WHEEL +1 -1
- treenode/__init__.py +0 -7
- treenode/admin.py +327 -82
- treenode/apps.py +20 -3
- treenode/cache.py +231 -0
- treenode/docs/Documentation +130 -54
- treenode/forms.py +75 -19
- treenode/managers.py +260 -48
- treenode/models/__init__.py +7 -0
- treenode/models/classproperty.py +24 -0
- treenode/models/closure.py +168 -0
- treenode/models/factory.py +71 -0
- treenode/models/proxy.py +650 -0
- treenode/static/treenode/css/tree_widget.css +62 -0
- treenode/static/treenode/css/treenode_admin.css +106 -0
- treenode/static/treenode/js/tree_widget.js +161 -0
- treenode/static/treenode/js/treenode_admin.js +171 -0
- treenode/templates/admin/export_success.html +26 -0
- treenode/templates/admin/tree_node_changelist.html +11 -0
- treenode/templates/admin/tree_node_export.html +27 -0
- treenode/templates/admin/tree_node_import.html +27 -0
- treenode/templates/widgets/tree_widget.css +23 -0
- treenode/templates/widgets/tree_widget.html +21 -0
- treenode/urls.py +34 -0
- treenode/utils/__init__.py +4 -0
- treenode/utils/base36.py +35 -0
- treenode/utils/exporter.py +141 -0
- treenode/utils/importer.py +296 -0
- treenode/version.py +11 -1
- treenode/views.py +102 -2
- treenode/widgets.py +49 -27
- django_fast_treenode-1.1.3.dist-info/RECORD +0 -33
- treenode/compat.py +0 -8
- treenode/factory.py +0 -68
- treenode/models.py +0 -668
- treenode/static/select2tree/.gitkeep +0 -1
- treenode/static/select2tree/select2tree.css +0 -176
- treenode/static/select2tree/select2tree.js +0 -181
- treenode/static/treenode/css/treenode.css +0 -85
- treenode/static/treenode/js/treenode.js +0 -201
- treenode/templates/widgets/.gitkeep +0 -1
- treenode/templates/widgets/attrs.html +0 -7
- treenode/templates/widgets/options.html +0 -1
- treenode/templates/widgets/select2tree.html +0 -22
- treenode/tests.py +0 -3
- {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.0.dist-info}/LICENSE +0 -0
- {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.0.dist-info}/top_level.txt +0 -0
- /treenode/{docs → templates/admin}/.gitkeep +0 -0
treenode/models/proxy.py
ADDED
@@ -0,0 +1,650 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Proxy Model
|
4
|
+
|
5
|
+
This module defines an abstract base model `TreeNodeModel` that
|
6
|
+
implements hierarchical data storage using the Adjacency Table method.
|
7
|
+
It integrates with a Closure Table for optimized tree operations.
|
8
|
+
|
9
|
+
Features:
|
10
|
+
- Supports adjacency list representation with parent-child relationships.
|
11
|
+
- Integrates with a Closure Table for efficient ancestor and descendant
|
12
|
+
queries.
|
13
|
+
- Provides a caching mechanism for performance optimization.
|
14
|
+
- Includes methods for tree traversal, manipulation, and serialization.
|
15
|
+
|
16
|
+
Version: 2.0.0
|
17
|
+
Author: Timur Kady
|
18
|
+
Email: timurkady@yandex.com
|
19
|
+
"""
|
20
|
+
|
21
|
+
|
22
|
+
# proxy.py
|
23
|
+
|
24
|
+
from django.db import models, transaction
|
25
|
+
|
26
|
+
from .factory import TreeFactory
|
27
|
+
from .classproperty import classproperty
|
28
|
+
from ..utils.base36 import to_base36
|
29
|
+
from ..managers import TreeNodeModelManager
|
30
|
+
from ..cache import cached_method, treenode_cache
|
31
|
+
import logging
|
32
|
+
|
33
|
+
logger = logging.getLogger(__name__)
|
34
|
+
|
35
|
+
|
36
|
+
class TreeNodeModel(models.Model, metaclass=TreeFactory):
|
37
|
+
"""
|
38
|
+
Abstract TreeNode Model.
|
39
|
+
|
40
|
+
Implements hierarchy storage using the Adjacency Table method.
|
41
|
+
To increase performance, it has an additional attribute - a model
|
42
|
+
that stores data from the Adjacency Table in the form of
|
43
|
+
a Closure Table.
|
44
|
+
"""
|
45
|
+
|
46
|
+
treenode_display_field = None
|
47
|
+
closure_model = None
|
48
|
+
|
49
|
+
tn_parent = models.ForeignKey(
|
50
|
+
'self',
|
51
|
+
related_name='tn_children',
|
52
|
+
on_delete=models.CASCADE,
|
53
|
+
null=True,
|
54
|
+
blank=True
|
55
|
+
)
|
56
|
+
|
57
|
+
tn_priority = models.PositiveIntegerField(default=0)
|
58
|
+
|
59
|
+
objects = TreeNodeModelManager()
|
60
|
+
|
61
|
+
class Meta:
|
62
|
+
"""Meta Class."""
|
63
|
+
|
64
|
+
abstract = True
|
65
|
+
|
66
|
+
def __str__(self):
|
67
|
+
"""Display information about a class object."""
|
68
|
+
if self.treenode_display_field:
|
69
|
+
return str(getattr(self, self.treenode_display_field))
|
70
|
+
else:
|
71
|
+
return 'Node %d' % self.pk
|
72
|
+
|
73
|
+
# ---------------------------------------------------
|
74
|
+
# Public methods
|
75
|
+
# ---------------------------------------------------
|
76
|
+
|
77
|
+
@classmethod
|
78
|
+
def clear_cache(cls):
|
79
|
+
"""Clear cache for this model only."""
|
80
|
+
treenode_cache.invalidate(cls._meta.label)
|
81
|
+
|
82
|
+
@classmethod
|
83
|
+
def get_closure_model(cls):
|
84
|
+
"""Return ClosureModel for class."""
|
85
|
+
return cls.closure_model
|
86
|
+
|
87
|
+
@classmethod
|
88
|
+
def get_roots(cls):
|
89
|
+
"""Get a list with all root nodes."""
|
90
|
+
qs = cls.get_roots_queryset()
|
91
|
+
return list(item for item in qs)
|
92
|
+
|
93
|
+
@classmethod
|
94
|
+
@cached_method
|
95
|
+
def get_roots_queryset(cls):
|
96
|
+
"""Get root nodes queryset with preloaded children."""
|
97
|
+
qs = cls.objects.filter(tn_parent=None).prefetch_related('tn_children')
|
98
|
+
return qs
|
99
|
+
|
100
|
+
@classmethod
|
101
|
+
def get_tree(cls, instance=None):
|
102
|
+
"""Get an n-dimensional dict representing the model tree."""
|
103
|
+
objs_list = [instance] if instance else cls.get_roots()
|
104
|
+
return [item._object2dict(item, []) for item in objs_list]
|
105
|
+
|
106
|
+
@classmethod
|
107
|
+
@cached_method
|
108
|
+
def get_tree_display(cls):
|
109
|
+
"""Get a multiline string representing the model tree."""
|
110
|
+
objs = list(cls.objects.all())
|
111
|
+
return '\n'.join(['%s' % (obj,) for obj in objs])
|
112
|
+
|
113
|
+
@classmethod
|
114
|
+
@transaction.atomic
|
115
|
+
def update_tree(cls):
|
116
|
+
"""Rebuilds the closure table."""
|
117
|
+
# Clear cache
|
118
|
+
cls.closure_model.delete_all()
|
119
|
+
objs = list(cls.objects.all())
|
120
|
+
cls.closure_model.objects.bulk_create(objs, batch_size=1000)
|
121
|
+
cls.clear_cache()
|
122
|
+
|
123
|
+
@classmethod
|
124
|
+
def delete_tree(cls):
|
125
|
+
"""Delete the whole tree for the current node class."""
|
126
|
+
cls.clear_cache()
|
127
|
+
cls.objects.all().delete()
|
128
|
+
cls.closure_model.delete_all()
|
129
|
+
|
130
|
+
# Ancestors -------------------
|
131
|
+
|
132
|
+
def get_ancestors_queryset(self, include_self=True, depth=None):
|
133
|
+
"""Get the ancestors queryset (ordered from parent to root)."""
|
134
|
+
return self.closure_model.get_ancestors_queryset(
|
135
|
+
self, include_self, depth)
|
136
|
+
|
137
|
+
def get_ancestors(self, include_self=True, depth=None):
|
138
|
+
"""Get a list with all ancestors (ordered from root to self/parent)."""
|
139
|
+
return list(
|
140
|
+
self.get_ancestors_queryset(include_self, depth).iterator()
|
141
|
+
)
|
142
|
+
|
143
|
+
def get_ancestors_count(self, include_self=True, depth=None):
|
144
|
+
"""Get the ancestors count."""
|
145
|
+
return self.get_ancestors_queryset(include_self, depth).count()
|
146
|
+
|
147
|
+
def get_ancestors_pks(self, include_self=True, depth=None):
|
148
|
+
"""Get the ancestors pks list."""
|
149
|
+
qs = self.get_ancestors_queryset(include_self, depth).only('pk')
|
150
|
+
return [ch.pk for ch in qs] if qs else []
|
151
|
+
|
152
|
+
# Children --------------------
|
153
|
+
|
154
|
+
@cached_method
|
155
|
+
def get_children_queryset(self):
|
156
|
+
"""Get the children queryset with prefetch."""
|
157
|
+
return self.tn_children.prefetch_related('tn_children')
|
158
|
+
|
159
|
+
def get_children(self):
|
160
|
+
"""Get a list containing all children."""
|
161
|
+
return list(self.get_children_queryset().iterator())
|
162
|
+
|
163
|
+
def get_children_count(self):
|
164
|
+
"""Get the children count."""
|
165
|
+
return self.get_children_queryset().count()
|
166
|
+
|
167
|
+
def get_children_pks(self):
|
168
|
+
"""Get the children pks list."""
|
169
|
+
return [ch.pk for ch in self.get_children_queryset().only('pk')]
|
170
|
+
|
171
|
+
# Descendants -----------------
|
172
|
+
|
173
|
+
def get_descendants_queryset(self, include_self=False, depth=None):
|
174
|
+
"""Get the descendants queryset."""
|
175
|
+
return self.closure_model.get_descendants_queryset(
|
176
|
+
self, include_self, depth
|
177
|
+
)
|
178
|
+
|
179
|
+
def get_descendants(self, include_self=False, depth=None):
|
180
|
+
"""Get a list containing all descendants."""
|
181
|
+
return list(
|
182
|
+
self.get_descendants_queryset(include_self, depth).iterator()
|
183
|
+
)
|
184
|
+
|
185
|
+
def get_descendants_count(self, include_self=False, depth=None):
|
186
|
+
"""Get the descendants count."""
|
187
|
+
return self.get_descendants_queryset(include_self, depth).count()
|
188
|
+
|
189
|
+
def get_descendants_pks(self, include_self=False, depth=None):
|
190
|
+
"""Get the descendants pks list."""
|
191
|
+
qs = self.get_descendants_queryset(include_self, depth)
|
192
|
+
return [ch.pk for ch in qs] if qs else []
|
193
|
+
|
194
|
+
# Siblings --------------------
|
195
|
+
|
196
|
+
@cached_method
|
197
|
+
def get_siblings_queryset(self):
|
198
|
+
"""Get the siblings queryset with prefetch."""
|
199
|
+
if self.tn_parent:
|
200
|
+
qs = self.tn_parent.tn_children.prefetch_related('tn_children')
|
201
|
+
else:
|
202
|
+
qs = self._meta.model.objects.filter(tn_parent__isnull=True)
|
203
|
+
return qs.exclude(pk=self.pk)
|
204
|
+
|
205
|
+
def get_siblings(self):
|
206
|
+
"""Get a list with all the siblings."""
|
207
|
+
return list(self.get_siblings_queryset())
|
208
|
+
|
209
|
+
def get_siblings_count(self):
|
210
|
+
"""Get the siblings count."""
|
211
|
+
return self.get_siblings_queryset().count()
|
212
|
+
|
213
|
+
def get_siblings_pks(self):
|
214
|
+
"""Get the siblings pks list."""
|
215
|
+
return [item.pk for item in self.get_siblings_queryset()]
|
216
|
+
|
217
|
+
# -----------------------------
|
218
|
+
|
219
|
+
def get_breadcrumbs(self, attr=None):
|
220
|
+
"""Get the breadcrumbs to current node (self, included)."""
|
221
|
+
return self.closure_model.get_breadcrumbs(self, attr)
|
222
|
+
|
223
|
+
def get_depth(self):
|
224
|
+
"""Get the node depth (self, how many levels of descendants)."""
|
225
|
+
return self.closure_model.get_depth(self)
|
226
|
+
|
227
|
+
def get_first_child(self):
|
228
|
+
"""Get the first child node."""
|
229
|
+
return self.get_children_queryset().first()
|
230
|
+
|
231
|
+
@cached_method
|
232
|
+
def get_index(self):
|
233
|
+
"""Get the node index (self, index in node.parent.children list)."""
|
234
|
+
if self.tn_parent is None:
|
235
|
+
return self.tn_priority
|
236
|
+
source = list(self.tn_parent.tn_children.all())
|
237
|
+
return source.index(self) if self in source else self.tn_priority
|
238
|
+
|
239
|
+
def get_order(self):
|
240
|
+
"""Return the materialized path."""
|
241
|
+
path = self.closure_model.get_breadcrumbs(self, attr='tn_priority')
|
242
|
+
segments = [to_base36(i).rjust(6, '0') for i in path]
|
243
|
+
return ''.join(segments)
|
244
|
+
|
245
|
+
def get_last_child(self):
|
246
|
+
"""Get the last child node."""
|
247
|
+
return self.get_children_queryset().last()
|
248
|
+
|
249
|
+
def get_level(self):
|
250
|
+
"""Get the node level (self, starting from 1)."""
|
251
|
+
return self.closure_model.get_level(self)
|
252
|
+
|
253
|
+
def get_path(self, prefix='', suffix='', delimiter='.', format_str=''):
|
254
|
+
"""Return Materialized Path of node."""
|
255
|
+
path = self.closure_model.get_path(self, delimiter, format_str)
|
256
|
+
return prefix+path+suffix
|
257
|
+
|
258
|
+
@cached_method
|
259
|
+
def get_parent(self):
|
260
|
+
"""Get the parent node."""
|
261
|
+
return self.tn_parent
|
262
|
+
|
263
|
+
def set_parent(self, parent_obj):
|
264
|
+
"""Set the parent node."""
|
265
|
+
self._meta.model.clear_cache()
|
266
|
+
self.tn_parent = parent_obj
|
267
|
+
self.save()
|
268
|
+
|
269
|
+
def get_parent_pk(self):
|
270
|
+
"""Get the parent node pk."""
|
271
|
+
return self.get_parent().pk if self.tn_parent else None
|
272
|
+
|
273
|
+
@cached_method
|
274
|
+
def get_priority(self):
|
275
|
+
"""Get the node priority."""
|
276
|
+
return self.tn_priority
|
277
|
+
|
278
|
+
def set_priority(self, priority=0):
|
279
|
+
"""Set the node priority."""
|
280
|
+
self._meta.model.clear_cache()
|
281
|
+
self.tn_priority = priority
|
282
|
+
self.save()
|
283
|
+
|
284
|
+
def get_root(self):
|
285
|
+
"""Get the root node for the current node."""
|
286
|
+
return self.closure_model.get_root(self)
|
287
|
+
|
288
|
+
def get_root_pk(self):
|
289
|
+
"""Get the root node pk for the current node."""
|
290
|
+
root = self.get_root()
|
291
|
+
return root.pk if root else None
|
292
|
+
|
293
|
+
# Logics ----------------------
|
294
|
+
|
295
|
+
def is_ancestor_of(self, target_obj):
|
296
|
+
"""Return True if the current node is ancestor of target_obj."""
|
297
|
+
return self in target_obj.get_ancestors(include_self=False)
|
298
|
+
|
299
|
+
def is_child_of(self, target_obj):
|
300
|
+
"""Return True if the current node is child of target_obj."""
|
301
|
+
return self in target_obj.get_children()
|
302
|
+
|
303
|
+
def is_descendant_of(self, target_obj):
|
304
|
+
"""Return True if the current node is descendant of target_obj."""
|
305
|
+
return self in target_obj.get_descendants()
|
306
|
+
|
307
|
+
def is_first_child(self):
|
308
|
+
"""Return True if the current node is the first child."""
|
309
|
+
return self.tn_priority == 0
|
310
|
+
|
311
|
+
def is_last_child(self):
|
312
|
+
"""Return True if the current node is the last child."""
|
313
|
+
return self.tn_priority == self.get_siblings_count() - 1
|
314
|
+
|
315
|
+
def is_leaf(self):
|
316
|
+
"""Return True if the current node is a leaf."""
|
317
|
+
return self.tn_children.count() == 0
|
318
|
+
|
319
|
+
def is_parent_of(self, target_obj):
|
320
|
+
"""Return True if the current node is parent of target_obj."""
|
321
|
+
return self == target_obj.tn_parent
|
322
|
+
|
323
|
+
def is_root(self):
|
324
|
+
"""Return True if the current node is root."""
|
325
|
+
return self.tn_parent is None
|
326
|
+
|
327
|
+
def is_root_of(self, target_obj):
|
328
|
+
"""Return True if the current node is root of target_obj."""
|
329
|
+
return self == target_obj.get_root()
|
330
|
+
|
331
|
+
def is_sibling_of(self, target_obj):
|
332
|
+
"""Return True if the current node is sibling of target_obj."""
|
333
|
+
if target_obj.tn_parent is None and self.tn_parent is None:
|
334
|
+
# Both objects are roots
|
335
|
+
return True
|
336
|
+
return (self.tn_parent == target_obj.tn_parent)
|
337
|
+
|
338
|
+
def delete(self, cascade=True):
|
339
|
+
"""Delete node."""
|
340
|
+
model = self._meta.model
|
341
|
+
|
342
|
+
if not cascade:
|
343
|
+
# Get a list of children
|
344
|
+
children = self.get_children()
|
345
|
+
# Move them to one level up
|
346
|
+
for child in children:
|
347
|
+
child.tn_parent = self.tn_parent
|
348
|
+
# Udate both models in bulk
|
349
|
+
model.objects.bulk_update(
|
350
|
+
children,
|
351
|
+
("tn_parent",),
|
352
|
+
batch_size=1000
|
353
|
+
)
|
354
|
+
# All descendants and related records in the ClosingModel will be
|
355
|
+
# cleared by cascading the removal of ForeignKeys.
|
356
|
+
super().delete()
|
357
|
+
# Can be excluded. The cache has already been cleared by the manager.
|
358
|
+
model.clear_cache()
|
359
|
+
|
360
|
+
def save(self, force_insert=False, *args, **kwargs):
|
361
|
+
"""Save method."""
|
362
|
+
# --- 1. Preparations -------------------------------------------------
|
363
|
+
is_new = self.pk is None
|
364
|
+
is_move = False
|
365
|
+
old_parent = None
|
366
|
+
old_priority = None
|
367
|
+
model = self._meta.model
|
368
|
+
closure_model = self.closure_model
|
369
|
+
|
370
|
+
# --- 2. Check mode _-------------------------------------------------
|
371
|
+
# If the object already exists in the DB, we'll extract its old parent
|
372
|
+
if not is_new:
|
373
|
+
ql = model.objects.filter(pk=self.pk).values_list(
|
374
|
+
'tn_parent',
|
375
|
+
'tn_priority').first()
|
376
|
+
old_parent = ql[0]
|
377
|
+
old_priority = ql[1]
|
378
|
+
is_move = old_priority != self.tn_priority
|
379
|
+
|
380
|
+
# Check if we are moving the node into itself (child).
|
381
|
+
# If old parent != self.tn_parent, "moving" is possible.
|
382
|
+
if old_parent and old_parent != self.tn_parent:
|
383
|
+
# Let's make sure we don't move into our descendant
|
384
|
+
descendants = self.get_descendants_queryset()
|
385
|
+
if self.tn_parent and self.tn_parent.pk in {
|
386
|
+
d.pk for d in descendants}:
|
387
|
+
raise ValueError("You cannot move a node into its own child.")
|
388
|
+
|
389
|
+
# --- 3. Saving ------------------------------------------------------
|
390
|
+
super().save(force_insert=force_insert, *args, **kwargs)
|
391
|
+
|
392
|
+
# --- 4. Synchronization with Closure Model --------------------------
|
393
|
+
if is_new:
|
394
|
+
closure_model.insert_node(self)
|
395
|
+
|
396
|
+
# If the parent has changed, we move it
|
397
|
+
if (old_parent != self.tn_parent):
|
398
|
+
closure_model.move_node(self)
|
399
|
+
|
400
|
+
# --- 5. Update siblings ---------------------------------------------
|
401
|
+
if is_new or is_move:
|
402
|
+
# Now we need recalculate tn_priority
|
403
|
+
self._update_priority()
|
404
|
+
else:
|
405
|
+
self._meta.model.clear_cache()
|
406
|
+
|
407
|
+
# ---------------------------------------------------
|
408
|
+
# Public properties
|
409
|
+
#
|
410
|
+
# All properties map a get_{{property}}() method.
|
411
|
+
# ---------------------------------------------------
|
412
|
+
|
413
|
+
@property
|
414
|
+
def ancestors(self):
|
415
|
+
"""Get a list with all ancestors; self included."""
|
416
|
+
return self.get_ancestors()
|
417
|
+
|
418
|
+
@property
|
419
|
+
def ancestors_count(self):
|
420
|
+
"""Get the ancestors count."""
|
421
|
+
return self.get_ancestors_count()
|
422
|
+
|
423
|
+
@property
|
424
|
+
def ancestors_pks(self):
|
425
|
+
"""Get the ancestors pks list; self included."""
|
426
|
+
return self.get_ancestors_pks()
|
427
|
+
|
428
|
+
@property
|
429
|
+
def breadcrumbs(self):
|
430
|
+
"""Get the breadcrumbs to current node (self, included)."""
|
431
|
+
return self.get_breadcrumbs()
|
432
|
+
|
433
|
+
@property
|
434
|
+
def children(self):
|
435
|
+
"""Get a list containing all children; self included."""
|
436
|
+
return self.get_children()
|
437
|
+
|
438
|
+
@property
|
439
|
+
def children_count(self):
|
440
|
+
"""Get the children count."""
|
441
|
+
return self.get_children_count()
|
442
|
+
|
443
|
+
@property
|
444
|
+
def children_pks(self):
|
445
|
+
"""Get the children pks list."""
|
446
|
+
return self.get_children_pks()
|
447
|
+
|
448
|
+
@property
|
449
|
+
def depth(self):
|
450
|
+
"""Get the node depth."""
|
451
|
+
return self.get_depth()
|
452
|
+
|
453
|
+
@property
|
454
|
+
def descendants(self):
|
455
|
+
"""Get a list containing all descendants; self not included."""
|
456
|
+
return self.get_descendants()
|
457
|
+
|
458
|
+
@property
|
459
|
+
def descendants_count(self):
|
460
|
+
"""Get the descendants count; self not included."""
|
461
|
+
return self.get_descendants_count()
|
462
|
+
|
463
|
+
@property
|
464
|
+
def descendants_pks(self):
|
465
|
+
"""Get the descendants pks list; self not included."""
|
466
|
+
return self.get_descendants_pks()
|
467
|
+
|
468
|
+
@property
|
469
|
+
def descendants_tree(self):
|
470
|
+
"""Get a n-dimensional dict representing the model tree."""
|
471
|
+
return self.get_descendants_tree()
|
472
|
+
|
473
|
+
@property
|
474
|
+
def descendants_tree_display(self):
|
475
|
+
"""Get a multiline string representing the model tree."""
|
476
|
+
return self.get_descendants_tree_display()
|
477
|
+
|
478
|
+
@property
|
479
|
+
def first_child(self):
|
480
|
+
"""Get the first child node."""
|
481
|
+
return self.get_first_child()
|
482
|
+
|
483
|
+
@property
|
484
|
+
def index(self):
|
485
|
+
"""Get the node index."""
|
486
|
+
return self.get_index()
|
487
|
+
|
488
|
+
@property
|
489
|
+
def last_child(self):
|
490
|
+
"""Get the last child node."""
|
491
|
+
return self.get_last_child()
|
492
|
+
|
493
|
+
@property
|
494
|
+
def level(self):
|
495
|
+
"""Get the node level."""
|
496
|
+
return self.get_level()
|
497
|
+
|
498
|
+
@property
|
499
|
+
def parent(self):
|
500
|
+
"""Get node parent."""
|
501
|
+
return self.tn_parent
|
502
|
+
|
503
|
+
@property
|
504
|
+
def parent_pk(self):
|
505
|
+
"""Get node parent pk."""
|
506
|
+
return self.get_parent_pk()
|
507
|
+
|
508
|
+
@property
|
509
|
+
def priority(self):
|
510
|
+
"""Get node priority."""
|
511
|
+
return self.get_priority()
|
512
|
+
|
513
|
+
@classproperty
|
514
|
+
def roots(cls):
|
515
|
+
"""Get a list with all root nodes."""
|
516
|
+
return cls.get_roots()
|
517
|
+
|
518
|
+
@property
|
519
|
+
def root(self):
|
520
|
+
"""Get the root node for the current node."""
|
521
|
+
return self.get_root()
|
522
|
+
|
523
|
+
@property
|
524
|
+
def root_pk(self):
|
525
|
+
"""Get the root node pk for the current node."""
|
526
|
+
return self.get_root_pk()
|
527
|
+
|
528
|
+
@property
|
529
|
+
def siblings(self):
|
530
|
+
"""Get a list with all the siblings."""
|
531
|
+
return self.get_siblings()
|
532
|
+
|
533
|
+
@property
|
534
|
+
def siblings_count(self):
|
535
|
+
"""Get the siblings count."""
|
536
|
+
return self.get_siblings_count()
|
537
|
+
|
538
|
+
@property
|
539
|
+
def siblings_pks(self):
|
540
|
+
"""Get the siblings pks list."""
|
541
|
+
return self.get_siblings_pks()
|
542
|
+
|
543
|
+
@classproperty
|
544
|
+
def tree(cls):
|
545
|
+
"""Get an n-dimensional dict representing the model tree."""
|
546
|
+
return cls.get_tree()
|
547
|
+
|
548
|
+
@classproperty
|
549
|
+
def tree_display(cls):
|
550
|
+
"""Get a multiline string representing the model tree."""
|
551
|
+
return cls.get_tree_display()
|
552
|
+
|
553
|
+
@property
|
554
|
+
def tn_order(self):
|
555
|
+
"""Return the materialized path."""
|
556
|
+
return self.get_order()
|
557
|
+
|
558
|
+
# ---------------------------------------------------
|
559
|
+
# Prived methods
|
560
|
+
#
|
561
|
+
# The usage of these methods is only allowed by developers. In future
|
562
|
+
# versions, these methods may be changed or removed without any warning.
|
563
|
+
# ---------------------------------------------------
|
564
|
+
|
565
|
+
def _update_priority(self):
|
566
|
+
"""Update tn_priority field for siblings."""
|
567
|
+
if self.tn_parent is None:
|
568
|
+
# Node is a root
|
569
|
+
parent = None
|
570
|
+
queryset = self._meta.model.get_roots_queryset()
|
571
|
+
else:
|
572
|
+
# Node isn't a root
|
573
|
+
parent = self.tn_parent
|
574
|
+
queryset = parent.tn_children.all()
|
575
|
+
|
576
|
+
siblings = list(queryset.exclude(pk=self.pk))
|
577
|
+
sorted_siblings = sorted(siblings, key=lambda x: x.tn_priority)
|
578
|
+
insert_pos = min(self.tn_priority, len(sorted_siblings))
|
579
|
+
sorted_siblings.insert(insert_pos, self)
|
580
|
+
for index, node in enumerate(sorted_siblings):
|
581
|
+
node.tn_priority = index
|
582
|
+
# Save changes
|
583
|
+
model = self._meta.model
|
584
|
+
with transaction.atomic():
|
585
|
+
model.objects.bulk_update(sorted_siblings, ('tn_priority',), 1000)
|
586
|
+
super().save(update_fields=['tn_priority'])
|
587
|
+
model.clear_cache()
|
588
|
+
|
589
|
+
def _object2dict(self, instance, exclude=None, visited=None):
|
590
|
+
"""
|
591
|
+
Convert a class instance to a dictionary.
|
592
|
+
|
593
|
+
:param instance: The object instance to convert.
|
594
|
+
:param exclude: List of attribute names to exclude.
|
595
|
+
:param visited: Set of visited objects to prevent circular references.
|
596
|
+
:return: A dictionary representation of the object.
|
597
|
+
"""
|
598
|
+
if exclude is None:
|
599
|
+
exclude = set()
|
600
|
+
if visited is None:
|
601
|
+
visited = set()
|
602
|
+
|
603
|
+
# Prevent infinite recursion by tracking visited objects
|
604
|
+
if id(instance) in visited:
|
605
|
+
raise RecursionError("Cycle detected in tree structure.")
|
606
|
+
|
607
|
+
visited.add(id(instance))
|
608
|
+
|
609
|
+
# Если объект не является моделью Django, просто вернуть его
|
610
|
+
if not isinstance(instance, models.Model):
|
611
|
+
return instance
|
612
|
+
|
613
|
+
# If the object has no `__dict__`, return its direct value
|
614
|
+
if not hasattr(instance, '__dict__'):
|
615
|
+
return instance
|
616
|
+
|
617
|
+
result = {}
|
618
|
+
|
619
|
+
for key, value in vars(instance).items():
|
620
|
+
if key.startswith('_') or key in exclude:
|
621
|
+
continue
|
622
|
+
|
623
|
+
# Recursively process nested objects
|
624
|
+
if isinstance(value, (list, tuple, set)):
|
625
|
+
result[key] = [
|
626
|
+
self._object2dict(v, exclude, visited) for v in value
|
627
|
+
]
|
628
|
+
elif isinstance(value, dict):
|
629
|
+
result[key] = {
|
630
|
+
k: self._object2dict(v, exclude, visited)
|
631
|
+
for k, v in value.items()
|
632
|
+
}
|
633
|
+
else:
|
634
|
+
result[key] = self._object2dict(value, exclude, visited)
|
635
|
+
|
636
|
+
# Include children
|
637
|
+
children = instance.tn_children.all()
|
638
|
+
if children.exists():
|
639
|
+
result['children'] = [
|
640
|
+
self._object2dict(child, exclude, visited)
|
641
|
+
for child in children
|
642
|
+
]
|
643
|
+
|
644
|
+
# Add path information
|
645
|
+
result['path'] = instance.get_path(format_str=':d')
|
646
|
+
|
647
|
+
return result
|
648
|
+
|
649
|
+
|
650
|
+
# The end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
/*
|
2
|
+
TreeNode Select2 Widget Stylesheet
|
3
|
+
|
4
|
+
This stylesheet customizes the Select2 dropdown widget for hierarchical
|
5
|
+
data representation in Django admin. It ensures compatibility with both
|
6
|
+
light and dark themes.
|
7
|
+
|
8
|
+
Features:
|
9
|
+
- Dark theme styling for the dropdown and selected items.
|
10
|
+
- Consistent color scheme for better readability.
|
11
|
+
- Custom styling for search fields and selection indicators.
|
12
|
+
- Enhances usability in tree-based data selection.
|
13
|
+
|
14
|
+
Version: 2.0.0
|
15
|
+
Author: Timur Kady
|
16
|
+
Email: timurkady@yandex.com
|
17
|
+
*/
|
18
|
+
|
19
|
+
/* Select2 dropdown */
|
20
|
+
.select2-dropdown.dark-theme {
|
21
|
+
background-color: #2c2c2c !important;
|
22
|
+
border: 1px solid #444 !important;
|
23
|
+
color: #ddd !important;
|
24
|
+
}
|
25
|
+
|
26
|
+
/* List of options */
|
27
|
+
.select2-dropdown.dark-theme .select2-results__option {
|
28
|
+
background-color: transparent !important;
|
29
|
+
color: #ddd !important;
|
30
|
+
}
|
31
|
+
|
32
|
+
/* Hover/selected option */
|
33
|
+
.select2-dropdown.dark-theme .select2-results__option--highlighted,
|
34
|
+
.select2-dropdown.dark-theme .select2-results__option--selected {
|
35
|
+
background-color: #555 !important;
|
36
|
+
color: #fff !important;
|
37
|
+
}
|
38
|
+
|
39
|
+
/* Search field */
|
40
|
+
.select2-dropdown.dark-theme .select2-search.select2-search--dropdown .select2-search__field {
|
41
|
+
background-color: #2c2c2c !important;
|
42
|
+
color: #ddd !important;
|
43
|
+
border: 1px solid #444 !important;
|
44
|
+
outline: none !important;
|
45
|
+
}
|
46
|
+
|
47
|
+
/* Container of the selected element (if we want to darken it too) */
|
48
|
+
.select2-container--default .select2-selection--single.dark-theme {
|
49
|
+
background-color: #2c2c2c !important;
|
50
|
+
border: 1px solid #444 !important;
|
51
|
+
color: #ddd !important;
|
52
|
+
}
|
53
|
+
|
54
|
+
/* Arrow */
|
55
|
+
.select2-container--default .select2-selection--single.dark-theme .select2-selection__arrow b {
|
56
|
+
border-color: #ddd transparent transparent transparent !important;
|
57
|
+
}
|
58
|
+
|
59
|
+
/* Text of the selected element */
|
60
|
+
.select2-container--default .select2-selection--single.dark-theme .select2-selection__rendered {
|
61
|
+
color: #ddd !important;
|
62
|
+
}
|