django-fast-treenode 2.0.11__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 (68) hide show
  1. {django_fast_treenode-2.0.11.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.11.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 +8 -10
  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 +23 -24
  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/treenode_ajax_rows.html +7 -0
  49. treenode/tests/tests.py +488 -0
  50. treenode/urls.py +10 -6
  51. treenode/utils/__init__.py +2 -0
  52. treenode/utils/aid.py +46 -0
  53. treenode/utils/base16.py +38 -0
  54. treenode/utils/base36.py +3 -1
  55. treenode/utils/db.py +116 -0
  56. treenode/utils/exporter.py +2 -0
  57. treenode/utils/importer.py +0 -1
  58. treenode/utils/radix.py +61 -0
  59. treenode/version.py +2 -2
  60. treenode/views.py +118 -43
  61. treenode/widgets.py +91 -43
  62. django_fast_treenode-2.0.11.dist-info/METADATA +0 -698
  63. django_fast_treenode-2.0.11.dist-info/RECORD +0 -42
  64. treenode/admin.py +0 -439
  65. treenode/docs/Documentation +0 -636
  66. treenode/managers.py +0 -419
  67. treenode/models/proxy.py +0 -669
  68. {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,343 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ TreeNode Proxy Model
4
+
5
+ This module defines an abstract base model `TreeNodeModel` that
6
+ implements hierarchical data storage using the Adjacency Table method.
7
+ It integrates with a Closure Table for optimized tree operations.
8
+
9
+ Features:
10
+ - Supports Adjacency List representation with parent-child relationships.
11
+ - Integrates with a Closure Table for efficient ancestor and descendant
12
+ queries.
13
+ - Provides a caching mechanism for performance optimization.
14
+ - Includes methods for tree traversal, manipulation, and serialization.
15
+
16
+ Version: 2.1.0
17
+ Author: Timur Kady
18
+ Email: timurkady@yandex.com
19
+ """
20
+
21
+ from django.db import models, transaction
22
+ from django.db.models.signals import pre_save, post_save
23
+ from django.utils.translation import gettext_lazy as _
24
+
25
+ from .factory import TreeFactory
26
+ import treenode.models.mixins as mx
27
+ from ..managers import TreeNodeModelManager
28
+ from ..cache import treenode_cache, cached_method
29
+ from ..signals import disable_signals
30
+ from ..utils.base36 import to_base36
31
+ import logging
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ class TreeNodeModel(
37
+ mx.TreeNodeAncestorsMixin, mx.TreeNodeChildrenMixin,
38
+ mx.TreeNodeFamilyMixin, mx.TreeNodeDescendantsMixin,
39
+ mx.TreeNodeLogicalMixin, mx.TreeNodeNodeMixin,
40
+ mx.TreeNodePropertiesMixin, mx.TreeNodeRootsMixin,
41
+ mx.TreeNodeSiblingsMixin, mx.TreeNodeTreeMixin,
42
+ models.Model, metaclass=TreeFactory):
43
+ """
44
+ Abstract TreeNode Model.
45
+
46
+ Implements hierarchy storage using the Adjacency Table method.
47
+ To increase performance, it has an additional attribute - a model
48
+ that stores data from the Adjacency Table in the form of
49
+ a Closure Table.
50
+ """
51
+
52
+ treenode_display_field = None
53
+ treenode_sort_field = None # not now
54
+ closure_model = None
55
+
56
+ tn_parent = models.ForeignKey(
57
+ 'self',
58
+ related_name='tn_children',
59
+ on_delete=models.CASCADE,
60
+ null=True,
61
+ blank=True,
62
+ verbose_name=_('Parent')
63
+ )
64
+
65
+ tn_priority = models.PositiveIntegerField(
66
+ default=0,
67
+ verbose_name=_('Priority')
68
+ )
69
+
70
+ objects = TreeNodeModelManager()
71
+
72
+ class Meta:
73
+ """Meta Class."""
74
+
75
+ abstract = True
76
+ indexes = [
77
+ models.Index(fields=["tn_parent"]),
78
+ models.Index(fields=["tn_parent", "id"]),
79
+ models.Index(fields=["tn_parent", "tn_priority"]),
80
+ ]
81
+
82
+ def __str__(self):
83
+ """Display information about a class object."""
84
+ if self.treenode_display_field:
85
+ return str(getattr(self, self.treenode_display_field))
86
+ else:
87
+ return 'Node %d' % self.pk
88
+
89
+ # ---------------------------------------------------
90
+ # Public methods
91
+ # ---------------------------------------------------
92
+
93
+ @classmethod
94
+ def clear_cache(cls):
95
+ """Clear cache for this model only."""
96
+ treenode_cache.invalidate(cls._meta.label)
97
+
98
+ @classmethod
99
+ def get_closure_model(cls):
100
+ """Return ClosureModel for class."""
101
+ return cls.closure_model
102
+
103
+ def delete(self, cascade=True):
104
+ """Delete node."""
105
+ model = self._meta.model
106
+ parent = self.get_parent()
107
+
108
+ if not cascade:
109
+ new_siblings_count = parent.get_siblings_count()
110
+ # Get a list of children
111
+ children = self.get_children()
112
+ if children:
113
+ # Move them to one level up
114
+ for child in children:
115
+ child.tn_parent = self.tn_parent
116
+ child.tn_priority = new_siblings_count + child.tn_priority
117
+ # Udate both models in bulk
118
+ model.objects.bulk_update(
119
+ children,
120
+ ("tn_parent",),
121
+ batch_size=1000
122
+ )
123
+
124
+ # All descendants and related records in the ClosingModel will be
125
+ # cleared by cascading the removal of ForeignKeys.
126
+ super().delete()
127
+ # Can be excluded. The cache has already been cleared by the manager.
128
+ model.clear_cache()
129
+
130
+ # Update tn_priority
131
+ if parent is None:
132
+ siblings = model.get_roots()
133
+ else:
134
+ siblings = parent.get_children()
135
+
136
+ if siblings:
137
+ siblings = [node for node in siblings if node.pk != self.pk]
138
+ sorted_siblings = sorted(siblings, key=lambda x: x.tn_priority)
139
+ for index, node in enumerate(sorted_siblings):
140
+ node.tn_priority = index
141
+ model.objects.bulk_update(siblings, ['tn_priority'])
142
+
143
+ def save(self, force_insert=False, *args, **kwargs):
144
+ """Save a model instance with sync closure table."""
145
+ model = self._meta.model
146
+ # Send signal pre_save
147
+ pre_save.send(
148
+ sender=model,
149
+ instance=self,
150
+ raw=False,
151
+ using=self._state.db,
152
+ update_fields=kwargs.get("update_fields", None)
153
+ )
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
191
+
192
+ # Clear model cache
193
+ model.clear_cache()
194
+ # Send signal post_save
195
+ post_save.send(sender=model, instance=self, created=is_new)
196
+
197
+ # ---------------------------------------------------
198
+ # Prived methods
199
+ #
200
+ # The usage of these methods is only allowed by developers. In future
201
+ # versions, these methods may be changed or removed without any warning.
202
+ # ---------------------------------------------------
203
+
204
+ def _update_priority(self):
205
+ """Update tn_priority field for siblings."""
206
+ siblings = self.get_siblings()
207
+ siblings = sorted(siblings, key=lambda x: x.tn_priority)
208
+ insert_pos = min(self.tn_priority, len(siblings))
209
+ siblings.insert(insert_pos, self)
210
+ for index, node in enumerate(siblings):
211
+ node.tn_priority = index
212
+ siblings = [s for s in siblings if s.tn_priority != self.tn_priority]
213
+
214
+ # Save changes
215
+ model = self._meta.model
216
+ model.objects.bulk_update(siblings, ['tn_priority'])
217
+ model.clear_cache()
218
+
219
+ @classmethod
220
+ def _get_place(cls, target, position=0):
221
+ """
222
+ Get position relative to the target node.
223
+
224
+ position – the position, relative to the target node, where the
225
+ current node object will be moved to, can be one of:
226
+
227
+ - first-root: the node will be the first root node;
228
+ - last-root: the node will be the last root node;
229
+ - sorted-root: the new node will be moved after sorting by
230
+ the treenode_sort_field field;
231
+
232
+ - first-sibling: the node will be the new leftmost sibling of the
233
+ target node;
234
+ - left-sibling: the node will take the target node’s place, which will
235
+ be moved to the target position with shifting follows nodes;
236
+ - right-sibling: the node will be moved to the position after the
237
+ target node;
238
+ - last-sibling: the node will be the new rightmost sibling of the
239
+ target node;
240
+ - sorted-sibling: the new node will be moved after sorting by
241
+ the treenode_sort_field field;
242
+
243
+ - first-child: the node will be the first child of the target node;
244
+ - last-child: the node will be the new rightmost child of the target
245
+ - sorted-child: the new node will be moved after sorting by
246
+ the treenode_sort_field field.
247
+
248
+ """
249
+ if isinstance(position, int):
250
+ priority = position
251
+ elif not isinstance(position, str) or '-' not in position:
252
+ raise ValueError(f"Invalid position format: {position}")
253
+
254
+ part1, part2 = position.split('-')
255
+ if part1 not in {'first', 'last', 'left', 'right', 'sorted'} or \
256
+ part2 not in {'root', 'child', 'sibling'}:
257
+ raise ValueError(f"Unknown position type: {position}")
258
+
259
+ # Determine the parent depending on the type of position
260
+ if part2 == 'root':
261
+ parent = None
262
+ elif part2 == 'sibling':
263
+ parent = target.tn_parent
264
+ elif part2 == 'child':
265
+ parent = target
266
+ else:
267
+ parent = None
268
+
269
+ if parent:
270
+ count = parent.get_children_count()
271
+ else:
272
+ count = cls.get_roots_count()
273
+
274
+ # Определяем позицию (приоритет)
275
+ if part1 == 'first':
276
+ priority = 0
277
+ elif part1 == 'left':
278
+ priority = target.tn_priority
279
+ elif part1 == 'right':
280
+ priority = target.tn_priority + 1
281
+ elif part1 in {'last', 'sorted'}:
282
+ priority = count
283
+ else:
284
+ priority = count
285
+
286
+ return parent, priority
287
+
288
+ @classmethod
289
+ @cached_method
290
+ def _sort_node_list(cls, nodes):
291
+ """
292
+ Sort list of nodes by materialized path oreder.
293
+
294
+ Collect the materialized path without accessing the DB and perform
295
+ sorting
296
+ """
297
+ # Create a list of tuples: (node, materialized_path)
298
+ nodes_with_path = [(node, node.tn_order) for node in nodes]
299
+ # Sort the list by the materialized path
300
+ nodes_with_path.sort(key=lambda tup: tup[1])
301
+ # Extract sorted nodes
302
+ return [tup[0] for tup in nodes_with_path]
303
+
304
+ @classmethod
305
+ @cached_method
306
+ def _get_sorting_map(self, model):
307
+ """Return the sorting map of model objects."""
308
+ # --1 Extracting data from the model
309
+ qs_list = model.objects.values_list('pk', 'tn_parent', 'tn_priority')
310
+ node_map = {pk: {"pk": pk, "parent": tn_parent, "priority": tn_priority}
311
+ for pk, tn_parent, tn_priority in qs_list}
312
+
313
+ def build_path(node_id):
314
+ """Recursive path construction."""
315
+ path = []
316
+ while node_id:
317
+ node = node_map.get(node_id)
318
+ if not node:
319
+ break
320
+ path.append(node["priority"])
321
+ node_id = node["parent"]
322
+ return list(reversed(path))
323
+
324
+ # -- 2. Collecting materialized paths
325
+ paths = []
326
+ for pk, node in node_map.items():
327
+ path = build_path(pk)
328
+ paths.append({"pk": pk, "path": path})
329
+
330
+ # -- 3. Convert paths to strings
331
+ for item in paths:
332
+ pk_path = item["path"]
333
+ segments = [to_base36(i).rjust(6, '0') for i in pk_path]
334
+ item["path_str"] = "".join(segments)
335
+
336
+ # -- 5. Sort by string representation of the path
337
+ paths.sort(key=lambda x: x["path_str"])
338
+ index_map = {i: item["pk"] for i, item in enumerate(paths)}
339
+
340
+ return index_map
341
+
342
+
343
+ # The end
@@ -22,3 +22,6 @@ class classproperty(object):
22
22
  def __get__(self, instance, owner):
23
23
  """Get."""
24
24
  return self.getter(owner)
25
+
26
+
27
+ # The end
@@ -11,16 +11,17 @@ Features:
11
11
  - Implements cached queries for improved performance.
12
12
  - Provides bulk operations for inserting, moving, and deleting nodes.
13
13
 
14
- Version: 2.0.11
14
+ Version: 2.1.0
15
15
  Author: Timur Kady
16
16
  Email: timurkady@yandex.com
17
17
  """
18
18
 
19
19
 
20
20
  from django.db import models, transaction
21
+ from django.db.models.signals import pre_save, post_save
21
22
 
22
23
  from ..managers import ClosureModelManager
23
- from ..cache import cached_method, treenode_cache
24
+ from ..signals import disable_signals
24
25
 
25
26
 
26
27
  class ClosureModel(models.Model):
@@ -44,6 +45,14 @@ class ClosureModel(models.Model):
44
45
 
45
46
  depth = models.PositiveIntegerField()
46
47
 
48
+ node = models.OneToOneField(
49
+ 'TreeNodeModel',
50
+ related_name="tn_closure",
51
+ on_delete=models.CASCADE,
52
+ null=True,
53
+ blank=True,
54
+ )
55
+
47
56
  objects = ClosureModelManager()
48
57
 
49
58
  class Meta:
@@ -53,6 +62,7 @@ class ClosureModel(models.Model):
53
62
  unique_together = (("parent", "child"),)
54
63
  indexes = [
55
64
  models.Index(fields=["parent", "child"]),
65
+ models.Index(fields=["child", "parent"]),
56
66
  models.Index(fields=["parent", "child", "depth"]),
57
67
  ]
58
68
 
@@ -63,39 +73,34 @@ class ClosureModel(models.Model):
63
73
  # ----------- Methods of working with tree structure ----------- #
64
74
 
65
75
  @classmethod
66
- def clear_cache(cls):
67
- """Clear cache for this model only."""
68
- treenode_cache.invalidate(cls._meta.label)
69
-
70
- @classmethod
71
- @cached_method
72
76
  def get_ancestors_pks(cls, node, include_self=True, depth=None):
73
77
  """Get the ancestors pks list."""
74
78
  options = dict(child_id=node.pk, depth__gte=0 if include_self else 1)
75
79
  if depth:
76
80
  options["depth__lte"] = depth
77
- queryset = cls.objects.filter(**options).order_by('depth')
81
+ queryset = cls.objects.filter(**options)\
82
+ .order_by('depth')\
83
+ .values_list('parent_id', flat=True)
78
84
  return list(queryset.values_list("parent_id", flat=True))
79
85
 
80
86
  @classmethod
81
- @cached_method
82
87
  def get_descendants_pks(cls, node, include_self=False, depth=None):
83
88
  """Get a list containing all descendants."""
84
89
  options = dict(parent_id=node.pk, depth__gte=0 if include_self else 1)
85
90
  if depth:
86
91
  options.update({'depth__lte': depth})
87
- queryset = cls.objects.filter(**options)
88
- return list(queryset.values_list("child_id", flat=True))
92
+ queryset = cls.objects.filter(**options)\
93
+ .order_by('depth')\
94
+ .values_list('child_id', flat=True)
95
+ return queryset
89
96
 
90
97
  @classmethod
91
- @cached_method
92
98
  def get_root(cls, node):
93
99
  """Get the root node pk for the current node."""
94
100
  queryset = cls.objects.filter(child=node).order_by('-depth')
95
- return queryset.firts().parent if queryset.count() > 0 else None
101
+ return queryset.first().parent if queryset.count() > 0 else None
96
102
 
97
103
  @classmethod
98
- @cached_method
99
104
  def get_depth(cls, node):
100
105
  """Get the node depth (how deep the node is in the tree)."""
101
106
  result = cls.objects.filter(child__pk=node.pk).aggregate(
@@ -104,7 +109,6 @@ class ClosureModel(models.Model):
104
109
  return result if result is not None else 0
105
110
 
106
111
  @classmethod
107
- @cached_method
108
112
  def get_level(cls, node):
109
113
  """Get the node level (starting from 1)."""
110
114
  return cls.objects.filter(child__pk=node.pk).aggregate(
@@ -116,8 +120,6 @@ class ClosureModel(models.Model):
116
120
  """Add a node to a Closure table."""
117
121
  # Call bulk_create passing a single object
118
122
  cls.objects.bulk_create([node], batch_size=1000)
119
- # Clear cache
120
- cls.clear_cache()
121
123
 
122
124
  @classmethod
123
125
  @transaction.atomic
@@ -125,20 +127,17 @@ class ClosureModel(models.Model):
125
127
  """Move a nodes (node and its subtree) to a new parent."""
126
128
  # Call bulk_update passing a single object
127
129
  cls.objects.bulk_update(nodes, batch_size=1000)
128
- # Clear cache
129
- cls.clear_cache()
130
130
 
131
131
  @classmethod
132
132
  @transaction.atomic
133
133
  def delete_all(cls):
134
134
  """Clear the Closure Table."""
135
- # Clear cache
136
- cls.clear_cache()
137
135
  cls.objects.all().delete()
138
136
 
139
137
  def save(self, force_insert=False, *args, **kwargs):
140
138
  """Save method."""
141
- super().save(force_insert, *args, **kwargs)
142
- self._meta.model.clear_cache()
139
+ with (disable_signals(pre_save, self._meta.model),
140
+ disable_signals(post_save, self._meta.model)):
141
+ super().save(force_insert, *args, **kwargs)
143
142
 
144
143
  # The End
@@ -10,7 +10,7 @@ Features:
10
10
  - Dynamically creates and assigns a Closure Model for each TreeNodeModel.
11
11
  - Facilitates the management of hierarchical relationships.
12
12
 
13
- Version: 2.0.0
13
+ Version: 2.1.0
14
14
  Author: Timur Kady
15
15
  Email: timurkady@yandex.com
16
16
  """
@@ -18,7 +18,7 @@ Email: timurkady@yandex.com
18
18
 
19
19
  import sys
20
20
  from django.db import models
21
- from .closure import ClosureModel # Используем готовый ClosureModel
21
+ from .closure import ClosureModel
22
22
 
23
23
 
24
24
  class TreeFactory(models.base.ModelBase):
@@ -61,11 +61,21 @@ class TreeFactory(models.base.ModelBase):
61
61
  on_delete=models.CASCADE,
62
62
  ),
63
63
 
64
+ "node": models.OneToOneField(
65
+ cls._meta.model,
66
+ related_name="tn_closure",
67
+ on_delete=models.CASCADE,
68
+ null=True,
69
+ blank=True,
70
+ ),
71
+
64
72
  "__module__": cls.__module__
65
73
  }
74
+
66
75
  closure_model = type(closure_name, (ClosureModel,), fields)
67
76
  setattr(sys.modules[cls.__module__], closure_name, closure_model)
68
77
 
69
78
  cls.closure_model = closure_model
70
79
 
80
+
71
81
  # The End
@@ -0,0 +1,23 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from .ancestors import TreeNodeAncestorsMixin
4
+ from .children import TreeNodeChildrenMixin
5
+ from .descendants import TreeNodeDescendantsMixin
6
+ from .family import TreeNodeFamilyMixin
7
+ from .logical import TreeNodeLogicalMixin
8
+ from .node import TreeNodeNodeMixin
9
+ from .properties import TreeNodePropertiesMixin
10
+ from .roots import TreeNodeRootsMixin
11
+ from .siblings import TreeNodeSiblingsMixin
12
+ from .tree import TreeNodeTreeMixin
13
+
14
+
15
+ __all__ = [
16
+ "TreeNodeAncestorsMixin", "TreeNodeChildrenMixin", "TreeNodeFamilyMixin",
17
+ "TreeNodeDescendantsMixin", "TreeNodeLogicalMixin", "TreeNodeNodeMixin",
18
+ "TreeNodePropertiesMixin", "TreeNodeRootsMixin", "TreeNodeSiblingsMixin",
19
+ "TreeNodeTreeMixin"
20
+ ]
21
+
22
+
23
+ # The End
@@ -0,0 +1,65 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ TreeNode Ancestors 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 ...cache import treenode_cache, cached_method
12
+
13
+
14
+ class TreeNodeAncestorsMixin(models.Model):
15
+ """TreeNode Ancestors Mixin."""
16
+
17
+ class Meta:
18
+ """Moxin Meta Class."""
19
+
20
+ abstract = True
21
+
22
+ @cached_method
23
+ def get_ancestors_queryset(self, include_self=True, depth=None):
24
+ """Get the ancestors queryset (ordered from root to parent)."""
25
+ qs = self._meta.model.objects.filter(tn_closure__child=self.pk)
26
+
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")
34
+
35
+ @cached_method
36
+ def get_ancestors_pks(self, include_self=True, depth=None):
37
+ """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 []
55
+
56
+ def get_ancestors(self, include_self=True, depth=None):
57
+ """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)
60
+
61
+ def get_ancestors_count(self, include_self=True, depth=None):
62
+ """Get the ancestors count."""
63
+ return len(self.get_ancestors_pks(include_self, depth))
64
+
65
+ # The End
@@ -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