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
@@ -2,17 +2,18 @@
2
2
  """
3
3
  TreeNode Tree Mixin
4
4
 
5
- Version: 2.1.0
5
+ Version: 3.0.0
6
6
  Author: Timur Kady
7
7
  Email: timurkady@yandex.com
8
8
  """
9
9
 
10
10
  import json
11
- from django.db import models, transaction
11
+ import inspect
12
+ from django.db import models, transaction, connection
12
13
  from collections import OrderedDict
13
14
  from django.core.serializers.json import DjangoJSONEncoder
14
15
 
15
- from ...cache import cached_method
16
+ from ...cache import treenode_cache as cache
16
17
 
17
18
 
18
19
  class TreeNodeTreeMixin(models.Model):
@@ -23,17 +24,41 @@ class TreeNodeTreeMixin(models.Model):
23
24
 
24
25
  abstract = True
25
26
 
26
- @classmethod
27
- def dump_tree(cls, instance=None):
27
+ def clone_subtree(self, parent=None):
28
28
  """
29
- Return an n-dimensional dictionary representing the model tree.
29
+ Clone self and entire subtree under given parent.
30
30
 
31
- Introduced for compatibility with other packages.
31
+ Returns new root of the cloned subtree.
32
32
  """
33
- return cls.get_tree(cls, instance)
33
+ model = self._meta.model
34
+
35
+ def _clone_node(node, parent):
36
+ # Copy all regular fields (including ForeignKey via attname)
37
+ data = {
38
+ f.attname: getattr(node, f.attname)
39
+ for f in node._meta.concrete_fields
40
+ if not f.primary_key and f.name != "_path"
41
+ }
42
+ data['parent'] = parent
43
+
44
+ # Create a new node
45
+ new_node = model.objects.create(**data)
46
+
47
+ # Copy ManyToMany fields
48
+ for m2m_field in node._meta.many_to_many:
49
+ related_ids = getattr(
50
+ node, m2m_field.name).values_list('pk', flat=True)
51
+ getattr(new_node, m2m_field.name).set(related_ids)
52
+
53
+ # Clone descendants recursively
54
+ for child in node.get_children():
55
+ _clone_node(child, new_node)
56
+
57
+ return new_node
58
+
59
+ return _clone_node(self, parent)
34
60
 
35
61
  @classmethod
36
- @cached_method
37
62
  def get_tree(cls, instance=None):
38
63
  """
39
64
  Return an n-dimensional dictionary representing the model tree.
@@ -44,39 +69,39 @@ class TreeNodeTreeMixin(models.Model):
44
69
  """
45
70
  # If instance is passed, we get all its descendants (including itself)
46
71
  if instance:
47
- queryset = instance.get_descendants_queryset(include_self=True)\
48
- .annotate(depth=models.Max("parents_set__depth"))
72
+ queryset = instance.get_descendants_queryset(include_self=True)
49
73
  else:
50
74
  # Load all records at once
51
- queryset = cls.objects.all()
75
+ queryset = cls.objects.get_queryset()
52
76
 
53
77
  # Dictionary for quick access to nodes by id and list for iteration
54
78
  nodes_by_id = {}
55
79
  nodes_list = []
56
80
 
57
81
  # Loop through all nodes using an iterator
58
- for node in queryset.iterator(chunk_size=1000):
82
+ for node in queryset.order_by("_path").iterator(chunk_size=1000):
59
83
  # Create a dictionary for the node.
60
- # In Python 3.7+, the standard dict preserves insertion order.
61
- # We'll stick to the order:
62
- # id, 'tn_parent', 'tn_priority', 'level', then the rest of
63
- # the fields. Сlose the dictionary with the fields 'level', 'path',
64
- # 'children'
65
84
  node_dict = OrderedDict()
66
85
  node_dict['id'] = node.id
67
- node_dict['tn_parent'] = node.tn_parent_id
68
- node_dict['tn_priority'] = node.tn_priority
69
- node_dict['level'] = node.get_depth()
70
- node_dict['path'] = node.get_breadcrumbs('tn_priority')
86
+ node_dict['parent'] = node.parent_id
87
+ node_dict['priority'] = node.priority
88
+ node_dict['depth'] = node.get_depth()
89
+ node_dict['path'] = node.get_order()
90
+ node_dict['children'] = [] # node.get_children_pks()
71
91
 
72
92
  # Add the rest of the model fields.
73
93
  # Iterate over all the fields obtained via _meta.get_fields()
74
- for field in node._meta.get_fields():
94
+ fields = [
95
+ f for f in node._meta.get_fields()
96
+ if f.concrete and not f.auto_created and
97
+ not f.name.startswith('_')
98
+ ]
99
+
100
+ for field in fields:
75
101
  # Skipping fields that are already added or not required
76
102
  # (e.g. tn_closure or virtual links)
77
103
  if field.name in [
78
- 'id', 'tn_parent', 'tn_priority', 'tn_closure',
79
- 'children']:
104
+ 'id', 'parent', 'priority', '_depth', 'children']:
80
105
  continue
81
106
 
82
107
  try:
@@ -91,14 +116,6 @@ class TreeNodeTreeMixin(models.Model):
91
116
 
92
117
  node_dict[field.name] = value
93
118
 
94
- # Adding a materialized path
95
- node_dict['path'] = None
96
- # Adding a nesting level
97
- node_dict['level'] = None
98
- # We initialize the list of children, which we will then fill
99
- # when assembling the tree
100
- node_dict['children'] = []
101
-
102
119
  # Save the node both in the list and in the dictionary by id
103
120
  # for quick access
104
121
  nodes_by_id[node.id] = node_dict
@@ -107,7 +124,7 @@ class TreeNodeTreeMixin(models.Model):
107
124
  # Build a tree: assign each node a list of its children
108
125
  tree = []
109
126
  for node_dict in nodes_list:
110
- parent_id = node_dict['tn_parent']
127
+ parent_id = node_dict['parent']
111
128
  # If there is a parent and it is present in nodes_by_id, then
112
129
  # add the current node to the list of its children
113
130
  if parent_id and parent_id in nodes_by_id:
@@ -122,7 +139,7 @@ class TreeNodeTreeMixin(models.Model):
122
139
  @classmethod
123
140
  def get_tree_json(cls, instance=None):
124
141
  """Represent the tree as a JSON-compatible string."""
125
- tree = cls.dump_tree(instance)
142
+ tree = cls.get_tree(instance)
126
143
  return DjangoJSONEncoder().encode(tree)
127
144
 
128
145
  @classmethod
@@ -130,86 +147,132 @@ class TreeNodeTreeMixin(models.Model):
130
147
  """
131
148
  Load a tree from a list of dictionaries.
132
149
 
133
- Loaded nodes are synchronized with the database: existing records
134
- are updated, new ones are created.
135
- Each dictionary must contain the 'id' key to identify the record.
150
+ - Ignores visual/path metadata ('path', 'depth', '_path', '_depth')
151
+ - Accepts 'id', 'name', 'priority', 'parent'
152
+ - Automatically recalculates _path, _depth, priority after loading
136
153
  """
137
-
138
- def flatten_tree(nodes, model_fields):
139
- """
140
- Recursively traverse the tree and generate lists of nodes.
141
-
142
- Each node in the list is a copy of the dictionary without
143
- the service keys 'children', 'level', 'path'.
144
- """
154
+ # Step 1: Flatten nested tree into list of nodes with temporary
155
+ # parent references
156
+ def flatten_tree(tree, parent_temp_id=None):
145
157
  flat = []
146
- for node in nodes:
147
- # Create a copy of the dictionary so as not to affect
148
- # the original tree
149
- node_copy = node.copy()
150
- children = node_copy.pop('children', [])
151
- # Remove temporary/service fields that are not related to
152
- # the model
153
- for key in list(node_copy.keys() - model_fields):
154
- del node_copy[key]
155
- flat.append(node_copy)
156
- # Recursively add all children
157
- flat.extend(flatten_tree(children, model_fields))
158
+ for node in tree:
159
+ node = node.copy()
160
+ children = node.pop('children', [])
161
+ node_id = node.pop('id', None)
162
+ node.pop('path', None)
163
+ node.pop('depth', None)
164
+ node.pop('parent', None)
165
+ flat_node = {
166
+ **node,
167
+ '_id': int(node_id) if node_id is not None else None,
168
+ '_parent': int(parent_temp_id) if parent_temp_id is not None else None # noqa: D501
169
+ }
170
+ flat.append(flat_node)
171
+ flat.extend(flatten_tree(children, parent_temp_id=node_id))
158
172
  return flat
159
173
 
160
- # Get a flat list of nodes (from root to leaf)
161
- model_fields = [field.name for field in cls._meta.get_fields()]
162
- flat_nodes = flatten_tree(tree_data, model_fields)
163
-
164
- # Load all ids for a given model from the database to minimize
165
- # the number of database requests
166
- existing_ids = set(cls.objects.values_list('id', flat=True))
167
-
168
- # Lists for nodes to update and create
169
- nodes_to_update = []
170
- nodes_to_create = []
171
-
172
- # Determine which model fields should be updated (excluding 'id')
173
- # This assumes that all model fields are used in serialization
174
- # (excluding service ones, like children)
175
- field_names = model_fields.remove('id')
176
-
177
- # Iterate over each node from the flat list
178
- for node_data in flat_nodes:
179
- # Collect data for creation/update.
180
- # There is already an 'id' in node_data, so we can distinguish
181
- # an existing entry from a new one.
182
- data = {k: v for k, v in node_data.items()
183
- if k in field_names or k == 'id'}
184
-
185
- # Handle the foreign key tn_parent.
186
- # If the value is None, then there is no parent, otherwise
187
- # the parent id is expected.
188
- # Additional checks can be added here if needed.
189
- if 'tn_parent' in data and data['tn_parent'] is None:
190
- data['tn_parent'] = None
191
-
192
- # Если id уже есть в базе, то будем обновлять запись,
193
- # иначе создаем новую.
194
- if data['id'] in existing_ids:
195
- nodes_to_update.append(cls(**data))
196
- else:
197
- nodes_to_create.append(cls(**data))
198
-
199
- # Perform operations in a transaction to ensure data integrity.
200
- with transaction.atomic():
201
- # bulk_create – creating new nodes
202
- if nodes_to_create:
203
- cls.objects.bulk_create(nodes_to_create, batch_size=1000)
204
- # bulk_update updating existing nodes.
205
- # When bulk_update, we must specify a list of fields to update.
206
- if nodes_to_update:
174
+ raw_data = flatten_tree(tree_data)
175
+
176
+ # Step 2: Identify which objects should be updated
177
+ pks = [r["_id"] for r in raw_data if r["_id"] is not None]
178
+ existing_ids = set(cls.objects.filter(
179
+ pk__in=pks).values_list('id', flat=True))
180
+ to_update_ids = {r["_id"] for r in raw_data if r["_id"] in existing_ids}
181
+
182
+ existing_objs = {
183
+ obj.pk: obj for obj in cls.objects.filter(pk__in=to_update_ids)}
184
+
185
+ # Step 3: Determine tree levels for correct creation order
186
+ levels = {}
187
+ for record in raw_data:
188
+ level = 0
189
+ p = record.get('_parent')
190
+ while p:
191
+ level += 1
192
+ p = next((r['_parent']
193
+ for r in raw_data if r['_id'] == p), None)
194
+ levels.setdefault(level, []).append(record)
195
+
196
+ records_by_level = [
197
+ sorted(levels[level], key=lambda x: x.get('_parent') or -1)
198
+ for level in sorted(levels)
199
+ ]
200
+
201
+ # Step 4: Map old temporary IDs to actual DB IDs
202
+ created_objects = []
203
+ updated_objects = []
204
+ # ID mapping for existing objects
205
+ id_map = {pk: pk for pk in to_update_ids}
206
+
207
+ model_fields = {
208
+ f.name for f in cls._meta.get_fields()
209
+ if f.concrete and not f.auto_created and not f.name.startswith('_')
210
+ }
211
+
212
+ # Step 5: Process records level by level
213
+ for level_records in records_by_level:
214
+ objs_to_create = []
215
+ objs_to_update = []
216
+ fields_to_update_set = set()
217
+
218
+ for record in level_records:
219
+ temp_id = record['_id']
220
+ temp_parent = record.get('_parent')
221
+ data = {k: v for k, v in record.items() if not k.startswith(
222
+ '_') and k in model_fields}
223
+
224
+ if temp_parent and temp_parent in id_map:
225
+ data['parent_id'] = id_map[temp_parent]
226
+
227
+ if temp_id in to_update_ids:
228
+ obj = existing_objs[temp_id]
229
+ for field, value in data.items():
230
+ setattr(obj, field, value)
231
+ setattr(obj, '_temp_parent', temp_parent)
232
+ objs_to_update.append(obj)
233
+ fields_to_update_set.update(data.keys())
234
+ else:
235
+ obj = cls(**data)
236
+ obj.full_clean()
237
+ setattr(obj, '_temp_id', temp_id)
238
+ setattr(obj, '_temp_parent', temp_parent)
239
+ objs_to_create.append(obj)
240
+
241
+ # Step 6: Bulk create and track real IDs
242
+ if objs_to_create:
243
+ created = cls.objects.bulk_create(objs_to_create)
244
+ for obj in created:
245
+ temp_id = getattr(obj, '_temp_id')
246
+ id_map[temp_id] = obj.id
247
+ delattr(obj, '_temp_id')
248
+ created_objects.append(obj)
249
+
250
+ # Step 7: Bulk update existing objects
251
+ if objs_to_update:
252
+ for obj in objs_to_update:
253
+ obj.full_clean()
207
254
  cls.objects.bulk_update(
208
- nodes_to_update,
209
- fields=field_names,
210
- batch_size=1000
211
- )
212
- cls.clear_cache()
255
+ objs_to_update, list(fields_to_update_set))
256
+ updated_objects.extend(objs_to_update)
257
+
258
+ all_objects = created_objects + updated_objects
259
+
260
+ # Step 8: Finalize parent assignments using ID map
261
+ for obj in all_objects:
262
+ temp_parent = getattr(obj, '_temp_parent', None)
263
+ if temp_parent is not None and temp_parent in id_map:
264
+ obj.parent_id = id_map[temp_parent]
265
+ for attr in ['_temp_id', '_temp_parent']:
266
+ if hasattr(obj, attr):
267
+ delattr(obj, attr)
268
+
269
+ cls.objects.bulk_update(all_objects, ['parent_id'])
270
+
271
+ # Step 9: Rebuild tree structure (path, depth, priority)
272
+ cls.tasks.run()
273
+
274
+ # Step 10: Invalidate tree-related caches
275
+ cache.invalidate(cls._meta.label)
213
276
 
214
277
  @classmethod
215
278
  def load_tree_json(cls, json_str):
@@ -222,36 +285,9 @@ class TreeNodeTreeMixin(models.Model):
222
285
  tree_data = json.loads(json_str)
223
286
  except json.JSONDecodeError as e:
224
287
  raise ValueError(f"Error decoding JSON: {e}")
225
-
226
- cls.load_tree(tree_data)
227
-
228
- @classmethod
229
- @cached_method
230
- def get_tree_display(cls, instance=None, symbol="—"):
231
- """Get a multiline string representing the model tree."""
232
- # If instance is passed, we get all its descendants (including itself)
233
- if instance:
234
- queryset = instance.get_descendants_queryset(include_self=True)\
235
- .prefetch_related("tn_children")\
236
- .annotate(depth=models.Max("parents_set__depth"))\
237
- .order_by("depth", "tn_parent", "tn_priority")
238
- else:
239
- queryset = cls.objects.all()\
240
- .prefetch_related("tn_children")\
241
- .annotate(depth=models.Max("parents_set__depth"))\
242
- .order_by("depth", "tn_parent", "tn_priority")
243
- # Convert queryset to list for indexed access
244
- nodes = list(queryset)
245
- sorted_nodes = cls._sort_node_list(nodes)
246
- result = []
247
- for node in sorted_nodes:
248
- # Insert an indent proportional to the depth of the node
249
- indent = symbol * node.depth
250
- result.append(indent + str(node))
251
- return result
288
+ return tree_data
252
289
 
253
290
  @classmethod
254
- @cached_method
255
291
  def get_tree_annotated(cls):
256
292
  """
257
293
  Get an annotated list from a tree branch.
@@ -285,10 +321,7 @@ class TreeNodeTreeMixin(models.Model):
285
321
 
286
322
  """
287
323
  # Load the tree with the required preloads and depth annotation.
288
- queryset = cls.objects.all()\
289
- .prefetch_related("tn_children")\
290
- .annotate(depth=models.Max("parents_set__depth"))\
291
- .order_by("depth", "tn_parent", "tn_priority")
324
+ queryset = cls.objects.all()
292
325
  # Convert queryset to list for indexed access
293
326
  nodes = list(queryset)
294
327
  total_nodes = len(nodes)
@@ -301,7 +334,7 @@ class TreeNodeTreeMixin(models.Model):
301
334
  value = str(node)
302
335
  # Determine if there are descendants (use prefetch_related to avoid
303
336
  # additional queries)
304
- value_open = len(node.tn_children.all()) > 0
337
+ value_open = len(node.children.all()) > 0
305
338
  level = node.depth
306
339
 
307
340
  # Calculate the "close" field
@@ -326,19 +359,45 @@ class TreeNodeTreeMixin(models.Model):
326
359
  return result
327
360
 
328
361
  @classmethod
329
- @transaction.atomic
330
362
  def update_tree(cls):
331
- """Rebuilds the closure table."""
332
- # Clear cache
333
- cls.clear_cache()
334
- cls.closure_model.delete_all()
335
- objs = list(cls.objects.all())
336
- cls.closure_model.objects.bulk_create(objs, batch_size=1000)
363
+ """Rebuld the tree."""
364
+ cls.tasks.add("update", None)
365
+ cls.tasks.run()
366
+
367
+ def delete_tree(self, include_self=True):
368
+ """
369
+ Delete current node and all descendants.
370
+
371
+ If include_self=False, only descendants will be deleted.
372
+ """
373
+ table = self._meta.db_table
374
+ path = self._path
375
+ like_pattern = f"{path}.%"
376
+
377
+ if include_self:
378
+ sql = f"""
379
+ DELETE FROM {table}
380
+ WHERE _path = %s OR _path LIKE %s
381
+ """
382
+ params = [path, like_pattern]
383
+ else:
384
+ sql = f"""
385
+ DELETE FROM {table}
386
+ WHERE _path LIKE %s
387
+ """
388
+ params = [like_pattern]
389
+
390
+ with connection.cursor() as cursor:
391
+ cursor.execute(sql, params)
392
+ self.clear_cache()
337
393
 
338
394
  @classmethod
339
- def delete_tree(cls):
395
+ def delete_forest(cls):
340
396
  """Delete the whole tree for the current node class."""
341
- cls.clear_cache()
342
- cls.objects.all().delete()
397
+ # cls.objects.all()._raw_delete(cls._base_manager.db)
398
+ table = cls._meta.db_table
399
+ with connection.cursor() as cursor:
400
+ cursor.execute(f"DELETE FROM {table}")
401
+ cache.invalidate(cls._meta.label)
343
402
 
344
403
  # The end
@@ -0,0 +1,154 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ TreeNode Raw SQL Mixin
4
+
5
+ Version: 3.0.0
6
+ Author: Timur Kady
7
+ Email: timurkady@yandex.com
8
+ """
9
+
10
+ from django.db import models, connection
11
+
12
+ from ...settings import SEGMENT_LENGTH, BASE
13
+ from ...utils.db.sqlcompat import SQLCompat
14
+
15
+
16
+ class RawSQLMixin(models.Model):
17
+ """Raw SQL Mixin."""
18
+
19
+ class Meta:
20
+ """Meta Class."""
21
+
22
+ abstract = True
23
+
24
+ def refresh(self):
25
+ """Refresh key fields from DB."""
26
+ task_query = self._meta.model.tasks
27
+ if len(task_query.queue) > 0:
28
+ task_query.run()
29
+
30
+ table = self._meta.db_table
31
+ sql = f"SELECT priority, _path, _depth FROM {table} WHERE id = %s"
32
+ with connection.cursor() as cursor:
33
+ cursor.execute(sql, [self.pk])
34
+ row = cursor.fetchone()
35
+ self.priority, self._path, self._depth = row
36
+
37
+ self._parent_id = self.parent_pk
38
+ self._priority = self.priority
39
+
40
+ def _shift_siblings_forward(self):
41
+ """
42
+ Shift the priority of all siblings starting from self.priority.
43
+
44
+ Uses direct SQL for maximum speed.
45
+ """
46
+ if (self.priority is None) or (self.priority >= BASE - 1):
47
+ return
48
+
49
+ db_table = self._meta.db_table
50
+
51
+ if self.parent_id is None:
52
+ where_clause = "parent_id IS NULL"
53
+ params = [self.priority]
54
+ else:
55
+ where_clause = "parent_id = %s"
56
+ params = [self.parent_id, self.priority]
57
+
58
+ sql = f"""
59
+ UPDATE {db_table}
60
+ SET priority = priority + 1
61
+ WHERE {where_clause} AND priority >= %s
62
+ """
63
+
64
+ with connection.cursor() as cursor:
65
+ cursor.execute(sql, params)
66
+
67
+ def _update_path(self, parent_id):
68
+ """
69
+ Rebuild subtree starting from parent_id.
70
+
71
+ If parent_id=None, then the whole tree is rebuilt.
72
+ Only fields are used: parent_id and id. All others (priority, _path,
73
+ _depth) are recalculated.
74
+ """
75
+ db_table = self._meta.db_table
76
+
77
+ sorting_field = self.sorting_field
78
+ sorting_fields = ["priority", "id"] if sorting_field == "priority" else [sorting_field] # noqa: D501
79
+ sort_expr = ", ".join([
80
+ field if "." in field else f"c.{field}"
81
+ for field in sorting_fields
82
+ ])
83
+
84
+ cte_header = "(id, parent_id, new_priority, new_path, new_depth)"
85
+
86
+ row_number_expr = "ROW_NUMBER() OVER (ORDER BY {sort_expr}) - 1"
87
+ hex_expr = SQLCompat.to_hex(row_number_expr)
88
+ lpad_expr = SQLCompat.lpad(hex_expr, SEGMENT_LENGTH, "'0'")
89
+
90
+ if parent_id is None:
91
+ new_path_expr = lpad_expr
92
+ base_sql = f"""
93
+ SELECT
94
+ c.id,
95
+ c.parent_id,
96
+ {row_number_expr} AS new_priority,
97
+ {new_path_expr} AS new_path,
98
+ 0 AS new_depth
99
+ FROM {db_table} AS c
100
+ WHERE c.parent_id IS NULL
101
+ """
102
+ params = []
103
+ else:
104
+ path_expr = SQLCompat.concat("p._path", "'.'", lpad_expr)
105
+ base_sql = f"""
106
+ SELECT
107
+ c.id,
108
+ c.parent_id,
109
+ {row_number_expr} AS new_priority,
110
+ {path_expr} AS new_path,
111
+ p._depth + 1 AS new_depth
112
+ FROM {db_table} c
113
+ JOIN {db_table} p ON c.parent_id = p.id
114
+ WHERE p.id = %s
115
+ """
116
+ params = [parent_id]
117
+
118
+ recursive_row_number_expr = "ROW_NUMBER() OVER (PARTITION BY c.parent_id ORDER BY {sort_expr}) - 1" # noqa: D501
119
+ recursive_hex_expr = SQLCompat.to_hex(recursive_row_number_expr)
120
+ recursive_lpad_expr = SQLCompat.lpad(
121
+ recursive_hex_expr, SEGMENT_LENGTH, "'0'")
122
+ recursive_path_expr = SQLCompat.concat(
123
+ "t.new_path", "'.'", recursive_lpad_expr)
124
+
125
+ recursive_sql = f"""
126
+ SELECT
127
+ c.id,
128
+ c.parent_id,
129
+ {recursive_row_number_expr} AS new_priority,
130
+ {recursive_path_expr} AS new_path,
131
+ t.new_depth + 1 AS new_depth
132
+ FROM {db_table} c
133
+ JOIN tree_cte t ON c.parent_id = t.id
134
+ """
135
+
136
+ final_sql = f"""
137
+ WITH RECURSIVE tree_cte {cte_header} AS (
138
+ {base_sql}
139
+ UNION ALL
140
+ {recursive_sql}
141
+ )
142
+ UPDATE {db_table} AS orig
143
+ SET
144
+ priority = t.new_priority,
145
+ _path = t.new_path,
146
+ _depth = t.new_depth
147
+ FROM tree_cte t
148
+ WHERE orig.id = t.id;
149
+ """
150
+
151
+ self.sqlq.append((final_sql.format(sort_expr=sort_expr), params))
152
+
153
+
154
+ # The End