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.
- {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.dist-info}/LICENSE +2 -2
- django_fast_treenode-2.1.1.dist-info/METADATA +158 -0
- django_fast_treenode-2.1.1.dist-info/RECORD +64 -0
- {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.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/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.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,210 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Node Mixin
|
4
|
+
|
5
|
+
Version: 2.1.0
|
6
|
+
Author: Timur Kady
|
7
|
+
Email: timurkady@yandex.com
|
8
|
+
"""
|
9
|
+
|
10
|
+
from django.db import models
|
11
|
+
from django.core.exceptions import FieldDoesNotExist
|
12
|
+
|
13
|
+
from ...cache import cached_method, treenode_cache
|
14
|
+
from ...utils.base36 import to_base36
|
15
|
+
|
16
|
+
|
17
|
+
class TreeNodeNodeMixin(models.Model):
|
18
|
+
"""TreeNode Node Mixin."""
|
19
|
+
|
20
|
+
class Meta:
|
21
|
+
"""Moxin Meta Class."""
|
22
|
+
|
23
|
+
abstract = True
|
24
|
+
|
25
|
+
@cached_method
|
26
|
+
def get_breadcrumbs(self, attr='pk'):
|
27
|
+
"""Optimized breadcrumbs retrieval with direct cache check."""
|
28
|
+
try:
|
29
|
+
self._meta.get_field(attr)
|
30
|
+
except FieldDoesNotExist:
|
31
|
+
raise ValueError(f"Invalid attribute name: {attr}")
|
32
|
+
|
33
|
+
# Easy logics for roots
|
34
|
+
if self.tn_parent is None:
|
35
|
+
return [getattr(self, attr)]
|
36
|
+
|
37
|
+
# Generate parents cache key
|
38
|
+
cache_key = treenode_cache.generate_cache_key(
|
39
|
+
self._meta.label,
|
40
|
+
self.get_breadcrumbs.__name__,
|
41
|
+
self.tn_parent.pk,
|
42
|
+
attr
|
43
|
+
)
|
44
|
+
|
45
|
+
# Try get value from cache
|
46
|
+
breadcrumbs = treenode_cache.get(cache_key)
|
47
|
+
if breadcrumbs is not None:
|
48
|
+
return breadcrumbs + [getattr(self, attr)]
|
49
|
+
|
50
|
+
queryset = self.get_ancestors_queryset(include_self=True).only(attr)
|
51
|
+
return [getattr(item, attr) for item in queryset]
|
52
|
+
|
53
|
+
@cached_method
|
54
|
+
def get_depth(self):
|
55
|
+
"""Get the node depth (self, how many levels of descendants)."""
|
56
|
+
return self.closure_model.get_depth(self)
|
57
|
+
|
58
|
+
@cached_method
|
59
|
+
def get_index(self):
|
60
|
+
"""Get the node index (self, index in node.parent.children list)."""
|
61
|
+
if self.tn_parent is None:
|
62
|
+
return self.tn_priority
|
63
|
+
source = list(self.tn_parent.tn_children.all())
|
64
|
+
return source.index(self) if self in source else self.tn_priority
|
65
|
+
|
66
|
+
@cached_method
|
67
|
+
def get_level(self):
|
68
|
+
"""Get the node level (self, starting from 1)."""
|
69
|
+
return self.closure_model.get_level(self)
|
70
|
+
|
71
|
+
def get_order(self):
|
72
|
+
"""Return the materialized path."""
|
73
|
+
path = self.get_breadcrumbs(attr='tn_priority')
|
74
|
+
segments = [to_base36(i).rjust(6, '0') for i in path]
|
75
|
+
return ''.join(segments)
|
76
|
+
|
77
|
+
def insert_at(self, target, position='first-child', save=False):
|
78
|
+
"""
|
79
|
+
Insert a node into the tree relative to the target node.
|
80
|
+
|
81
|
+
Parameters:
|
82
|
+
target: еhe target node relative to which this node will be placed.
|
83
|
+
|
84
|
+
position – the position, relative to the target node, where the
|
85
|
+
current node object will be moved to, can be one of:
|
86
|
+
|
87
|
+
- first-root: the node will be the first root node;
|
88
|
+
- last-root: the node will be the last root node;
|
89
|
+
- sorted-root: the new node will be moved after sorting by
|
90
|
+
the treenode_sort_field field;
|
91
|
+
|
92
|
+
- first-sibling: the node will be the new leftmost sibling of the
|
93
|
+
target node;
|
94
|
+
- left-sibling: the node will take the target node’s place, which will
|
95
|
+
be moved to the target position with shifting follows nodes;
|
96
|
+
- right-sibling: the node will be moved to the position after the
|
97
|
+
target node;
|
98
|
+
- last-sibling: the node will be the new rightmost sibling of the
|
99
|
+
target node;
|
100
|
+
- sorted-sibling: the new node will be moved after sorting by
|
101
|
+
the treenode_sort_field field;
|
102
|
+
|
103
|
+
- first-child: the node will be the first child of the target node;
|
104
|
+
- last-child: the node will be the new rightmost child of the target
|
105
|
+
- sorted-child: the new node will be moved after sorting by
|
106
|
+
the treenode_sort_field field.
|
107
|
+
|
108
|
+
save : if `save=true`, the node will be saved in the tree. Otherwise,
|
109
|
+
the method will return a model instance with updated fields: parent
|
110
|
+
field and position in sibling list.
|
111
|
+
|
112
|
+
Before using this method, the model instance must be correctly created
|
113
|
+
with all required fields defined. If the model has required fields,
|
114
|
+
then simply creating an object and calling insert_at() will not work,
|
115
|
+
because Django will raise an exception.
|
116
|
+
"""
|
117
|
+
# This method seems to have very dubious practical value.
|
118
|
+
parent, priority = self._meta.model._get_place(target, position)
|
119
|
+
self.tn_parent = parent
|
120
|
+
self.tn_priority = priority
|
121
|
+
|
122
|
+
if save:
|
123
|
+
self.save()
|
124
|
+
|
125
|
+
def move_to(self, target, position=0):
|
126
|
+
"""
|
127
|
+
Move node relative to target node and position.
|
128
|
+
|
129
|
+
Moves the model instance relative to the target node and sets its
|
130
|
+
position (if necessary).
|
131
|
+
|
132
|
+
position – the position, relative to the target node, where the
|
133
|
+
current node object will be moved to, can be one of:
|
134
|
+
|
135
|
+
- first-root: the node will be the first root node;
|
136
|
+
- last-root: the node will be the last root node;
|
137
|
+
- sorted-root: the new node will be moved after sorting by
|
138
|
+
the treenode_sort_field field;
|
139
|
+
Note: if `position` contains `root`, then `target` parametr is ignored
|
140
|
+
|
141
|
+
- first-sibling: the node will be the new leftmost sibling of the
|
142
|
+
target node;
|
143
|
+
- left-sibling: the node will take the target node’s place, which will
|
144
|
+
be moved to the target position with shifting follows nodes;
|
145
|
+
- right-sibling: the node will be moved to the position after the
|
146
|
+
target node;
|
147
|
+
- last-sibling: the node will be the new rightmost sibling of the
|
148
|
+
target node;
|
149
|
+
- sorted-sibling: the new node will be moved after sorting by
|
150
|
+
the treenode_sort_field field;
|
151
|
+
|
152
|
+
- first-child: the node will be the first child of the target node;
|
153
|
+
- last-child: the node will be the new rightmost child of the target
|
154
|
+
- sorted-child: the new node will be moved after sorting by
|
155
|
+
the treenode_sort_field field;
|
156
|
+
"""
|
157
|
+
parent, priority = self._meta.model._get_place(target, position)
|
158
|
+
self.tn_parent = parent
|
159
|
+
self.tn_priority = priority
|
160
|
+
self.save()
|
161
|
+
|
162
|
+
def get_path(self, prefix='', suffix='', delimiter='.', format_str=''):
|
163
|
+
"""Return Materialized Path of node."""
|
164
|
+
priorities = self.get_breadcrumbs(attr='tn_priority')
|
165
|
+
if not priorities or all(p is None for p in priorities):
|
166
|
+
return prefix + suffix
|
167
|
+
|
168
|
+
str_ = "{%s}" % format_str
|
169
|
+
path = delimiter.join([
|
170
|
+
str_.format(p)
|
171
|
+
for p in priorities
|
172
|
+
if p is not None
|
173
|
+
])
|
174
|
+
return prefix + path + suffix
|
175
|
+
|
176
|
+
@cached_method
|
177
|
+
def get_parent(self):
|
178
|
+
"""Get the parent node."""
|
179
|
+
return self.tn_parent
|
180
|
+
|
181
|
+
def set_parent(self, parent_obj):
|
182
|
+
"""Set the parent node."""
|
183
|
+
self.tn_parent = parent_obj
|
184
|
+
self.save()
|
185
|
+
|
186
|
+
def get_parent_pk(self):
|
187
|
+
"""Get the parent node pk."""
|
188
|
+
return self.get_parent().pk if self.tn_parent else None
|
189
|
+
|
190
|
+
@cached_method
|
191
|
+
def get_priority(self):
|
192
|
+
"""Get the node priority."""
|
193
|
+
return self.tn_priority
|
194
|
+
|
195
|
+
def set_priority(self, priority=0):
|
196
|
+
"""Set the node priority."""
|
197
|
+
self.tn_priority = priority
|
198
|
+
self.save()
|
199
|
+
|
200
|
+
@cached_method
|
201
|
+
def get_root(self):
|
202
|
+
"""Get the root node for the current node."""
|
203
|
+
return self.closure_model.get_root(self)
|
204
|
+
|
205
|
+
def get_root_pk(self):
|
206
|
+
"""Get the root node pk for the current node."""
|
207
|
+
root = self.get_root()
|
208
|
+
return root.pk if root else None
|
209
|
+
|
210
|
+
# The End
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Properties Mixin
|
4
|
+
|
5
|
+
Version: 2.1.0
|
6
|
+
Author: Timur Kady
|
7
|
+
Email: timurkady@yandex.com
|
8
|
+
"""
|
9
|
+
|
10
|
+
from ..classproperty import classproperty
|
11
|
+
|
12
|
+
|
13
|
+
class TreeNodePropertiesMixin:
|
14
|
+
"""
|
15
|
+
TreeNode Properties Mixin.
|
16
|
+
|
17
|
+
Public properties.
|
18
|
+
All properties map a get_{{property}}() method.
|
19
|
+
"""
|
20
|
+
|
21
|
+
@property
|
22
|
+
def ancestors(self):
|
23
|
+
"""Get a list with all ancestors; self included."""
|
24
|
+
return self.get_ancestors()
|
25
|
+
|
26
|
+
@property
|
27
|
+
def ancestors_count(self):
|
28
|
+
"""Get the ancestors count."""
|
29
|
+
return self.get_ancestors_count()
|
30
|
+
|
31
|
+
@property
|
32
|
+
def ancestors_pks(self):
|
33
|
+
"""Get the ancestors pks list; self included."""
|
34
|
+
return self.get_ancestors_pks()
|
35
|
+
|
36
|
+
@property
|
37
|
+
def breadcrumbs(self):
|
38
|
+
"""Get the breadcrumbs to current node(self, included)."""
|
39
|
+
return self.get_breadcrumbs()
|
40
|
+
|
41
|
+
@property
|
42
|
+
def children(self):
|
43
|
+
"""Get a list containing all children; self included."""
|
44
|
+
return self.get_children()
|
45
|
+
|
46
|
+
@property
|
47
|
+
def children_count(self):
|
48
|
+
"""Get the children count."""
|
49
|
+
return self.get_children_count()
|
50
|
+
|
51
|
+
@property
|
52
|
+
def children_pks(self):
|
53
|
+
"""Get the children pks list."""
|
54
|
+
return self.get_children_pks()
|
55
|
+
|
56
|
+
@property
|
57
|
+
def depth(self):
|
58
|
+
"""Get the node depth."""
|
59
|
+
return self.get_depth()
|
60
|
+
|
61
|
+
@property
|
62
|
+
def descendants(self):
|
63
|
+
"""Get a list containing all descendants; self not included."""
|
64
|
+
return self.get_descendants()
|
65
|
+
|
66
|
+
@property
|
67
|
+
def descendants_count(self):
|
68
|
+
"""Get the descendants count; self not included."""
|
69
|
+
return self.get_descendants_count()
|
70
|
+
|
71
|
+
@property
|
72
|
+
def descendants_pks(self):
|
73
|
+
"""Get the descendants pks list; self not included."""
|
74
|
+
return self.get_descendants_pks()
|
75
|
+
|
76
|
+
@property
|
77
|
+
def first_child(self):
|
78
|
+
"""Get the first child node."""
|
79
|
+
return self.get_first_child()
|
80
|
+
|
81
|
+
@property
|
82
|
+
def index(self):
|
83
|
+
"""Get the node index."""
|
84
|
+
return self.get_index()
|
85
|
+
|
86
|
+
@property
|
87
|
+
def last_child(self):
|
88
|
+
"""Get the last child node."""
|
89
|
+
return self.get_last_child()
|
90
|
+
|
91
|
+
@property
|
92
|
+
def level(self):
|
93
|
+
"""Get the node level."""
|
94
|
+
return self.get_level()
|
95
|
+
|
96
|
+
@property
|
97
|
+
def parent(self):
|
98
|
+
"""Get node parent."""
|
99
|
+
return self.tn_parent
|
100
|
+
|
101
|
+
@property
|
102
|
+
def parent_pk(self):
|
103
|
+
"""Get node parent pk."""
|
104
|
+
return self.get_parent_pk()
|
105
|
+
|
106
|
+
@property
|
107
|
+
def priority(self):
|
108
|
+
"""Get node priority."""
|
109
|
+
return self.get_priority()
|
110
|
+
|
111
|
+
@classproperty
|
112
|
+
def roots(cls):
|
113
|
+
"""Get a list with all root nodes."""
|
114
|
+
return cls.get_roots()
|
115
|
+
|
116
|
+
@property
|
117
|
+
def root(self):
|
118
|
+
"""Get the root node for the current node."""
|
119
|
+
return self.get_root()
|
120
|
+
|
121
|
+
@property
|
122
|
+
def root_pk(self):
|
123
|
+
"""Get the root node pk for the current node."""
|
124
|
+
return self.get_root_pk()
|
125
|
+
|
126
|
+
@property
|
127
|
+
def siblings(self):
|
128
|
+
"""Get a list with all the siblings."""
|
129
|
+
return self.get_siblings()
|
130
|
+
|
131
|
+
@property
|
132
|
+
def siblings_count(self):
|
133
|
+
"""Get the siblings count."""
|
134
|
+
return self.get_siblings_count()
|
135
|
+
|
136
|
+
@property
|
137
|
+
def siblings_pks(self):
|
138
|
+
"""Get the siblings pks list."""
|
139
|
+
return self.get_siblings_pks()
|
140
|
+
|
141
|
+
@classproperty
|
142
|
+
def tree(cls):
|
143
|
+
"""Get an n-dimensional dict representing the model tree."""
|
144
|
+
return cls.get_tree()
|
145
|
+
|
146
|
+
@classproperty
|
147
|
+
def tree_display(cls):
|
148
|
+
"""Get a multiline string representing the model tree."""
|
149
|
+
return cls.get_tree_display()
|
150
|
+
|
151
|
+
@property
|
152
|
+
def tn_order(self):
|
153
|
+
"""Return the materialized path."""
|
154
|
+
return self.get_order()
|
155
|
+
|
156
|
+
# The End
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Roots Mixin
|
4
|
+
|
5
|
+
Version: 2.1.0
|
6
|
+
Author: Timur Kady
|
7
|
+
Email: timurkady@yandex.com
|
8
|
+
"""
|
9
|
+
|
10
|
+
from django.db import models
|
11
|
+
from treenode.cache import cached_method
|
12
|
+
|
13
|
+
|
14
|
+
class TreeNodeRootsMixin(models.Model):
|
15
|
+
"""TreeNode Roots Mixin."""
|
16
|
+
|
17
|
+
class Meta:
|
18
|
+
"""Moxin Meta Class."""
|
19
|
+
|
20
|
+
abstract = True
|
21
|
+
|
22
|
+
@classmethod
|
23
|
+
def add_root(cls, position=None, **kwargs):
|
24
|
+
"""
|
25
|
+
Add a root node to the tree.
|
26
|
+
|
27
|
+
Adds a new root node at the specified position. If no position is
|
28
|
+
specified, the new node will be the last element in the root.
|
29
|
+
Parameters:
|
30
|
+
position: can be 'first-root', 'last-root', 'sorted-root' or integer
|
31
|
+
value.
|
32
|
+
**kwargs – Object creation data that will be passed to the inherited
|
33
|
+
Node model
|
34
|
+
instance – Instead of passing object creation data, you can pass
|
35
|
+
an already-constructed (but not yet saved) model instance to be
|
36
|
+
inserted into the tree.
|
37
|
+
|
38
|
+
Returns:
|
39
|
+
The created node object. It will be save()d by this method.
|
40
|
+
"""
|
41
|
+
if isinstance(position, int):
|
42
|
+
priority = position
|
43
|
+
else:
|
44
|
+
if position not in ['first-root', 'last-root', 'sorted-root']:
|
45
|
+
raise ValueError(f"Invalid position format: {position}")
|
46
|
+
|
47
|
+
parent, priority = cls._get_place(None, position)
|
48
|
+
|
49
|
+
instance = kwargs.get("instance")
|
50
|
+
if instance is None:
|
51
|
+
instance = cls(**kwargs)
|
52
|
+
|
53
|
+
parent, priority = cls._get_place(None, position)
|
54
|
+
instance.tn_parent = None
|
55
|
+
instance.tn_priority = priority
|
56
|
+
instance.save()
|
57
|
+
return instance
|
58
|
+
|
59
|
+
@classmethod
|
60
|
+
@cached_method
|
61
|
+
def get_roots_queryset(cls):
|
62
|
+
"""Get root nodes queryset with preloaded children."""
|
63
|
+
qs = cls.objects.filter(tn_parent=None).prefetch_related('tn_children')
|
64
|
+
return qs
|
65
|
+
|
66
|
+
@classmethod
|
67
|
+
@cached_method
|
68
|
+
def get_roots_pks(cls):
|
69
|
+
"""Get a list with all root nodes."""
|
70
|
+
pks = cls.objects.filter(tn_parent=None).values_list("id", flat=True)
|
71
|
+
return list(pks)
|
72
|
+
|
73
|
+
@classmethod
|
74
|
+
def get_roots(cls):
|
75
|
+
"""Get a list with all root nodes."""
|
76
|
+
qs = cls.get_roots_queryset()
|
77
|
+
return list(qs)
|
78
|
+
|
79
|
+
@classmethod
|
80
|
+
def get_roots_count(cls):
|
81
|
+
"""Get a list with all root nodes."""
|
82
|
+
return len(cls.get_roots_pks())
|
83
|
+
|
84
|
+
@classmethod
|
85
|
+
def get_first_root(cls):
|
86
|
+
"""Return the first root node in the tree or None if it is empty."""
|
87
|
+
roots = cls.get_roots_queryset()
|
88
|
+
return roots.fiest() if roots.count() > 0 else None
|
89
|
+
|
90
|
+
@classmethod
|
91
|
+
def get_last_root(cls):
|
92
|
+
"""Return the last root node in the tree or None if it is empty."""
|
93
|
+
roots = cls.get_roots_queryset()
|
94
|
+
return roots.last() if roots.count() > 0 else None
|
95
|
+
|
96
|
+
# The End
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Siblings Mixin
|
4
|
+
|
5
|
+
Version: 2.1.0
|
6
|
+
Author: Timur Kady
|
7
|
+
Email: timurkady@yandex.com
|
8
|
+
"""
|
9
|
+
|
10
|
+
from django.db import models
|
11
|
+
from treenode.cache import cached_method
|
12
|
+
|
13
|
+
|
14
|
+
class TreeNodeSiblingsMixin(models.Model):
|
15
|
+
"""TreeNode Siblings Mixin."""
|
16
|
+
|
17
|
+
class Meta:
|
18
|
+
"""Moxin Meta Class."""
|
19
|
+
|
20
|
+
abstract = True
|
21
|
+
|
22
|
+
def add_sibling(self, position=None, **kwargs):
|
23
|
+
"""
|
24
|
+
Add a new node as a sibling to the current node object.
|
25
|
+
|
26
|
+
Returns the created node object or None if failed. It will be saved
|
27
|
+
by this method.
|
28
|
+
"""
|
29
|
+
if isinstance(position, int):
|
30
|
+
priority = position
|
31
|
+
parent = self.tn_parent
|
32
|
+
else:
|
33
|
+
if position not in [
|
34
|
+
'first-sibling', 'left-sibling', 'right-sibling',
|
35
|
+
'last-sibling', 'sorted-sibling']:
|
36
|
+
raise ValueError(f"Invalid position format: {position}")
|
37
|
+
parent, priority = self._meta.model._get_place(self, position)
|
38
|
+
|
39
|
+
instance = kwargs.get("instance")
|
40
|
+
if instance is None:
|
41
|
+
instance = self._meta.model(**kwargs)
|
42
|
+
instance.tn_priority = priority
|
43
|
+
instance.tn_priority = parent
|
44
|
+
instance.save()
|
45
|
+
return instance
|
46
|
+
|
47
|
+
@cached_method
|
48
|
+
def get_siblings_queryset(self):
|
49
|
+
"""Get the siblings queryset with prefetch."""
|
50
|
+
if self.tn_parent:
|
51
|
+
qs = self.tn_parent.tn_children.prefetch_related('tn_children')
|
52
|
+
else:
|
53
|
+
qs = self._meta.model.objects.filter(tn_parent__isnull=True)
|
54
|
+
return qs.exclude(pk=self.pk)
|
55
|
+
|
56
|
+
def get_siblings(self):
|
57
|
+
"""Get a list with all the siblings."""
|
58
|
+
return list(self.get_siblings_queryset())
|
59
|
+
|
60
|
+
def get_siblings_count(self):
|
61
|
+
"""Get the siblings count."""
|
62
|
+
return self.get_siblings_queryset().count()
|
63
|
+
|
64
|
+
def get_siblings_pks(self):
|
65
|
+
"""Get the siblings pks list."""
|
66
|
+
return [item.pk for item in self.get_siblings_queryset()]
|
67
|
+
|
68
|
+
def get_first_sibling(self):
|
69
|
+
"""
|
70
|
+
Return the fist node’s sibling.
|
71
|
+
|
72
|
+
Method can return the node itself if it was the leftmost sibling.
|
73
|
+
"""
|
74
|
+
return self.get_siblings_queryset().fist()
|
75
|
+
|
76
|
+
def get_previous_sibling(self):
|
77
|
+
"""Return the previous sibling in the tree, or None."""
|
78
|
+
priority = self.tn_priority - 1
|
79
|
+
if priority < 0:
|
80
|
+
return None
|
81
|
+
return self.get_siblings_queryset.filter(tn_priority=priority)
|
82
|
+
|
83
|
+
def get_next_sibling(self):
|
84
|
+
"""Return the next sibling in the tree, or None."""
|
85
|
+
priority = self.tn_priority = 1
|
86
|
+
queryset = self.get_siblings_queryset()
|
87
|
+
if priority == queryset.count():
|
88
|
+
return None
|
89
|
+
return queryset.filter(tn_priority=priority)
|
90
|
+
|
91
|
+
def get_last_sibling(self):
|
92
|
+
"""
|
93
|
+
Return the fist node’s sibling.
|
94
|
+
|
95
|
+
Method can return the node itself if it was the leftmost sibling.
|
96
|
+
"""
|
97
|
+
return self.get_siblings_queryset().last()
|
98
|
+
|
99
|
+
# The End
|