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.
@@ -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
- # If the object already exists, get the old parent and priority values
156
- is_new = self.pk is None
157
- if not is_new:
158
- old_parent, old_priority = model.objects\
159
- .filter(pk=self.pk)\
160
- .values_list('tn_parent', 'tn_priority')\
161
- .first()
162
- is_move = (old_priority != self.tn_priority)
163
- else:
164
- force_insert = True
165
- is_move = False
166
- old_parent = None
167
-
168
- # Check if we are trying to move a node to a child
169
- if old_parent and old_parent != self.tn_parent and self.tn_parent:
170
- # Get pk of children via values_list to avoid creating full
171
- # set of objects
172
- if self.tn_parent.pk in self.get_descendants_pks():
173
- raise ValueError("You cannot move a node into its own child.")
174
-
175
- # Save the object and synchronize with the closing table
176
- # Disable signals
177
- with (disable_signals(pre_save, model),
178
- disable_signals(post_save, model)):
179
-
180
- if is_new or is_move:
181
- self._update_priority()
182
- super().save(force_insert=force_insert, *args, **kwargs)
183
- # Run synchronize
184
- if is_new:
185
- self.closure_model.insert_node(self)
186
- elif is_move:
187
- subtree_nodes = self.get_descendants(include_self=True)
188
- self.closure_model.move_node(subtree_nodes)
189
- # Update priorities among neighbors or clear cache if there was
190
- # no movement
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):
@@ -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=["child", "parent"]),
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.0
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
- qs = self._meta.model.objects.filter(tn_closure__child=self.pk)
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
- if depth is not None:
28
- qs = qs.filter(tn_closure__depth__lte=depth)
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
- cache_key = treenode_cache.generate_cache_key(
39
- label=self._meta.label,
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
- queryset = self.get_ancestors_queryset(include_self, depth)
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
- queryset = self._meta.model.objects\
26
- .annotate(min_depth=models.Min("parents_set__depth"))\
27
- .filter(parents_set__parent=self.pk)
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
- if include_self and not queryset.filter(pk=self.pk).exists():
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("min_depth", "tn_priority")
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
- 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 []
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."""
@@ -2,7 +2,7 @@
2
2
  """
3
3
  TreeNode Node Mixin
4
4
 
5
- Version: 2.1.0
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='pk'):
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
- # 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]
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.tn_parent.tn_children.prefetch_related('tn_children')
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
@@ -4,10 +4,10 @@ TreeNode Version Module
4
4
 
5
5
  This module defines the current version of the TreeNode package.
6
6
 
7
- Version: 2.1.2
7
+ Version: 2.1.4
8
8
  Author: Timur Kady
9
9
  Email: timurkady@yandex.com
10
10
  """
11
11
 
12
12
 
13
- __version__ = '2.1.2'
13
+ __version__ = '2.1.4'