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.
Files changed (70) hide show
  1. {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/LICENSE +2 -2
  2. django_fast_treenode-2.1.0.dist-info/METADATA +161 -0
  3. django_fast_treenode-2.1.0.dist-info/RECORD +75 -0
  4. {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.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/docs/.gitignore +0 -0
  12. treenode/docs/about.md +36 -0
  13. treenode/docs/admin.md +104 -0
  14. treenode/docs/api.md +739 -0
  15. treenode/docs/cache.md +187 -0
  16. treenode/docs/import_export.md +35 -0
  17. treenode/docs/index.md +30 -0
  18. treenode/docs/installation.md +74 -0
  19. treenode/docs/migration.md +145 -0
  20. treenode/docs/models.md +128 -0
  21. treenode/docs/roadmap.md +45 -0
  22. treenode/forms.py +33 -22
  23. treenode/managers/__init__.py +21 -0
  24. treenode/managers/adjacency.py +203 -0
  25. treenode/managers/closure.py +278 -0
  26. treenode/models/__init__.py +2 -1
  27. treenode/models/adjacency.py +343 -0
  28. treenode/models/classproperty.py +3 -0
  29. treenode/models/closure.py +39 -65
  30. treenode/models/factory.py +12 -2
  31. treenode/models/mixins/__init__.py +23 -0
  32. treenode/models/mixins/ancestors.py +65 -0
  33. treenode/models/mixins/children.py +81 -0
  34. treenode/models/mixins/descendants.py +66 -0
  35. treenode/models/mixins/family.py +63 -0
  36. treenode/models/mixins/logical.py +68 -0
  37. treenode/models/mixins/node.py +210 -0
  38. treenode/models/mixins/properties.py +156 -0
  39. treenode/models/mixins/roots.py +96 -0
  40. treenode/models/mixins/siblings.py +99 -0
  41. treenode/models/mixins/tree.py +344 -0
  42. treenode/signals.py +26 -0
  43. treenode/static/treenode/css/tree_widget.css +201 -31
  44. treenode/static/treenode/css/treenode_admin.css +48 -41
  45. treenode/static/treenode/js/tree_widget.js +269 -131
  46. treenode/static/treenode/js/treenode_admin.js +131 -171
  47. treenode/templates/admin/tree_node_changelist.html +6 -0
  48. treenode/templates/admin/tree_node_import.html +27 -9
  49. treenode/templates/admin/tree_node_import_report.html +32 -0
  50. treenode/templates/admin/treenode_ajax_rows.html +7 -0
  51. treenode/tests/tests.py +488 -0
  52. treenode/urls.py +10 -6
  53. treenode/utils/__init__.py +2 -0
  54. treenode/utils/aid.py +46 -0
  55. treenode/utils/base16.py +38 -0
  56. treenode/utils/base36.py +3 -1
  57. treenode/utils/db.py +116 -0
  58. treenode/utils/exporter.py +63 -36
  59. treenode/utils/importer.py +168 -161
  60. treenode/utils/radix.py +61 -0
  61. treenode/version.py +2 -2
  62. treenode/views.py +119 -38
  63. treenode/widgets.py +104 -40
  64. django_fast_treenode-2.0.10.dist-info/METADATA +0 -698
  65. django_fast_treenode-2.0.10.dist-info/RECORD +0 -41
  66. treenode/admin.py +0 -396
  67. treenode/docs/Documentation +0 -664
  68. treenode/managers.py +0 -281
  69. treenode/models/proxy.py +0 -650
  70. {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,81 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ TreeNode Children 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 TreeNodeChildrenMixin(models.Model):
15
+ """TreeNode Ancestors Mixin."""
16
+
17
+ class Meta:
18
+ """Moxin Meta Class."""
19
+
20
+ abstract = True
21
+
22
+ def add_child(self, position=None, **kwargs):
23
+ """
24
+ Add a child to the node.
25
+
26
+ position:
27
+ Can be 'first-child', 'last-child', 'sorted-child' or integer value.
28
+
29
+ Parameters:
30
+ **kwargs – Object creation data that will be passed to the inherited
31
+ Node model
32
+ instance – Instead of passing object creation data, you can pass
33
+ an already-constructed (but not yet saved) model instance to be
34
+ inserted into the tree.
35
+
36
+ Returns:
37
+ The created node object. It will be save()d by this method.
38
+ """
39
+ if isinstance(position, int):
40
+ priority = position
41
+ parent = self
42
+ else:
43
+ if position not in ['first-child', 'last-child', 'sorted-child']:
44
+ raise ValueError(f"Invalid position format: {position}")
45
+ parent, priority = self._meta.model._get_place(self, position)
46
+
47
+ instance = kwargs.get("instance")
48
+ if instance is None:
49
+ instance = self._meta.model(**kwargs)
50
+ instance.tn_parent = parent
51
+ instance.tn_priority = priority
52
+ instance.save()
53
+ return instance
54
+
55
+ @cached_method
56
+ def get_children_pks(self):
57
+ """Get the children pks list."""
58
+ return list(self.get_children_queryset().values_list("id", flat=True))
59
+
60
+ @cached_method
61
+ def get_children_queryset(self):
62
+ """Get the children queryset with prefetch."""
63
+ return self.tn_children.prefetch_related('tn_children')
64
+
65
+ def get_children(self):
66
+ """Get a list containing all children."""
67
+ return list(self.get_children_queryset())
68
+
69
+ def get_children_count(self):
70
+ """Get the children count."""
71
+ return len(self.get_children_pks())
72
+
73
+ def get_first_child(self):
74
+ """Get the first child node or None if it has no children."""
75
+ return self.get_children_queryset().first() if self.is_leaf else None
76
+
77
+ def get_last_child(self):
78
+ """Get the last child node or None if it has no children."""
79
+ return self.get_children_queryset().last() if self.is_leaf else None
80
+
81
+ # The End
@@ -0,0 +1,66 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ TreeNode Descendants 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 treenode_cache, cached_method
12
+
13
+
14
+ class TreeNodeDescendantsMixin(models.Model):
15
+ """TreeNode Descendants Mixin."""
16
+
17
+ class Meta:
18
+ """Moxin Meta Class."""
19
+
20
+ abstract = True
21
+
22
+ @cached_method
23
+ def get_descendants_queryset(self, include_self=False, depth=None):
24
+ """Get the descendants queryset."""
25
+ queryset = self._meta.model.objects\
26
+ .annotate(min_depth=models.Min("parents_set__depth"))\
27
+ .filter(parents_set__parent=self.pk)
28
+
29
+ if depth is not None:
30
+ queryset = queryset.filter(min_depth__lte=depth)
31
+ if include_self and not queryset.filter(pk=self.pk).exists():
32
+ queryset = queryset | self._meta.model.objects.filter(pk=self.pk)
33
+
34
+ return queryset.order_by("min_depth", "tn_priority")
35
+
36
+ @cached_method
37
+ def get_descendants_pks(self, include_self=False, depth=None):
38
+ """Get the descendants pks list."""
39
+ cache_key = treenode_cache.generate_cache_key(
40
+ label=self._meta.label,
41
+ func_name=getattr(self, "get_descendants_queryset").__name__,
42
+ unique_id=self.pk,
43
+ arg={
44
+ "include_self": include_self,
45
+ "depth": depth
46
+ }
47
+ )
48
+ queryset = treenode_cache.get(cache_key)
49
+ if queryset is not None:
50
+ return list(queryset.values_list("id", flat=True))
51
+ elif hasattr(self, "closure_model"):
52
+ return self.closure_model.get_descendants_pks(
53
+ self, include_self, depth
54
+ )
55
+ return []
56
+
57
+ def get_descendants(self, include_self=False, depth=None):
58
+ """Get a list containing all descendants."""
59
+ queryset = self.get_descendants_queryset(include_self, depth)
60
+ return list(queryset)
61
+
62
+ def get_descendants_count(self, include_self=False, depth=None):
63
+ """Get the descendants count."""
64
+ return len(self.get_descendants_pks(include_self, depth))
65
+
66
+ # The End
@@ -0,0 +1,63 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ TreeNode Descendants 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 TreeNodeFamilyMixin(models.Model):
15
+ """TreeNode Family Mixin."""
16
+
17
+ class Meta:
18
+ """Moxin Meta Class."""
19
+
20
+ abstract = True
21
+
22
+ @cached_method
23
+ def get_family_queryset(self):
24
+ """
25
+ Return node family.
26
+
27
+ Return a QuerySet containing the ancestors, itself and the descendants,
28
+ in tree order.
29
+ """
30
+ model = self._meta.model
31
+ queryset = model.objects.filter(
32
+ models.Q(tn_closure__child=self.pk) |
33
+ models.Q(tn_closure__parent=self.pk) |
34
+ models.Q(pk=self.pk)
35
+ ).distinct().order_by("tn_closure__depth", "tn_parent", "tn_priority")
36
+ return queryset
37
+
38
+ @cached_method
39
+ def get_family_pks(self):
40
+ """
41
+ Return node family.
42
+
43
+ Return a pk-list containing the ancestors, the model itself and
44
+ the descendants, in tree order.
45
+ """
46
+ pks = self.get_family_queryset().values_list("id", flat=True)
47
+ return list(pks)
48
+
49
+ def get_family(self):
50
+ """
51
+ Return node family.
52
+
53
+ Return a list containing the ancestors, the model itself and
54
+ the descendants, in tree order.
55
+ """
56
+ queryset = self.get_family_queryset()
57
+ return list(queryset)
58
+
59
+ def get_family_count(self):
60
+ """Return number of nodes in family."""
61
+ return self.get_family_queryset().count()
62
+
63
+ # The End
@@ -0,0 +1,68 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ TreeNode Logical methods 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
+
12
+
13
+ class TreeNodeLogicalMixin(models.Model):
14
+ """TreeNode Logical Mixin."""
15
+
16
+ class Meta:
17
+ """Moxin Meta Class."""
18
+
19
+ abstract = True
20
+
21
+ def is_ancestor_of(self, target_obj):
22
+ """Return True if the current node is ancestor of target_obj."""
23
+ return self in target_obj.get_ancestors(include_self=False)
24
+
25
+ def is_child_of(self, target_obj):
26
+ """Return True if the current node is child of target_obj."""
27
+ return self in target_obj.get_children()
28
+
29
+ def is_descendant_of(self, target_obj):
30
+ """Return True if the current node is descendant of target_obj."""
31
+ return self in target_obj.get_descendants()
32
+
33
+ def is_first_child(self):
34
+ """Return True if the current node is the first child."""
35
+ return self.tn_priority == 0
36
+
37
+ def has_children(self):
38
+ """Return True if the node has children."""
39
+ return self.tn_children.exists()
40
+
41
+ def is_last_child(self):
42
+ """Return True if the current node is the last child."""
43
+ return self.tn_priority == self.get_siblings_count() - 1
44
+
45
+ def is_leaf(self):
46
+ """Return True if the current node is a leaf."""
47
+ return self.tn_children.count() == 0
48
+
49
+ def is_parent_of(self, target_obj):
50
+ """Return True if the current node is parent of target_obj."""
51
+ return self == target_obj.tn_parent
52
+
53
+ def is_root(self):
54
+ """Return True if the current node is root."""
55
+ return self.tn_parent is None
56
+
57
+ def is_root_of(self, target_obj):
58
+ """Return True if the current node is root of target_obj."""
59
+ return self == target_obj.get_root()
60
+
61
+ def is_sibling_of(self, target_obj):
62
+ """Return True if the current node is sibling of target_obj."""
63
+ if target_obj.tn_parent is None and self.tn_parent is None:
64
+ # Both objects are roots
65
+ return True
66
+ return (self.tn_parent == target_obj.tn_parent)
67
+
68
+ # The End
@@ -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