django-fast-treenode 2.1.5__py3-none-any.whl → 3.0.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 (107) hide show
  1. django_fast_treenode-3.0.0.dist-info/METADATA +203 -0
  2. django_fast_treenode-3.0.0.dist-info/RECORD +90 -0
  3. {django_fast_treenode-2.1.5.dist-info → django_fast_treenode-3.0.0.dist-info}/WHEEL +1 -1
  4. treenode/admin/__init__.py +0 -5
  5. treenode/admin/admin.py +137 -208
  6. treenode/admin/changelist.py +21 -39
  7. treenode/admin/exporter.py +170 -0
  8. treenode/admin/importer.py +171 -0
  9. treenode/admin/mixin.py +291 -0
  10. treenode/apps.py +42 -20
  11. treenode/cache.py +192 -303
  12. treenode/forms.py +45 -65
  13. treenode/managers/__init__.py +4 -20
  14. treenode/managers/managers.py +216 -0
  15. treenode/managers/queries.py +233 -0
  16. treenode/managers/tasks.py +167 -0
  17. treenode/models/__init__.py +8 -5
  18. treenode/models/decorators.py +54 -0
  19. treenode/models/factory.py +44 -68
  20. treenode/models/mixins/__init__.py +2 -1
  21. treenode/models/mixins/ancestors.py +44 -20
  22. treenode/models/mixins/children.py +33 -26
  23. treenode/models/mixins/descendants.py +33 -22
  24. treenode/models/mixins/family.py +25 -15
  25. treenode/models/mixins/logical.py +23 -21
  26. treenode/models/mixins/node.py +162 -104
  27. treenode/models/mixins/properties.py +22 -16
  28. treenode/models/mixins/roots.py +59 -15
  29. treenode/models/mixins/siblings.py +46 -43
  30. treenode/models/mixins/tree.py +212 -153
  31. treenode/models/mixins/update.py +154 -0
  32. treenode/models/models.py +365 -0
  33. treenode/settings.py +28 -0
  34. treenode/static/{treenode/css → css}/tree_widget.css +1 -1
  35. treenode/static/{treenode/css → css}/treenode_admin.css +43 -2
  36. treenode/static/css/treenode_tabs.css +51 -0
  37. treenode/static/js/lz-string.min.js +1 -0
  38. treenode/static/{treenode/js → js}/tree_widget.js +9 -23
  39. treenode/static/js/treenode_admin.js +531 -0
  40. treenode/static/vendors/jquery-ui/AUTHORS.txt +384 -0
  41. treenode/static/vendors/jquery-ui/LICENSE.txt +43 -0
  42. treenode/static/vendors/jquery-ui/external/jquery/jquery.js +10716 -0
  43. treenode/static/vendors/jquery-ui/images/ui-icons_444444_256x240.png +0 -0
  44. treenode/static/vendors/jquery-ui/images/ui-icons_555555_256x240.png +0 -0
  45. treenode/static/vendors/jquery-ui/images/ui-icons_777620_256x240.png +0 -0
  46. treenode/static/vendors/jquery-ui/images/ui-icons_777777_256x240.png +0 -0
  47. treenode/static/vendors/jquery-ui/images/ui-icons_cc0000_256x240.png +0 -0
  48. treenode/static/vendors/jquery-ui/images/ui-icons_ffffff_256x240.png +0 -0
  49. treenode/static/vendors/jquery-ui/index.html +297 -0
  50. treenode/static/vendors/jquery-ui/jquery-ui.css +438 -0
  51. treenode/static/vendors/jquery-ui/jquery-ui.js +5223 -0
  52. treenode/static/vendors/jquery-ui/jquery-ui.min.css +7 -0
  53. treenode/static/vendors/jquery-ui/jquery-ui.min.js +6 -0
  54. treenode/static/vendors/jquery-ui/jquery-ui.structure.css +16 -0
  55. treenode/static/vendors/jquery-ui/jquery-ui.structure.min.css +5 -0
  56. treenode/static/vendors/jquery-ui/jquery-ui.theme.css +439 -0
  57. treenode/static/vendors/jquery-ui/jquery-ui.theme.min.css +5 -0
  58. treenode/static/vendors/jquery-ui/package.json +82 -0
  59. treenode/templates/admin/treenode_changelist.html +25 -0
  60. treenode/templates/admin/treenode_import_export.html +85 -0
  61. treenode/templates/admin/treenode_rows.html +57 -0
  62. treenode/tests.py +3 -0
  63. treenode/urls.py +6 -27
  64. treenode/utils/__init__.py +0 -15
  65. treenode/utils/db/__init__.py +7 -0
  66. treenode/utils/db/compiler.py +114 -0
  67. treenode/utils/db/db_vendor.py +50 -0
  68. treenode/utils/db/service.py +84 -0
  69. treenode/utils/db/sqlcompat.py +60 -0
  70. treenode/utils/db/sqlquery.py +70 -0
  71. treenode/version.py +2 -2
  72. treenode/views/__init__.py +5 -0
  73. treenode/views/autoapi.py +91 -0
  74. treenode/views/autocomplete.py +52 -0
  75. treenode/views/children.py +41 -0
  76. treenode/views/common.py +23 -0
  77. treenode/views/crud.py +209 -0
  78. treenode/views/search.py +48 -0
  79. treenode/widgets.py +27 -44
  80. django_fast_treenode-2.1.5.dist-info/METADATA +0 -165
  81. django_fast_treenode-2.1.5.dist-info/RECORD +0 -63
  82. treenode/admin/mixins.py +0 -302
  83. treenode/managers/adjacency.py +0 -205
  84. treenode/managers/closure.py +0 -278
  85. treenode/models/adjacency.py +0 -342
  86. treenode/models/classproperty.py +0 -27
  87. treenode/models/closure.py +0 -122
  88. treenode/static/treenode/js/.gitkeep +0 -1
  89. treenode/static/treenode/js/treenode_admin.js +0 -131
  90. treenode/templates/admin/export_success.html +0 -26
  91. treenode/templates/admin/tree_node_changelist.html +0 -19
  92. treenode/templates/admin/tree_node_export.html +0 -27
  93. treenode/templates/admin/tree_node_import.html +0 -45
  94. treenode/templates/admin/tree_node_import_report.html +0 -32
  95. treenode/templates/widgets/tree_widget.css +0 -23
  96. treenode/utils/aid.py +0 -46
  97. treenode/utils/base16.py +0 -38
  98. treenode/utils/base36.py +0 -37
  99. treenode/utils/db.py +0 -116
  100. treenode/utils/exporter.py +0 -196
  101. treenode/utils/importer.py +0 -328
  102. treenode/utils/radix.py +0 -61
  103. treenode/views.py +0 -184
  104. {django_fast_treenode-2.1.5.dist-info → django_fast_treenode-3.0.0.dist-info}/licenses/LICENSE +0 -0
  105. {django_fast_treenode-2.1.5.dist-info → django_fast_treenode-3.0.0.dist-info}/top_level.txt +0 -0
  106. /treenode/static/{treenode → css}/.gitkeep +0 -0
  107. /treenode/static/{treenode/css → js}/.gitkeep +0 -0
@@ -1,278 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- Closure Table Manager and QuerySet
4
-
5
- This module defines custom managers and query sets for the ClosureModel.
6
- It includes optimized bulk operations for handling hierarchical data
7
- using the Closure Table approach.
8
-
9
- Version: 2.1.0
10
- Author: Timur Kady
11
- Email: timurkady@yandex.com
12
- """
13
-
14
- from collections import deque, defaultdict
15
- from django.db import models, transaction
16
-
17
-
18
- # ----------------------------------------------------------------------------
19
- # Closere Model
20
- # ----------------------------------------------------------------------------
21
-
22
-
23
- class ClosureQuerySet(models.QuerySet):
24
- """QuerySet для ClosureModel."""
25
-
26
- def sort_nodes(self, node_list):
27
- """
28
- Sort nodes topologically.
29
-
30
- Returns a list of nodes sorted from roots to leaves.
31
- A node is considered a root if its tn_parent is None or its
32
- parent is not in node_list.
33
- """
34
- visited = set() # Will store the ids of already processed nodes
35
- result = []
36
- # Set of node ids included in the original list
37
- node_ids = {node.id for node in node_list}
38
-
39
- def dfs(node):
40
- if node.id in visited:
41
- return
42
- # If there is a parent and it is included in node_list, then
43
- # process it first
44
- if node.tn_parent and node.tn_parent_id in node_ids:
45
- dfs(node.tn_parent)
46
- visited.add(node.id)
47
- result.append(node)
48
-
49
- for n in node_list:
50
- dfs(n)
51
-
52
- return result
53
-
54
- @transaction.atomic
55
- def bulk_create(self, objs, batch_size=1000, *args, **kwargs):
56
- """Insert new nodes in bulk."""
57
- result = []
58
-
59
- # 1. Topological sorting of nodes
60
- objs = self.sort_nodes(objs)
61
-
62
- # 1. Create self-links for all nodes: (node, node, 0, node).
63
- self_links = [
64
- self.model(parent=obj, child=obj, depth=0, node=obj)
65
- for obj in objs
66
- ]
67
- result.extend(
68
- super(ClosureQuerySet, self).bulk_create(
69
- self_links, batch_size, *args, **kwargs
70
- )
71
- )
72
-
73
- # 2. We form a display: parent id -> list of its children.
74
- children_map = defaultdict(list)
75
- for obj in objs:
76
- if obj.tn_parent_id:
77
- children_map[obj.tn_parent_id].append(obj)
78
-
79
- # 3. We try to determine the root nodes (with tn_parent == None).
80
- root_nodes = [obj for obj in objs if obj.tn_parent is None]
81
-
82
- # If there are no root nodes, then we insert a subtree.
83
- if not root_nodes:
84
- # Define the "top" nodes of the subtree:
85
- # those whose parent is not included in the list of inserted objects
86
- objs_ids = {obj.id for obj in objs if obj.id is not None}
87
- top_nodes = [
88
- obj for obj in objs if obj.tn_parent_id not in objs_ids
89
- ]
90
-
91
- # For each such node, if the parent exists, get the closure records
92
- # for the parent and add new records for (ancestor -> node) with
93
- # depth = ancestor.depth + 1.
94
- new_entries = []
95
- for node in top_nodes:
96
- if node.tn_parent_id:
97
- parent_closures = self.model.objects.filter(
98
- child_id=node.tn_parent_id
99
- )
100
- for ancestor in parent_closures:
101
- new_entries.append(
102
- self.model(
103
- parent=ancestor.parent,
104
- child=node,
105
- depth=ancestor.depth + 1
106
- )
107
- )
108
- if new_entries:
109
- result.extend(
110
- super(ClosureQuerySet, self).bulk_create(
111
- new_entries, batch_size, *args, **kwargs
112
- )
113
-
114
- )
115
-
116
- # Set the top-level nodes of the subtree as the starting ones for
117
- # traversal.
118
- current_nodes = top_nodes
119
- else:
120
- current_nodes = root_nodes
121
-
122
- def process_level(current_nodes):
123
- """Recursive function for traversing levels."""
124
- next_level = []
125
- new_entries = []
126
- for node in current_nodes:
127
- # For the current node, we get all the closure records
128
- # (its ancestors).
129
- ancestors = self.model.objects.filter(child=node)
130
- for child in children_map.get(node.id, []):
131
- for ancestor in ancestors:
132
- new_entries.append(
133
- self.model(
134
- parent=ancestor.parent,
135
- child=child,
136
- depth=ancestor.depth + 1
137
- )
138
- )
139
- next_level.append(child)
140
- if new_entries:
141
- result.extend(
142
- super(ClosureQuerySet, self).bulk_create(
143
- new_entries, batch_size, *args, **kwargs
144
- )
145
- )
146
- if next_level:
147
- process_level(next_level)
148
-
149
- # 4. Run traversing levels.
150
- process_level(current_nodes)
151
- return result
152
-
153
- @transaction.atomic
154
- def bulk_update(self, objs, fields=None, batch_size=1000):
155
- """
156
- Update the closure table for objects whose tn_parent has changed.
157
-
158
- It is assumed that all objects from the objs list are already in the
159
- closure table, but their links (both for parents and for children) may
160
- have changed.
161
-
162
- Algorithm:
163
- 1. Form a mapping: parent id → list of its children.
164
- 2. Determine the root nodes of the subtree to be updated:
165
- – A node is considered a root if its tn_parent is None or its
166
- parent is not in objs.
167
- 3. For each root node, if there is an external parent, get its
168
- closure from the database.
169
- Then form closure records for the node (all external links with
170
- increased depth and self-reference).
171
- 4. Using BFS, traverse the subtree: for each node, for each of its
172
- children, create records using parent records (increased by 1) and add
173
- a self-reference for the child.
174
- 5. Remove old closure records for objects from objs and save new ones in
175
- batches.
176
- """
177
- # 1. Topological sorting of nodes
178
- objs = self.sort_nodes(objs)
179
-
180
- # 2. Let's build a mapping: parent id → list of children
181
- children_map = defaultdict(list)
182
- for obj in objs:
183
- if obj.tn_parent_id:
184
- children_map[obj.tn_parent_id].append(obj)
185
-
186
- # Set of id's of objects to be updated
187
- objs_ids = {obj.id for obj in objs}
188
-
189
- # 3. Determine the root nodes of the updated subtree:
190
- # A node is considered root if its tn_parent is either None or its
191
- # parent is not in objs.
192
- roots = [
193
- obj for obj in objs
194
- if (obj.tn_parent is None) or (obj.tn_parent_id not in objs_ids)
195
- ]
196
-
197
- # List for accumulating new closure records
198
- new_closure_entries = []
199
-
200
- # Queue for BFS: each element is a tuple (node, node_closure), where
201
- # node_closure is a list of closure entries for that node.
202
- queue = deque()
203
- for node in roots:
204
- if node.tn_parent_id:
205
- # Get the closure of the external parent from the database
206
- external_ancestors = list(
207
- self.model.objects.filter(child_id=node.tn_parent_id)
208
- .values('parent_id', 'depth')
209
- )
210
- # For each ancestor found, create an entry for node with
211
- # depth+1
212
- node_closure = [
213
- self.model(
214
- parent_id=entry['parent_id'],
215
- child=node,
216
- depth=entry['depth'] + 1
217
- )
218
- for entry in external_ancestors
219
- ]
220
- else:
221
- node_closure = []
222
- # Add self-reference (node ​​→ node, depth 0)
223
- node_closure.append(
224
- self.model(parent=node, child=node, depth=0, node=node)
225
- )
226
-
227
- # Save records for the current node and put them in a queue for
228
- # processing its subtree
229
- new_closure_entries.extend(node_closure)
230
- queue.append((node, node_closure))
231
-
232
- # 4. BFS subtree traversal: for each node, create a closure for its
233
- # children
234
- while queue:
235
- parent_node, parent_closure = queue.popleft()
236
- for child in children_map.get(parent_node.id, []):
237
- # For the child, new closure records:
238
- # for each parent record, create (ancestor -> child) with
239
- # depth+1
240
- child_closure = [
241
- self.model(
242
- parent_id=entry.parent_id,
243
- child=child,
244
- depth=entry.depth + 1
245
- )
246
- for entry in parent_closure
247
- ]
248
- # Add a self-link for the child
249
- child_closure.append(
250
- self.model(parent=child, child=child, depth=0)
251
- )
252
-
253
- new_closure_entries.extend(child_closure)
254
- queue.append((child, child_closure))
255
-
256
- # 5. Remove old closure records for updatable objects
257
- self.model.objects.filter(child_id__in=objs_ids).delete()
258
-
259
- # 6. Save new records in batches
260
- super(ClosureQuerySet, self).bulk_create(new_closure_entries)
261
-
262
-
263
- class ClosureModelManager(models.Manager):
264
- """ClosureModel Manager."""
265
-
266
- def get_queryset(self):
267
- """get_queryset method."""
268
- return ClosureQuerySet(self.model, using=self._db)
269
-
270
- def bulk_create(self, objs, batch_size=1000):
271
- """Create objects in bulk."""
272
- return self.get_queryset().bulk_create(objs, batch_size=batch_size)
273
-
274
- def bulk_update(self, objs, fields=None, batch_size=1000):
275
- """Move nodes in ClosureModel."""
276
- return self.get_queryset().bulk_update(
277
- objs, fields, batch_size=batch_size
278
- )
@@ -1,342 +0,0 @@
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", "tn_priority"]),
78
- ]
79
-
80
- def __str__(self):
81
- """Display information about a class object."""
82
- if self.treenode_display_field:
83
- return str(getattr(self, self.treenode_display_field))
84
- else:
85
- return 'Node %d' % self.pk
86
-
87
- # ---------------------------------------------------
88
- # Public methods
89
- # ---------------------------------------------------
90
-
91
- @classmethod
92
- def clear_cache(cls):
93
- """Clear cache for this model only."""
94
- treenode_cache.invalidate(cls._meta.label)
95
-
96
- @classmethod
97
- def get_closure_model(cls):
98
- """Return ClosureModel for class."""
99
- return cls.closure_model
100
-
101
- def delete(self, cascade=True):
102
- """Delete node."""
103
- model = self._meta.model
104
- parent = self.get_parent()
105
-
106
- if not cascade:
107
- new_siblings_count = parent.get_siblings_count()
108
- # Get a list of children
109
- children = self.get_children()
110
- if children:
111
- # Move them to one level up
112
- for child in children:
113
- child.tn_parent = self.tn_parent
114
- child.tn_priority = new_siblings_count + child.tn_priority
115
- # Udate both models in bulk
116
- model.objects.bulk_update(
117
- children,
118
- ("tn_parent",),
119
- batch_size=1000
120
- )
121
-
122
- # All descendants and related records in the ClosingModel will be
123
- # cleared by cascading the removal of ForeignKeys.
124
- super().delete()
125
- # Can be excluded. The cache has already been cleared by the manager.
126
- model.clear_cache()
127
-
128
- # Update tn_priority
129
- if parent is None:
130
- siblings = model.get_roots()
131
- else:
132
- siblings = parent.get_children()
133
-
134
- if siblings:
135
- siblings = [node for node in siblings if node.pk != self.pk]
136
- sorted_siblings = sorted(siblings, key=lambda x: x.tn_priority)
137
- for index, node in enumerate(sorted_siblings):
138
- node.tn_priority = index
139
- model.objects.bulk_update(siblings, ['tn_priority'])
140
-
141
- def save(self, force_insert=False, *args, **kwargs):
142
- """Save a model instance with sync closure table."""
143
- model = self._meta.model
144
- # Send signal pre_save
145
- pre_save.send(
146
- sender=model,
147
- instance=self,
148
- raw=False,
149
- using=self._state.db,
150
- update_fields=kwargs.get("update_fields", None)
151
- )
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
-
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(include_self=False)
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
-
218
- @classmethod
219
- def _get_place(cls, target, position=0):
220
- """
221
- Get position relative to the target node.
222
-
223
- position – the position, relative to the target node, where the
224
- current node object will be moved to, can be one of:
225
-
226
- - first-root: the node will be the first root node;
227
- - last-root: the node will be the last root node;
228
- - sorted-root: the new node will be moved after sorting by
229
- the treenode_sort_field field;
230
-
231
- - first-sibling: the node will be the new leftmost sibling of the
232
- target node;
233
- - left-sibling: the node will take the target node’s place, which will
234
- be moved to the target position with shifting follows nodes;
235
- - right-sibling: the node will be moved to the position after the
236
- target node;
237
- - last-sibling: the node will be the new rightmost sibling of the
238
- target node;
239
- - sorted-sibling: the new node will be moved after sorting by
240
- the treenode_sort_field field;
241
-
242
- - first-child: the node will be the first child of the target node;
243
- - last-child: the node will be the new rightmost child of the target
244
- - sorted-child: the new node will be moved after sorting by
245
- the treenode_sort_field field.
246
-
247
- """
248
- if isinstance(position, int):
249
- priority = position
250
- elif not isinstance(position, str) or '-' not in position:
251
- raise ValueError(f"Invalid position format: {position}")
252
-
253
- part1, part2 = position.split('-')
254
- if part1 not in {'first', 'last', 'left', 'right', 'sorted'} or \
255
- part2 not in {'root', 'child', 'sibling'}:
256
- raise ValueError(f"Unknown position type: {position}")
257
-
258
- # Determine the parent depending on the type of position
259
- if part2 == 'root':
260
- parent = None
261
- elif part2 == 'sibling':
262
- parent = target.tn_parent
263
- elif part2 == 'child':
264
- parent = target
265
- else:
266
- parent = None
267
-
268
- if parent:
269
- count = parent.get_children_count()
270
- else:
271
- count = cls.get_roots_count()
272
-
273
- # Определяем позицию (приоритет)
274
- if part1 == 'first':
275
- priority = 0
276
- elif part1 == 'left':
277
- priority = target.tn_priority
278
- elif part1 == 'right':
279
- priority = target.tn_priority + 1
280
- elif part1 in {'last', 'sorted'}:
281
- priority = count
282
- else:
283
- priority = count
284
-
285
- return parent, priority
286
-
287
- @classmethod
288
- @cached_method
289
- def _sort_node_list(cls, nodes):
290
- """
291
- Sort list of nodes by materialized path oreder.
292
-
293
- Collect the materialized path without accessing the DB and perform
294
- sorting
295
- """
296
- # Create a list of tuples: (node, materialized_path)
297
- nodes_with_path = [(node, node.tn_order) for node in nodes]
298
- # Sort the list by the materialized path
299
- nodes_with_path.sort(key=lambda tup: tup[1])
300
- # Extract sorted nodes
301
- return [tup[0] for tup in nodes_with_path]
302
-
303
- @classmethod
304
- @cached_method
305
- def _get_sorting_map(self, model):
306
- """Return the sorting map of model objects."""
307
- # --1 Extracting data from the model
308
- qs_list = model.objects.values_list('pk', 'tn_parent', 'tn_priority')
309
- node_map = {pk: {"pk": pk, "parent": tn_parent, "priority": tn_priority}
310
- for pk, tn_parent, tn_priority in qs_list}
311
-
312
- def build_path(node_id):
313
- """Recursive path construction."""
314
- path = []
315
- while node_id:
316
- node = node_map.get(node_id)
317
- if not node:
318
- break
319
- path.append(node["priority"])
320
- node_id = node["parent"]
321
- return list(reversed(path))
322
-
323
- # -- 2. Collecting materialized paths
324
- paths = []
325
- for pk, node in node_map.items():
326
- path = build_path(pk)
327
- paths.append({"pk": pk, "path": path})
328
-
329
- # -- 3. Convert paths to strings
330
- for item in paths:
331
- pk_path = item["path"]
332
- segments = [to_base36(i).rjust(6, '0') for i in pk_path]
333
- item["path_str"] = "".join(segments)
334
-
335
- # -- 5. Sort by string representation of the path
336
- paths.sort(key=lambda x: x["path_str"])
337
- index_map = {i: item["pk"] for i, item in enumerate(paths)}
338
-
339
- return index_map
340
-
341
-
342
- # The end
@@ -1,27 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- Class Property Decorator
4
-
5
- This module provides a `classproperty` decorator that allows defining
6
- read-only class-level properties.
7
-
8
- Features:
9
- - Enables class-level properties similar to instance properties.
10
- - Uses a custom descriptor for property-like behavior.
11
-
12
- """
13
-
14
-
15
- class classproperty(object):
16
- """Classproperty class."""
17
-
18
- def __init__(self, getter):
19
- """Init."""
20
- self.getter = getter
21
-
22
- def __get__(self, instance, owner):
23
- """Get."""
24
- return self.getter(owner)
25
-
26
-
27
- # The end