django-fast-treenode 2.1.2__py3-none-any.whl → 2.1.4__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.1.2.dist-info → django_fast_treenode-2.1.4.dist-info}/METADATA +13 -5
- {django_fast_treenode-2.1.2.dist-info → django_fast_treenode-2.1.4.dist-info}/RECORD +16 -17
- {django_fast_treenode-2.1.2.dist-info → django_fast_treenode-2.1.4.dist-info}/WHEEL +1 -1
- treenode/__init__.py +0 -5
- treenode/cache.py +275 -154
- treenode/managers/adjacency.py +6 -4
- treenode/models/adjacency.py +40 -41
- treenode/models/closure.py +2 -23
- treenode/models/mixins/ancestors.py +11 -28
- treenode/models/mixins/children.py +2 -1
- treenode/models/mixins/descendants.py +16 -22
- treenode/models/mixins/node.py +5 -21
- treenode/models/mixins/siblings.py +8 -8
- treenode/version.py +2 -2
- treenode/tests/tests.py +0 -488
- {django_fast_treenode-2.1.2.dist-info → django_fast_treenode-2.1.4.dist-info}/LICENSE +0 -0
- {django_fast_treenode-2.1.2.dist-info → django_fast_treenode-2.1.4.dist-info}/top_level.txt +0 -0
treenode/models/adjacency.py
CHANGED
@@ -74,8 +74,6 @@ class TreeNodeModel(
|
|
74
74
|
|
75
75
|
abstract = True
|
76
76
|
indexes = [
|
77
|
-
models.Index(fields=["tn_parent"]),
|
78
|
-
models.Index(fields=["tn_parent", "id"]),
|
79
77
|
models.Index(fields=["tn_parent", "tn_priority"]),
|
80
78
|
]
|
81
79
|
|
@@ -151,43 +149,45 @@ class TreeNodeModel(
|
|
151
149
|
using=self._state.db,
|
152
150
|
update_fields=kwargs.get("update_fields", None)
|
153
151
|
)
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
#
|
172
|
-
if self.tn_parent
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
152
|
+
with transaction.atomic():
|
153
|
+
# If the object already exists, get the old parent and priority
|
154
|
+
# values
|
155
|
+
is_new = self.pk is None
|
156
|
+
if not is_new:
|
157
|
+
old_parent, old_priority = model.objects\
|
158
|
+
.filter(pk=self.pk)\
|
159
|
+
.values_list('tn_parent', 'tn_priority')\
|
160
|
+
.first()
|
161
|
+
is_move = (old_priority != self.tn_priority)
|
162
|
+
else:
|
163
|
+
force_insert = True
|
164
|
+
is_move = False
|
165
|
+
old_parent = None
|
166
|
+
|
167
|
+
descendants = self.get_descendants(include_self=True)
|
168
|
+
|
169
|
+
# Check if we are trying to move a node to a child
|
170
|
+
if old_parent and old_parent != self.tn_parent and self.tn_parent:
|
171
|
+
# Get pk of children via values_list to avoid creating full
|
172
|
+
# set of objects
|
173
|
+
if self.tn_parent in descendants:
|
174
|
+
raise ValueError(
|
175
|
+
"You cannot move a node into its own child."
|
176
|
+
)
|
177
|
+
|
178
|
+
# Save the object and synchronize with the closing table
|
179
|
+
# Disable signals
|
180
|
+
with (disable_signals(pre_save, model),
|
181
|
+
disable_signals(post_save, model)):
|
182
|
+
|
183
|
+
if is_new or is_move:
|
184
|
+
self._update_priority()
|
185
|
+
super().save(force_insert=force_insert, *args, **kwargs)
|
186
|
+
# Run synchronize
|
187
|
+
if is_new:
|
188
|
+
self.closure_model.insert_node(self)
|
189
|
+
elif is_move:
|
190
|
+
self.closure_model.move_node(descendants)
|
191
191
|
|
192
192
|
# Clear model cache
|
193
193
|
model.clear_cache()
|
@@ -203,7 +203,7 @@ class TreeNodeModel(
|
|
203
203
|
|
204
204
|
def _update_priority(self):
|
205
205
|
"""Update tn_priority field for siblings."""
|
206
|
-
siblings = self.get_siblings()
|
206
|
+
siblings = self.get_siblings(include_self=False)
|
207
207
|
siblings = sorted(siblings, key=lambda x: x.tn_priority)
|
208
208
|
insert_pos = min(self.tn_priority, len(siblings))
|
209
209
|
siblings.insert(insert_pos, self)
|
@@ -214,7 +214,6 @@ class TreeNodeModel(
|
|
214
214
|
# Save changes
|
215
215
|
model = self._meta.model
|
216
216
|
model.objects.bulk_update(siblings, ['tn_priority'])
|
217
|
-
model.clear_cache()
|
218
217
|
|
219
218
|
@classmethod
|
220
219
|
def _get_place(cls, target, position=0):
|
treenode/models/closure.py
CHANGED
@@ -62,7 +62,8 @@ class ClosureModel(models.Model):
|
|
62
62
|
unique_together = (("parent", "child"),)
|
63
63
|
indexes = [
|
64
64
|
models.Index(fields=["parent", "child"]),
|
65
|
-
models.Index(fields=["
|
65
|
+
models.Index(fields=["parent", "depth"]),
|
66
|
+
models.Index(fields=["child", "depth"]),
|
66
67
|
models.Index(fields=["parent", "child", "depth"]),
|
67
68
|
]
|
68
69
|
|
@@ -72,28 +73,6 @@ class ClosureModel(models.Model):
|
|
72
73
|
|
73
74
|
# ----------- Methods of working with tree structure ----------- #
|
74
75
|
|
75
|
-
@classmethod
|
76
|
-
def get_ancestors_pks(cls, node, include_self=True, depth=None):
|
77
|
-
"""Get the ancestors pks list."""
|
78
|
-
options = dict(child_id=node.pk, depth__gte=0 if include_self else 1)
|
79
|
-
if depth:
|
80
|
-
options["depth__lte"] = depth
|
81
|
-
queryset = cls.objects.filter(**options)\
|
82
|
-
.order_by('depth')\
|
83
|
-
.values_list('parent_id', flat=True)
|
84
|
-
return list(queryset.values_list("parent_id", flat=True))
|
85
|
-
|
86
|
-
@classmethod
|
87
|
-
def get_descendants_pks(cls, node, include_self=False, depth=None):
|
88
|
-
"""Get a list containing all descendants."""
|
89
|
-
options = dict(parent_id=node.pk, depth__gte=0 if include_self else 1)
|
90
|
-
if depth:
|
91
|
-
options.update({'depth__lte': depth})
|
92
|
-
queryset = cls.objects.filter(**options)\
|
93
|
-
.order_by('depth')\
|
94
|
-
.values_list('child_id', flat=True)
|
95
|
-
return queryset
|
96
|
-
|
97
76
|
@classmethod
|
98
77
|
def get_root(cls, node):
|
99
78
|
"""Get the root node pk for the current node."""
|
@@ -2,12 +2,13 @@
|
|
2
2
|
"""
|
3
3
|
TreeNode Ancestors Mixin
|
4
4
|
|
5
|
-
Version: 2.1.
|
5
|
+
Version: 2.1.4
|
6
6
|
Author: Timur Kady
|
7
7
|
Email: timurkady@yandex.com
|
8
8
|
"""
|
9
9
|
|
10
10
|
from django.db import models
|
11
|
+
from django.db.models import OuterRef, Subquery, IntegerField, Case, When, Value
|
11
12
|
from ...cache import treenode_cache, cached_method
|
12
13
|
|
13
14
|
|
@@ -22,41 +23,23 @@ class TreeNodeAncestorsMixin(models.Model):
|
|
22
23
|
@cached_method
|
23
24
|
def get_ancestors_queryset(self, include_self=True, depth=None):
|
24
25
|
"""Get the ancestors queryset (ordered from root to parent)."""
|
25
|
-
|
26
|
+
options = dict(child_id=self.pk, depth__gte=0 if include_self else 1)
|
27
|
+
if depth:
|
28
|
+
options.update({'depth__lte': depth})
|
26
29
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
if include_self:
|
31
|
-
qs = qs | self._meta.model.objects.filter(pk=self.pk)
|
32
|
-
|
33
|
-
return qs.distinct().order_by("tn_closure__depth")
|
30
|
+
return self.closure_model.objects\
|
31
|
+
.filter(**options)\
|
32
|
+
.order_by('-depth')
|
34
33
|
|
35
34
|
@cached_method
|
36
35
|
def get_ancestors_pks(self, include_self=True, depth=None):
|
37
36
|
"""Get the ancestors pks list."""
|
38
|
-
|
39
|
-
|
40
|
-
func_name=getattr(self, "get_ancestors_queryset").__name__,
|
41
|
-
unique_id=self.pk,
|
42
|
-
arg={
|
43
|
-
"include_self": include_self,
|
44
|
-
"depth": depth
|
45
|
-
}
|
46
|
-
)
|
47
|
-
queryset = treenode_cache.get(cache_key)
|
48
|
-
if queryset is not None:
|
49
|
-
return list(queryset.values_list("id", flat=True))
|
50
|
-
elif hasattr(self, "closure_model"):
|
51
|
-
return self.closure_model.get_ancestors_pks(
|
52
|
-
self, include_self, depth
|
53
|
-
)
|
54
|
-
return []
|
37
|
+
return self.get_ancestors_queryset(include_self, depth)\
|
38
|
+
.values_list('id', flat=True)
|
55
39
|
|
56
40
|
def get_ancestors(self, include_self=True, depth=None):
|
57
41
|
"""Get a list with all ancestors (ordered from root to self/parent)."""
|
58
|
-
|
59
|
-
return list(queryset)
|
42
|
+
return list(self.get_ancestors_queryset(include_self, depth))
|
60
43
|
|
61
44
|
def get_ancestors_count(self, include_self=True, depth=None):
|
62
45
|
"""Get the ancestors count."""
|
@@ -60,7 +60,8 @@ class TreeNodeChildrenMixin(models.Model):
|
|
60
60
|
@cached_method
|
61
61
|
def get_children_queryset(self):
|
62
62
|
"""Get the children queryset with prefetch."""
|
63
|
-
return self.tn_children.prefetch_related('tn_children')
|
63
|
+
# return self.tn_children.prefetch_related('tn_children')
|
64
|
+
return self._meta.model.objects.filter(tn_parent__pk=self.id)
|
64
65
|
|
65
66
|
def get_children(self):
|
66
67
|
"""Get a list containing all children."""
|
@@ -8,6 +8,8 @@ Email: timurkady@yandex.com
|
|
8
8
|
"""
|
9
9
|
|
10
10
|
from django.db import models
|
11
|
+
from django.db.models import OuterRef, Subquery, Min
|
12
|
+
|
11
13
|
from treenode.cache import treenode_cache, cached_method
|
12
14
|
|
13
15
|
|
@@ -22,37 +24,29 @@ class TreeNodeDescendantsMixin(models.Model):
|
|
22
24
|
@cached_method
|
23
25
|
def get_descendants_queryset(self, include_self=False, depth=None):
|
24
26
|
"""Get the descendants queryset."""
|
25
|
-
|
26
|
-
|
27
|
-
|
27
|
+
Closure = self.closure_model
|
28
|
+
desc_qs = Closure.objects.filter(child=OuterRef('pk'), parent=self.pk)
|
29
|
+
desc_qs = desc_qs.values('child').annotate(
|
30
|
+
mdepth=Min('depth')).values('mdepth')[:1]
|
31
|
+
|
32
|
+
queryset = self._meta.model.objects.annotate(
|
33
|
+
min_depth=Subquery(desc_qs)
|
34
|
+
).filter(min_depth__isnull=False)
|
28
35
|
|
29
36
|
if depth is not None:
|
30
37
|
queryset = queryset.filter(min_depth__lte=depth)
|
31
|
-
|
38
|
+
|
39
|
+
# add self if needed
|
40
|
+
if include_self:
|
32
41
|
queryset = queryset | self._meta.model.objects.filter(pk=self.pk)
|
33
42
|
|
34
|
-
return queryset.order_by(
|
43
|
+
return queryset.order_by('min_depth', 'tn_priority')
|
35
44
|
|
36
45
|
@cached_method
|
37
46
|
def get_descendants_pks(self, include_self=False, depth=None):
|
38
47
|
"""Get the descendants pks list."""
|
39
|
-
|
40
|
-
|
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 []
|
48
|
+
return self.get_descendants_queryset(include_self, depth)\
|
49
|
+
.values_list("id", flat=True)
|
56
50
|
|
57
51
|
def get_descendants(self, include_self=False, depth=None):
|
58
52
|
"""Get a list containing all descendants."""
|
treenode/models/mixins/node.py
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
"""
|
3
3
|
TreeNode Node Mixin
|
4
4
|
|
5
|
-
Version: 2.1.
|
5
|
+
Version: 2.1.3
|
6
6
|
Author: Timur Kady
|
7
7
|
Email: timurkady@yandex.com
|
8
8
|
"""
|
@@ -23,32 +23,16 @@ class TreeNodeNodeMixin(models.Model):
|
|
23
23
|
abstract = True
|
24
24
|
|
25
25
|
@cached_method
|
26
|
-
def get_breadcrumbs(self, attr='
|
26
|
+
def get_breadcrumbs(self, attr='id'):
|
27
27
|
"""Optimized breadcrumbs retrieval with direct cache check."""
|
28
|
+
|
28
29
|
try:
|
29
30
|
self._meta.get_field(attr)
|
30
31
|
except FieldDoesNotExist:
|
31
32
|
raise ValueError(f"Invalid attribute name: {attr}")
|
32
33
|
|
33
|
-
|
34
|
-
|
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]
|
34
|
+
ancestors = self.get_ancestors(include_self=True)
|
35
|
+
return [getattr(node, attr) for node in ancestors]
|
52
36
|
|
53
37
|
@cached_method
|
54
38
|
def get_depth(self):
|
@@ -45,25 +45,25 @@ class TreeNodeSiblingsMixin(models.Model):
|
|
45
45
|
return instance
|
46
46
|
|
47
47
|
@cached_method
|
48
|
-
def get_siblings_queryset(self):
|
48
|
+
def get_siblings_queryset(self, include_self=True):
|
49
49
|
"""Get the siblings queryset with prefetch."""
|
50
50
|
if self.tn_parent:
|
51
|
-
qs = self.
|
51
|
+
qs = self._meta.model.objects.filter(tn_parent=self.tn_parent)
|
52
52
|
else:
|
53
53
|
qs = self._meta.model.objects.filter(tn_parent__isnull=True)
|
54
|
-
return qs.exclude(pk=self.pk)
|
54
|
+
return qs if include_self else qs.exclude(pk=self.pk)
|
55
55
|
|
56
|
-
def get_siblings(self):
|
56
|
+
def get_siblings(self, include_self=True):
|
57
57
|
"""Get a list with all the siblings."""
|
58
58
|
return list(self.get_siblings_queryset())
|
59
59
|
|
60
|
-
def get_siblings_count(self):
|
60
|
+
def get_siblings_count(self, include_self=True):
|
61
61
|
"""Get the siblings count."""
|
62
|
-
return self.get_siblings_queryset().count()
|
62
|
+
return self.get_siblings_queryset(include_self).count()
|
63
63
|
|
64
|
-
def get_siblings_pks(self):
|
64
|
+
def get_siblings_pks(self, include_self=True):
|
65
65
|
"""Get the siblings pks list."""
|
66
|
-
return [item.pk for item in self.get_siblings_queryset()]
|
66
|
+
return [item.pk for item in self.get_siblings_queryset(include_self)]
|
67
67
|
|
68
68
|
def get_first_sibling(self):
|
69
69
|
"""
|
treenode/version.py
CHANGED