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.py
DELETED
@@ -1,668 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
"""
|
3
|
-
TreeNode Models Module
|
4
|
-
|
5
|
-
"""
|
6
|
-
|
7
|
-
|
8
|
-
from django.db import models
|
9
|
-
from django.db import transaction
|
10
|
-
from django.core.cache import caches
|
11
|
-
from django.utils.translation import gettext_lazy as _
|
12
|
-
from six import with_metaclass
|
13
|
-
from . import classproperty
|
14
|
-
from .compat import force_str
|
15
|
-
from .factory import TreeFactory
|
16
|
-
from .managers import TreeNodeManager
|
17
|
-
|
18
|
-
|
19
|
-
treenode_cache = caches['treenode']
|
20
|
-
|
21
|
-
def cached_tree_method(func):
|
22
|
-
"""
|
23
|
-
Decorator to cache the results of tree methods
|
24
|
-
|
25
|
-
The decorator caches the results of the decorated method using the
|
26
|
-
closure_path of the node as part of the cache key. If the cache is
|
27
|
-
cleared or invalidated, the cached results will be recomputed.
|
28
|
-
|
29
|
-
Usage:
|
30
|
-
@cached_tree_method
|
31
|
-
def my_tree_method(self):
|
32
|
-
# Tree method logic
|
33
|
-
"""
|
34
|
-
|
35
|
-
def wrapper(self, *args, **kwargs):
|
36
|
-
cache_key = f"{self.__class__.__name__}_{self.pk}_tree_{func.__name__}"
|
37
|
-
result = treenode_cache.get(cache_key)
|
38
|
-
|
39
|
-
if result is None:
|
40
|
-
result = func(self, *args, **kwargs)
|
41
|
-
treenode_cache.set(cache_key, result)
|
42
|
-
|
43
|
-
return result
|
44
|
-
|
45
|
-
return wrapper
|
46
|
-
|
47
|
-
|
48
|
-
class TreeNodeModel(with_metaclass(TreeFactory, models.Model)):
|
49
|
-
|
50
|
-
treenode_display_field = None
|
51
|
-
|
52
|
-
tn_parent = models.ForeignKey(
|
53
|
-
'self',
|
54
|
-
related_name='tn_children',
|
55
|
-
on_delete=models.CASCADE,
|
56
|
-
null=True,
|
57
|
-
blank=True,
|
58
|
-
verbose_name=_('Parent'),
|
59
|
-
)
|
60
|
-
|
61
|
-
tn_priority = models.PositiveIntegerField(
|
62
|
-
default=0,
|
63
|
-
verbose_name=_('Priority'),
|
64
|
-
)
|
65
|
-
|
66
|
-
objects = TreeNodeManager()
|
67
|
-
|
68
|
-
class Meta:
|
69
|
-
abstract = True
|
70
|
-
|
71
|
-
def __str__(self):
|
72
|
-
if self.treenode_display_field:
|
73
|
-
return str(getattr(self, self.treenode_display_field))
|
74
|
-
else:
|
75
|
-
return 'Node %d' % self.pk
|
76
|
-
|
77
|
-
# Public methods
|
78
|
-
|
79
|
-
@classmethod
|
80
|
-
def get_roots(cls):
|
81
|
-
"""Get a list with all root nodes"""
|
82
|
-
return list(item for item in cls.get_roots_queryset())
|
83
|
-
|
84
|
-
@classmethod
|
85
|
-
@cached_tree_method
|
86
|
-
def get_roots_queryset(cls):
|
87
|
-
"""Get root nodes queryset"""
|
88
|
-
return cls.objects.filter(tn_parent=None)
|
89
|
-
|
90
|
-
@classmethod
|
91
|
-
@cached_tree_method
|
92
|
-
def get_tree(cls, instance=None):
|
93
|
-
"""Get a n-dimensional dict representing the model tree"""
|
94
|
-
|
95
|
-
objs_list = list(instance,) if instance else cls.get_roots()
|
96
|
-
objs_tree = list()
|
97
|
-
for item in objs_list:
|
98
|
-
objs_tree.append(item.object2dict(item, []))
|
99
|
-
return objs_tree
|
100
|
-
|
101
|
-
@classmethod
|
102
|
-
@cached_tree_method
|
103
|
-
def get_tree_display(cls, cache=True):
|
104
|
-
"""Get a multiline string representing the model tree"""
|
105
|
-
|
106
|
-
objs = list(cls.objects.all())
|
107
|
-
return '\n'.join(['%s' % (obj,) for obj in objs])
|
108
|
-
|
109
|
-
@classmethod
|
110
|
-
def update_tree(cls):
|
111
|
-
"""Update tree manually, useful after bulk updates"""
|
112
|
-
|
113
|
-
treenode_cache.clear()
|
114
|
-
|
115
|
-
cls.closure_model.objects.all().delete()
|
116
|
-
|
117
|
-
# Apparently later, you should think about using iterators to reduce
|
118
|
-
# the amount of memory used.
|
119
|
-
# I'm sorry, I don't have time for that right now.
|
120
|
-
|
121
|
-
cls.closure_model.objects.bulk_create([
|
122
|
-
cls.closure_model(
|
123
|
-
parent_id=item.pk,
|
124
|
-
child_id=item.pk,
|
125
|
-
depth=0
|
126
|
-
)
|
127
|
-
for item in cls.objects.all()
|
128
|
-
])
|
129
|
-
|
130
|
-
for node in cls.objects.all():
|
131
|
-
queryset = cls.closure_model.objects.all()
|
132
|
-
parents = queryset.filter(
|
133
|
-
child=node.parent).values('parent', 'depth')
|
134
|
-
children = queryset.filter(
|
135
|
-
parent=node).values('child', 'depth')
|
136
|
-
objects = [
|
137
|
-
cls.closure_model(
|
138
|
-
parent_id=p['parent'],
|
139
|
-
child_id=c['child'],
|
140
|
-
depth=p['depth'] + c['depth'] + 1
|
141
|
-
)
|
142
|
-
for p in parents
|
143
|
-
for c in children
|
144
|
-
]
|
145
|
-
cls.closure_model.objects.bulk_create(objects)
|
146
|
-
|
147
|
-
@classmethod
|
148
|
-
def delete_tree(cls):
|
149
|
-
"""Delete the whole tree for the current node class"""
|
150
|
-
treenode_cache.clear()
|
151
|
-
cls.closure_model.objects.all().delete()
|
152
|
-
cls.objects.all().delete()
|
153
|
-
|
154
|
-
def get_ancestors(self, include_self=True, depth=None):
|
155
|
-
"""Get a list with all ancestors (ordered from root to self/parent)"""
|
156
|
-
|
157
|
-
options = dict(child_id=self.pk, depth__gte=0 if include_self else 1)
|
158
|
-
if depth:
|
159
|
-
options.update({'depth__lte': depth})
|
160
|
-
|
161
|
-
qs = self._closure_model.objects.filter(**options).order_by('-depth')
|
162
|
-
|
163
|
-
return list(item.parent for item in qs)
|
164
|
-
|
165
|
-
def get_ancestors_count(self, include_self=True, depth=None):
|
166
|
-
"""Get the ancestors count"""
|
167
|
-
|
168
|
-
return self.get_ancestors_queryset(include_self, depth).count()
|
169
|
-
|
170
|
-
def get_ancestors_pks(self, include_self=True, depth=None):
|
171
|
-
"""Get the ancestors pks list"""
|
172
|
-
|
173
|
-
options = dict(child_id=self.pk, depth__gte=0 if include_self else 1)
|
174
|
-
if depth:
|
175
|
-
options.update({'depth__lte': depth})
|
176
|
-
|
177
|
-
qs = self._closure_model.objects.filter(**options).order_by('-depth')
|
178
|
-
|
179
|
-
return list(item.parent.pk for item in qs)
|
180
|
-
|
181
|
-
@cached_tree_method
|
182
|
-
def get_ancestors_queryset(self, include_self=True, depth=None):
|
183
|
-
"""Get the ancestors queryset (self, ordered from parent to root)"""
|
184
|
-
|
185
|
-
options = dict(child_id=self.pk, depth__gte=0 if include_self else 1)
|
186
|
-
if depth:
|
187
|
-
options.update({'depth__lte': depth})
|
188
|
-
|
189
|
-
qs = self._closure_model.objects.filter(**options).order_by('-depth')
|
190
|
-
select = list(item.parent.pk for item in qs)
|
191
|
-
result = self._meta.model.objects.filter(pk__in=select)
|
192
|
-
return result
|
193
|
-
|
194
|
-
@cached_tree_method
|
195
|
-
def get_breadcrumbs(self, attr=None):
|
196
|
-
"""Get the breadcrumbs to current node (self, included)"""
|
197
|
-
|
198
|
-
qs = self._closure_model.objects.filter(child=self).order_by('-depth')
|
199
|
-
if attr:
|
200
|
-
return list(getattr(item.parent, attr) for item in qs)
|
201
|
-
else:
|
202
|
-
return list(item.parent for item in qs)
|
203
|
-
|
204
|
-
def get_children(self):
|
205
|
-
"""Get a list containing all children"""
|
206
|
-
|
207
|
-
return list(self.self.get_children_queryset())
|
208
|
-
|
209
|
-
def get_children_count(self):
|
210
|
-
"""Get the children count"""
|
211
|
-
|
212
|
-
return self.get_children_queryset().count()
|
213
|
-
|
214
|
-
def get_children_pks(self):
|
215
|
-
"""Get the children pks list"""
|
216
|
-
|
217
|
-
return [ch.pk for ch in self.get_children_queryset()]
|
218
|
-
|
219
|
-
@cached_tree_method
|
220
|
-
def get_children_queryset(self):
|
221
|
-
"""Get the children queryset"""
|
222
|
-
return self._meta.model.objects.filter(tn_parent=self.id)
|
223
|
-
|
224
|
-
def get_depth(self):
|
225
|
-
"""Get the node depth (self, how many levels of descendants)"""
|
226
|
-
|
227
|
-
depths = self._closure_model.objects.filter(parent=self).values_list(
|
228
|
-
'depth',
|
229
|
-
flat=True
|
230
|
-
)
|
231
|
-
return max(depths)
|
232
|
-
|
233
|
-
def get_descendants(self, include_self=False, depth=None):
|
234
|
-
"""Get a list containing all descendants"""
|
235
|
-
return list(self.get_descendants_queryset(include_self, depth))
|
236
|
-
|
237
|
-
def get_descendants_count(self, include_self=False, depth=None):
|
238
|
-
"""Get the descendants count"""
|
239
|
-
return self.get_descendants_queryset().count()
|
240
|
-
|
241
|
-
def get_descendants_pks(self, include_self=False, depth=None):
|
242
|
-
"""Get the descendants pks list"""
|
243
|
-
options = dict(parent_id=self.pk, depth__gte=0 if include_self else 1)
|
244
|
-
if depth:
|
245
|
-
options.update({'depth__lte': depth})
|
246
|
-
|
247
|
-
qs = self._closure_model.objects.filter(**options)
|
248
|
-
return [ch.child.pk for ch in qs] if qs else []
|
249
|
-
|
250
|
-
@cached_tree_method
|
251
|
-
def get_descendants_queryset(self, include_self=False, depth=None):
|
252
|
-
"""Get the descendants queryset"""
|
253
|
-
|
254
|
-
pks = self.get_descendants_pks(include_self, depth)
|
255
|
-
return self._meta.model.objects.filter(pk__in=pks)
|
256
|
-
|
257
|
-
def get_descendants_tree(self):
|
258
|
-
"""Get a n-dimensional dict representing the model tree"""
|
259
|
-
|
260
|
-
return self._meta.model.get_tree(instance=self)
|
261
|
-
|
262
|
-
def get_descendants_tree_display(self, include_self=False, depth=None):
|
263
|
-
"""Get a multiline string representing the model tree"""
|
264
|
-
|
265
|
-
objs = self.get_descendants()
|
266
|
-
return '\n'.join(['%s' % (obj,) for obj in objs])
|
267
|
-
|
268
|
-
def get_first_child(self):
|
269
|
-
"""Get the first child node"""
|
270
|
-
return self.get_children_queryset().first()
|
271
|
-
|
272
|
-
def get_index(self):
|
273
|
-
"""Get the node index (self, index in node.parent.children list)"""
|
274
|
-
source = self.get_siblings()
|
275
|
-
return source.index(self)
|
276
|
-
|
277
|
-
def get_last_child(self):
|
278
|
-
"""Get the last child node"""
|
279
|
-
return self.get_children_queryset().last()
|
280
|
-
|
281
|
-
def get_level(self):
|
282
|
-
"""Get the node level (self, starting from 1)"""
|
283
|
-
|
284
|
-
levels = self._closure_model.objects.filter(child=self).values_list(
|
285
|
-
'depth',
|
286
|
-
flat=True
|
287
|
-
)
|
288
|
-
return max(levels) + 1
|
289
|
-
|
290
|
-
def get_path(self, prefix='', suffix='', delimiter='.', format_str=''):
|
291
|
-
"""Return Materialized Path of node"""
|
292
|
-
|
293
|
-
str_ = '{%s}' % format_str
|
294
|
-
return prefix + delimiter.join(
|
295
|
-
str_.format(i) for i in self.get_breadcrumbs(attr='tn_priority')
|
296
|
-
) + suffix
|
297
|
-
|
298
|
-
def get_parent(self):
|
299
|
-
"""Get the parent node"""
|
300
|
-
return self.tn_parent
|
301
|
-
|
302
|
-
def get_parent_pk(self):
|
303
|
-
"""Get the parent node pk"""
|
304
|
-
return self.tn_parent.pk if self.tn_parent else None
|
305
|
-
|
306
|
-
def set_parent(self, parent_obj):
|
307
|
-
"""Set the parent node"""
|
308
|
-
self.tn_parent = parent_obj
|
309
|
-
|
310
|
-
def get_priority(self):
|
311
|
-
return self.tn_priority
|
312
|
-
|
313
|
-
def set_priority(self, priority=0):
|
314
|
-
"""Set the node priority"""
|
315
|
-
self.tn_priority = priority
|
316
|
-
|
317
|
-
def get_root(self):
|
318
|
-
"""Get the root node for the current node"""
|
319
|
-
qs = self._closure_model.objects.filter(child=self).order_by('depth')
|
320
|
-
return qs.last().parent if qs.count() > 0 else None
|
321
|
-
|
322
|
-
def get_root_pk(self):
|
323
|
-
"""Get the root node pk for the current node"""
|
324
|
-
self.get_root().pk
|
325
|
-
|
326
|
-
def get_siblings(self):
|
327
|
-
"""Get a list with all the siblings"""
|
328
|
-
return list(self.get_siblings_queryset())
|
329
|
-
|
330
|
-
def get_siblings_count(self):
|
331
|
-
"""Get the siblings count"""
|
332
|
-
return self.get_siblings_queryset().count()
|
333
|
-
|
334
|
-
def get_siblings_pks(self):
|
335
|
-
"""Get the siblings pks list"""
|
336
|
-
return [item.pk for item in self.get_siblings_queryset()]
|
337
|
-
|
338
|
-
@cached_tree_method
|
339
|
-
def get_siblings_queryset(self):
|
340
|
-
"""Get the siblings queryset"""
|
341
|
-
if self.tn_parent:
|
342
|
-
queryset = self.tn_parent.tn_children.all()
|
343
|
-
else:
|
344
|
-
queryset = self._meta.model.objects.filter(tn_parent__isnull=True)
|
345
|
-
return queryset.exclude(pk=self.pk)
|
346
|
-
|
347
|
-
def is_ancestor_of(self, target_obj):
|
348
|
-
"""Return True if the current node is ancestor of target_obj"""
|
349
|
-
return self.pk in target_obj.get_ancestors_pks()
|
350
|
-
|
351
|
-
def is_child_of(self, target_obj):
|
352
|
-
"""Return True if the current node is child of target_obj"""
|
353
|
-
|
354
|
-
return self.pk in target_obj.get_children_pks()
|
355
|
-
|
356
|
-
def is_descendant_of(self, target_obj):
|
357
|
-
"""Return True if the current node is descendant of target_obj"""
|
358
|
-
return self.pk in target_obj.get_descendants_pks()
|
359
|
-
|
360
|
-
def is_first_child(self):
|
361
|
-
"""Return True if the current node is the first child"""
|
362
|
-
return self.tn_priority == 0
|
363
|
-
|
364
|
-
def is_last_child(self):
|
365
|
-
"""Return True if the current node is the last child"""
|
366
|
-
|
367
|
-
return self.tn_priority == self.get_siblings_count() - 1
|
368
|
-
|
369
|
-
def is_leaf(self):
|
370
|
-
"""Return True if the current node is leaf (self, it has not children)"""
|
371
|
-
return self.tn_children.count() == 0
|
372
|
-
|
373
|
-
def is_parent_of(self, target_obj):
|
374
|
-
"""Return True if the current node is parent of target_obj"""
|
375
|
-
return self == target_obj.tn_parent
|
376
|
-
|
377
|
-
def is_root(self):
|
378
|
-
"""Return True if the current node is root"""
|
379
|
-
return self.tn_parent is None
|
380
|
-
|
381
|
-
def is_root_of(self, target_obj):
|
382
|
-
"""Return True if the current node is root of target_obj"""
|
383
|
-
return self == target_obj.get_root()
|
384
|
-
|
385
|
-
def is_sibling_of(self, target_obj):
|
386
|
-
"""Return True if the current node is sibling of target_obj"""
|
387
|
-
return self in target_obj.tn_parent.tn_children.all()
|
388
|
-
|
389
|
-
# I think this method is not needed.
|
390
|
-
# Clearing entries in the Closure Table will happen automatically
|
391
|
-
# via cascading deletion
|
392
|
-
|
393
|
-
# def delete(self, cascade=True, *args, **kwargs):
|
394
|
-
# """Delete a node if cascade=True (default behaviour), children and
|
395
|
-
# descendants will be deleted too, otherwise children's parent will be
|
396
|
-
# set to None (then children become roots)"""
|
397
|
-
|
398
|
-
# pass
|
399
|
-
|
400
|
-
# Public properties
|
401
|
-
# All properties map a get_{{property}}() method.
|
402
|
-
|
403
|
-
@property
|
404
|
-
def ancestors(self):
|
405
|
-
return self.get_ancestors()
|
406
|
-
|
407
|
-
@property
|
408
|
-
def ancestors_count(self):
|
409
|
-
return self.get_ancestors_count()
|
410
|
-
|
411
|
-
@property
|
412
|
-
def ancestors_pks(self):
|
413
|
-
return self.get_ancestors_pks()
|
414
|
-
|
415
|
-
@property
|
416
|
-
def breadcrumbs(self):
|
417
|
-
return self.get_breadcrumbs()
|
418
|
-
|
419
|
-
@property
|
420
|
-
def children(self):
|
421
|
-
return self.get_children()
|
422
|
-
|
423
|
-
@property
|
424
|
-
def children_count(self):
|
425
|
-
return self.get_children_count()
|
426
|
-
|
427
|
-
@property
|
428
|
-
def children_pks(self):
|
429
|
-
return self.get_children_pks()
|
430
|
-
|
431
|
-
@property
|
432
|
-
def depth(self):
|
433
|
-
return self.get_depth()
|
434
|
-
|
435
|
-
@property
|
436
|
-
def descendants(self):
|
437
|
-
return self.get_descendants()
|
438
|
-
|
439
|
-
@property
|
440
|
-
def descendants_count(self):
|
441
|
-
return self.get_descendants_count()
|
442
|
-
|
443
|
-
@property
|
444
|
-
def descendants_pks(self):
|
445
|
-
return self.get_descendants_pks()
|
446
|
-
|
447
|
-
@property
|
448
|
-
def descendants_tree(self):
|
449
|
-
return self.get_descendants_tree()
|
450
|
-
|
451
|
-
@property
|
452
|
-
def descendants_tree_display(self):
|
453
|
-
return self.get_descendants_tree_display()
|
454
|
-
|
455
|
-
@property
|
456
|
-
def first_child(self):
|
457
|
-
return self.get_first_child()
|
458
|
-
|
459
|
-
@property
|
460
|
-
def index(self):
|
461
|
-
return self.get_index()
|
462
|
-
|
463
|
-
@property
|
464
|
-
def last_child(self):
|
465
|
-
return self.get_last_child()
|
466
|
-
|
467
|
-
@property
|
468
|
-
def level(self):
|
469
|
-
return self.get_level()
|
470
|
-
|
471
|
-
@property
|
472
|
-
def parent(self):
|
473
|
-
return self.tn_parent
|
474
|
-
|
475
|
-
@property
|
476
|
-
def parent_pk(self):
|
477
|
-
return self.get_parent_pk()
|
478
|
-
|
479
|
-
@property
|
480
|
-
def priority(self):
|
481
|
-
return self.get_priority()
|
482
|
-
|
483
|
-
@classproperty
|
484
|
-
def roots(cls):
|
485
|
-
return cls.get_roots()
|
486
|
-
|
487
|
-
@property
|
488
|
-
def root(self):
|
489
|
-
return self.get_root()
|
490
|
-
|
491
|
-
@property
|
492
|
-
def root_pk(self):
|
493
|
-
return self.get_root_pk()
|
494
|
-
|
495
|
-
@property
|
496
|
-
def siblings(self):
|
497
|
-
return self.get_siblings()
|
498
|
-
|
499
|
-
@property
|
500
|
-
def siblings_count(self):
|
501
|
-
return self.get_siblings_count()
|
502
|
-
|
503
|
-
@property
|
504
|
-
def siblings_pks(self):
|
505
|
-
return self.get_siblings_pks()
|
506
|
-
|
507
|
-
@classproperty
|
508
|
-
def tree(cls):
|
509
|
-
return cls.get_tree()
|
510
|
-
|
511
|
-
@classproperty
|
512
|
-
def tree_display(cls):
|
513
|
-
return cls.get_tree_display()
|
514
|
-
|
515
|
-
# ----------------------------------------------------------------------
|
516
|
-
# Private methods
|
517
|
-
# The usage of these methods is only allowed by developers. In future
|
518
|
-
# versions, these methods may be changed or removed without any warning.
|
519
|
-
|
520
|
-
@property
|
521
|
-
def _closure_model(self):
|
522
|
-
return self._meta.model.closure_model
|
523
|
-
|
524
|
-
@property
|
525
|
-
def tn_order(self):
|
526
|
-
path = self.get_breadcrumbs(attr='tn_priority')
|
527
|
-
return ''.join(['{:0>6g}'.format(i) for i in path])
|
528
|
-
|
529
|
-
@cached_tree_method
|
530
|
-
def object2dict(self, instance, exclude=[]):
|
531
|
-
"""Convert Class Object to python dict"""
|
532
|
-
|
533
|
-
result = dict()
|
534
|
-
|
535
|
-
if not hasattr(instance, '__dict__'):
|
536
|
-
return instance
|
537
|
-
|
538
|
-
new_subdic = dict(vars(instance))
|
539
|
-
for key, value in new_subdic.items():
|
540
|
-
if key.startswith('_') or key in exclude:
|
541
|
-
continue
|
542
|
-
result.update({key: self.object2dict(value, exclude)})
|
543
|
-
|
544
|
-
childs = instance.tn_children.all()
|
545
|
-
if childs.count() > 0:
|
546
|
-
result.update({
|
547
|
-
'children': [
|
548
|
-
obj.object2dict(obj, exclude)
|
549
|
-
for obj in childs.all()]
|
550
|
-
})
|
551
|
-
result.update({'path': instance.get_path(format_str=':d')})
|
552
|
-
return result
|
553
|
-
|
554
|
-
@cached_tree_method
|
555
|
-
def get_display(self, indent=True, mark='— '):
|
556
|
-
indentation = (mark * self.tn_ancestors_count) if indent else ''
|
557
|
-
indentation = force_str(indentation)
|
558
|
-
text = self.get_display_text()
|
559
|
-
text = force_str(text)
|
560
|
-
return indentation + text
|
561
|
-
|
562
|
-
@cached_tree_method
|
563
|
-
def get_display_text(self):
|
564
|
-
"""
|
565
|
-
Gets the text that will be indented in `get_display` method.
|
566
|
-
Returns the `treenode_display_field` value if specified,
|
567
|
-
otherwise falls back on the model's pk.
|
568
|
-
Override this method to return another field or a computed value. #27
|
569
|
-
"""
|
570
|
-
text = ''
|
571
|
-
if (hasattr(self, 'treenode_display_field') and
|
572
|
-
self.treenode_display_field is not None):
|
573
|
-
field_name = getattr(self, 'treenode_display_field')
|
574
|
-
text = getattr(self, field_name, '')
|
575
|
-
if not text and self.pk:
|
576
|
-
text = self.pk
|
577
|
-
return force_str(text)
|
578
|
-
|
579
|
-
@transaction.atomic
|
580
|
-
def _insert(self):
|
581
|
-
"""Adds a new entry to the Adjacency Table and the Closure Table"""
|
582
|
-
|
583
|
-
treenode_cache.clear()
|
584
|
-
|
585
|
-
instance = self._closure_model.objects.create(
|
586
|
-
parent=self,
|
587
|
-
child=self,
|
588
|
-
depth=0
|
589
|
-
)
|
590
|
-
instance.save()
|
591
|
-
|
592
|
-
qs = self._closure_model.objects.all()
|
593
|
-
parents = qs.filter(child=self.tn_parent).values('parent', 'depth')
|
594
|
-
children = qs.filter(parent=self).values('child', 'depth')
|
595
|
-
objects = [
|
596
|
-
self._closure_model(
|
597
|
-
parent_id=p['parent'],
|
598
|
-
child_id=c['child'],
|
599
|
-
depth=p['depth'] + c['depth'] + 1
|
600
|
-
)
|
601
|
-
for p in parents
|
602
|
-
for c in children
|
603
|
-
]
|
604
|
-
self._closure_model.objects.bulk_create(objects)
|
605
|
-
|
606
|
-
@transaction.atomic
|
607
|
-
def _move_to(self, old_parent):
|
608
|
-
treenode_cache.clear()
|
609
|
-
|
610
|
-
target = self.tn_parent
|
611
|
-
qs = self._closure_model.objects.all()
|
612
|
-
subtree = qs.filter(parent=self).values('child', 'depth')
|
613
|
-
supertree = qs.filter(child=target).values('parent', 'depth')
|
614
|
-
|
615
|
-
# Step 1. Delete
|
616
|
-
subtree_pks = [node.child.pk for node in qs.filter(parent=self)]
|
617
|
-
qs.filter(child_id__in=subtree_pks).exclude(
|
618
|
-
parent_id__in=subtree_pks).delete()
|
619
|
-
|
620
|
-
# Step 2. Insert
|
621
|
-
objects = [
|
622
|
-
self._closure_model(
|
623
|
-
parent_id=p['parent'],
|
624
|
-
child_id=c['child'],
|
625
|
-
depth=p['depth'] + c['depth'] + 1
|
626
|
-
)
|
627
|
-
for p in supertree
|
628
|
-
for c in subtree
|
629
|
-
]
|
630
|
-
self._closure_model.objects.bulk_create(objects)
|
631
|
-
|
632
|
-
def _order(self):
|
633
|
-
|
634
|
-
treenode_cache.clear()
|
635
|
-
|
636
|
-
queryset = self.get_siblings_queryset()
|
637
|
-
|
638
|
-
if self.tn_priority > queryset.count():
|
639
|
-
self.tn_priority = queryset.count()
|
640
|
-
|
641
|
-
siblings = list(node for node in queryset)
|
642
|
-
sorted_siblings = sorted(siblings, key=lambda x: x.tn_priority)
|
643
|
-
sorted_siblings.insert(self.tn_priority, self)
|
644
|
-
|
645
|
-
for index in range(len(sorted_siblings)):
|
646
|
-
sorted_siblings[index].tn_priority = index
|
647
|
-
|
648
|
-
self._meta.model.objects.bulk_update(
|
649
|
-
sorted_siblings, ('tn_priority', ))
|
650
|
-
|
651
|
-
def save(self, force_insert=False, *args, **kwargs):
|
652
|
-
treenode_cache.clear()
|
653
|
-
|
654
|
-
try:
|
655
|
-
old = self._meta.model.objects.get(pk=self.pk)
|
656
|
-
old_parent = old.tn_parent
|
657
|
-
except self._meta.model.DoesNotExist:
|
658
|
-
force_insert = True
|
659
|
-
|
660
|
-
super().save(*args, **kwargs)
|
661
|
-
self._order()
|
662
|
-
|
663
|
-
if force_insert:
|
664
|
-
self._insert()
|
665
|
-
elif old_parent != self.tn_parent:
|
666
|
-
self._move_to(old_parent)
|
667
|
-
|
668
|
-
# The end
|
@@ -1 +0,0 @@
|
|
1
|
-
|