django-fast-treenode 2.0.10__py3-none-any.whl → 2.0.11__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.
treenode/managers.py CHANGED
@@ -11,13 +11,15 @@ Features:
11
11
  - `TreeNodeQuerySet` and `TreeNodeModelManager` for adjacency model operations.
12
12
  - Optimized `bulk_create` and `bulk_update` methods with atomic transactions.
13
13
 
14
- Version: 2.0.0
14
+ Version: 2.0.11
15
15
  Author: Timur Kady
16
16
  Email: timurkady@yandex.com
17
17
  """
18
18
 
19
-
19
+ from collections import deque, defaultdict
20
20
  from django.db import models, transaction
21
+ from django.db.models import F
22
+ from django.db import connection
21
23
 
22
24
 
23
25
  # ----------------------------------------------------------------------------
@@ -28,173 +30,237 @@ from django.db import models, transaction
28
30
  class ClosureQuerySet(models.QuerySet):
29
31
  """QuerySet для ClosureModel."""
30
32
 
31
- @transaction.atomic
32
- def bulk_create(self, objs, batch_size=1000):
33
+ def sort_nodes(self, node_list):
33
34
  """
34
- Insert new nodes in bulk.
35
+ Sort nodes topologically.
35
36
 
36
- For newly created AdjacencyModel objects:
37
- 1. Create self-referential records (parent=child, depth=0).
38
- 2. Build ancestors for each node based on tn_parent.
37
+ Возвращает список узлов, отсортированных от корней к листьям.
38
+ Узел считается корневым, если его tn_parent равен None или его
39
+ родитель отсутствует в node_list.
39
40
  """
40
- # --- 1. Create self-referential closure records for each object.
41
- self_closure_records = []
42
- for item in objs:
43
- self_closure_records.append(
44
- self.model(parent=item, child=item, depth=0)
45
- )
46
- super().bulk_create(self_closure_records, batch_size=batch_size)
41
+ visited = set() # будем хранить id уже обработанных узлов
42
+ result = []
43
+ # Множество id узлов, входящих в исходный список
44
+ node_ids = {node.id for node in node_list}
45
+
46
+ def dfs(node):
47
+ if node.id in visited:
48
+ return
49
+ # Если родитель есть и он входит в node_list – обрабатываем его
50
+ # первым
51
+ if node.tn_parent and node.tn_parent_id in node_ids:
52
+ dfs(node.tn_parent)
53
+ visited.add(node.id)
54
+ result.append(node)
55
+
56
+ for n in node_list:
57
+ dfs(n)
47
58
 
48
- # --- 2. Preparing closure for parents
49
- parent_ids = {node.tn_parent.pk for node in objs if node.tn_parent}
50
- parent_closures = list(
51
- self.filter(child__in=parent_ids)
52
- .values('child', 'parent', 'depth')
53
- )
54
- # Pack them into a dict
55
- parent_closures_dict = {}
56
- for pc in parent_closures:
57
- parent_id = pc['child']
58
- parent_closures_dict.setdefault(parent_id, []).append(pc)
59
-
60
- # --- 3. Get closure records for the objs themselves
61
- node_ids = [node.pk for node in objs]
62
- child_closures = list(
63
- self.filter(parent__in=node_ids)
64
- .values('parent', 'child', 'depth')
65
- )
66
- child_closures_dict = {}
67
- for cc in child_closures:
68
- parent_id = cc['parent']
69
- child_closures_dict.setdefault(parent_id, []).append(cc)
70
-
71
- # --- 4. Collecting new links
72
- new_records = []
73
- for node in objs:
74
- if not node.tn_parent:
75
- continue
76
- # parent closure
77
- parents = parent_closures_dict.get(node.tn_parent.pk, [])
78
- # closure of descendants
79
- # (often this is only the node itself, depth=0, but there can be
80
- # nested ones)
81
- children = child_closures_dict.get(node.pk, [])
82
- # Combine parents x children
83
- for p in parents:
84
- for c in children:
85
- new_records.append(self.model(
86
- parent_id=p['parent'],
87
- child_id=c['child'],
88
- depth=p['depth'] + c['depth'] + 1
89
- ))
90
- # --- 5. bulk_create
59
+ return result
60
+
61
+ @transaction.atomic
62
+ def bulk_create(self, objs, batch_size=1000, *args, **kwargs):
63
+ """Insert new nodes in bulk."""
91
64
  result = []
92
- if new_records:
93
- result = super().bulk_create(new_records, batch_size=batch_size)
65
+
66
+ # 1. Топологическая сортировка узлов
67
+ objs = self.sort_nodes(objs)
68
+
69
+ # 1. Создаем self-ссылки для всех узлов: (node, node, 0).
70
+ self_links = [
71
+ self.model(parent=obj, child=obj, depth=0)
72
+ for obj in objs
73
+ ]
74
+ result.extend(
75
+ super().bulk_create(self_links, batch_size, *args, **kwargs)
76
+ )
77
+
78
+ # 2. Формируем отображение: id родителя -> список его детей.
79
+ children_map = defaultdict(list)
80
+ for obj in objs:
81
+ if obj.tn_parent_id:
82
+ children_map[obj.tn_parent_id].append(obj)
83
+
84
+ # 3. Пробуем определить корневые узлы (с tn_parent == None).
85
+ root_nodes = [obj for obj in objs if obj.tn_parent is None]
86
+
87
+ # Если корневых узлов нет, значит вставляем поддерево.
88
+ if not root_nodes:
89
+ # Определяем "верхние" узлы поддерева:
90
+ # те, чей родитель не входит в список вставляемых объектов.
91
+ objs_ids = {obj.id for obj in objs if obj.id is not None}
92
+ top_nodes = [
93
+ obj for obj in objs if obj.tn_parent_id not in objs_ids
94
+ ]
95
+
96
+ # Для каждого такого узла, если родитель существует, получаем
97
+ # записи замыкания для родителя и добавляем новые записи для
98
+ # (ancestor -> node) с depth = ancestor.depth + 1.
99
+ new_entries = []
100
+ for node in top_nodes:
101
+ if node.tn_parent_id:
102
+ parent_closures = self.model.objects.filter(
103
+ child_id=node.tn_parent_id
104
+ )
105
+ for ancestor in parent_closures:
106
+ new_entries.append(
107
+ self.model(
108
+ parent=ancestor.parent,
109
+ child=node,
110
+ depth=ancestor.depth + 1
111
+ )
112
+ )
113
+ if new_entries:
114
+ result.extend(
115
+ super().bulk_create(
116
+ new_entries, batch_size, *args, **kwargs
117
+ )
118
+ )
119
+
120
+ # Устанавливаем узлы верхнего уровня поддерева как начальные
121
+ # для обхода.
122
+ current_nodes = top_nodes
123
+ else:
124
+ current_nodes = root_nodes
125
+
126
+ # 4. Рекурсивная функция для обхода уровней.
127
+ def process_level(current_nodes):
128
+ next_level = []
129
+ new_entries = []
130
+ for node in current_nodes:
131
+ # Для текущего узла получаем все записи замыкания (его предков).
132
+ ancestors = self.model.objects.filter(child=node)
133
+ for child in children_map.get(node.id, []):
134
+ for ancestor in ancestors:
135
+ new_entries.append(
136
+ self.model(
137
+ parent=ancestor.parent,
138
+ child=child,
139
+ depth=ancestor.depth + 1
140
+ )
141
+ )
142
+ next_level.append(child)
143
+ if new_entries:
144
+ result.extend(
145
+ super().bulk_create(
146
+ new_entries, batch_size, *args, **kwargs
147
+ )
148
+ )
149
+ if next_level:
150
+ process_level(next_level)
151
+
152
+ process_level(current_nodes)
153
+
94
154
  self.model.clear_cache()
95
155
  return result
96
156
 
97
157
  @transaction.atomic
98
158
  def bulk_update(self, objs, fields=None, batch_size=1000):
99
159
  """
100
- Update the records in the closure table for the list of updated nodes.
101
-
102
- For each node whose tn_parent has changed, the closure records
103
- for its entire subtree are recalculated:
104
- 1. The ancestor chain of the new parent is selected.
105
- 2. The subtree (with closure records) of the updated node is selected.
106
- 3. For each combination (ancestor, descendant), a new depth is
107
- calculated.
108
- 4. Old "dangling" records (those where the descendant has a link to
109
- a non-subtree) are removed.
110
- 5. New records are inserted using the bulk_create method.
160
+ Обновляет таблицу замыкания для объектов, у которых изменился tn_parent.
161
+
162
+ Предполагается, что все объекты из списка objs уже есть в таблице
163
+ замыкания, но их связи (как для родителей, так и для детей) могли
164
+ измениться.
165
+
166
+ Алгоритм:
167
+ 1. Формируем отображение: id родителя → список его детей.
168
+ 2. Определяем корневые узлы обновляемого поддерева:
169
+ Узел считается корневым, если его tn_parent равен None или его
170
+ родитель не входит в objs.
171
+ 3. Для каждого корневого узла, если есть внешний родитель, получаем его
172
+ замыкание из базы.
173
+ Затем формируем записи замыкания для узла (все внешние связи с
174
+ увеличенным depth и self-ссылка).
175
+ 4. С помощью BFS обходим поддерево: для каждого узла для каждого его
176
+ ребёнка создаём записи, используя родительские записи (увеличенные
177
+ на 1) и добавляем self-ссылку для ребёнка.
178
+ 5. Удаляем старые записи замыкания для объектов из objs и сохраняем
179
+ новые пакетно.
111
180
  """
112
- if not objs:
113
- return
114
-
115
- # --- 1. We obtain chains of ancestors for new parents.
116
- parent_ids = {node.tn_parent.pk for node in objs if node.tn_parent}
117
- parent_closures = list(
118
- self.filter(child__in=parent_ids).values(
119
- 'child',
120
- 'parent',
121
- 'depth'
122
- )
123
- )
124
- # We collect in a dictionary: key is the parent ID (tn_parent),
125
- # value is the list of records.
126
- parent_closures_dict = {}
127
- for pc in parent_closures:
128
- parent_closures_dict.setdefault(pc['child'], []).append(pc)
129
-
130
- # --- 2. Obtain closing records for the subtrees of the
131
- # nodes being updated.
132
- updated_ids = [node.pk for node in objs]
133
- subtree_closures = list(self.filter(parent__in=updated_ids).values(
134
- 'parent',
135
- 'child',
136
- 'depth'
137
- ))
138
- # Group by ID of the node being updated (i.e. by parent in the
139
- # closing record)
140
- subtree_closures_dict = {}
141
- for sc in subtree_closures:
142
- subtree_closures_dict.setdefault(sc['parent'], []).append(sc)
143
-
144
- # --- 3. Construct new close records for each updated node with
145
- # a new parent.
146
- new_records = []
147
- for node in objs:
148
- # If the node has become root (tn_parent=None), then there are
149
- # no additional connections with ancestors.
150
- if not node.tn_parent:
151
- continue
152
- # From the closing chain of the new parent we get a list of its
153
- # ancestors
154
- p_closures = parent_closures_dict.get(node.tn_parent.pk, [])
155
- # From the node subtree we get the closing records
156
- s_closures = subtree_closures_dict.get(node.pk, [])
157
- # If for some reason the subtree entries are missing, we will
158
- # ensure that there is a custom entry.
159
- if not s_closures:
160
- s_closures = [{
161
- 'parent': node.pk,
162
- 'child': node.pk,
163
- 'depth': 0
164
- }]
165
- # Combine: for each ancestor of the new parent and for each
166
- # descendant from the subtree
167
- for p in p_closures:
168
- for s in s_closures:
169
- new_depth = p['depth'] + s['depth'] + 1
170
- new_records.append(
171
- self.model(
172
- parent_id=p['parent'],
173
- child_id=s['child'],
174
- depth=new_depth
175
- )
181
+ # 1. Топологическая сортировка узлов
182
+ objs = self.sort_nodes(objs)
183
+
184
+ # 2. Построим отображение: id родителя список детей
185
+ children_map = defaultdict(list)
186
+ for obj in objs:
187
+ if obj.tn_parent_id:
188
+ children_map[obj.tn_parent_id].append(obj)
189
+
190
+ # Множество id обновляемых объектов
191
+ objs_ids = {obj.id for obj in objs}
192
+
193
+ # 3. Определяем корневые узлы обновляемого поддерева:
194
+ # Узел считается корневым, если его tn_parent либо равен None, либо
195
+ # его родитель не входит в objs.
196
+ roots = [
197
+ obj for obj in objs
198
+ if (obj.tn_parent is None) or (obj.tn_parent_id not in objs_ids)
199
+ ]
200
+
201
+ # Список для накопления новых записей замыкания
202
+ new_closure_entries = []
203
+
204
+ # Очередь для BFS: каждый элемент — кортеж (node, node_closure),
205
+ # где node_closure — список записей замыкания для этого узла.
206
+ queue = deque()
207
+ for node in roots:
208
+ if node.tn_parent_id:
209
+ # Получаем замыкание внешнего родителя из базы
210
+ external_ancestors = list(
211
+ self.model.objects.filter(child_id=node.tn_parent_id)
212
+ .values('parent_id', 'depth')
213
+ )
214
+ # Для каждого найденного предка создаём запись для node с
215
+ # depth+1
216
+ node_closure = [
217
+ self.model(
218
+ parent_id=entry['parent_id'],
219
+ child=node,
220
+ depth=entry['depth'] + 1
221
+ )
222
+ for entry in external_ancestors
223
+ ]
224
+ else:
225
+ node_closure = []
226
+ # Добавляем self-ссылку (node node, depth 0)
227
+ node_closure.append(self.model(parent=node, child=node, depth=0))
228
+
229
+ # Сохраняем записи для текущего узла и кладем в очередь для
230
+ # обработки его поддерева
231
+ new_closure_entries.extend(node_closure)
232
+ queue.append((node, node_closure))
233
+
234
+ # 4. BFS-обход поддерева: для каждого узла создаём замыкание для его
235
+ # детей
236
+ while queue:
237
+ parent_node, parent_closure = queue.popleft()
238
+ for child in children_map.get(parent_node.id, []):
239
+ # Для ребенка новые записи замыкания:
240
+ # для каждого записи родителя создаем (ancestor -> child)
241
+ # с depth+1
242
+ child_closure = [
243
+ self.model(
244
+ parent_id=entry.parent_id,
245
+ child=child,
246
+ depth=entry.depth + 1
176
247
  )
248
+ for entry in parent_closure
249
+ ]
250
+ # Добавляем self-ссылку для ребенка
251
+ child_closure.append(
252
+ self.model(parent=child, child=child, depth=0)
253
+ )
177
254
 
178
- # --- 4. Remove old closing records so that there are no "tails".
179
- # For each updated node, calculate a subset of IDs related to its
180
- # subtree
181
- for node in objs:
182
- subtree_ids = set()
183
- # Be sure to include the node itself (its self-link should
184
- # already be there)
185
- subtree_ids.add(node.pk)
186
- for sc in subtree_closures_dict.get(node.pk, []):
187
- subtree_ids.add(sc['child'])
188
- # Remove records where the child belongs to the subtree, but the
189
- # parent is not included in it.
190
- self.filter(child_id__in=subtree_ids).exclude(
191
- parent_id__in=subtree_ids).delete()
192
-
193
- # --- 5. Insert new closing records in bulk.
194
- if new_records:
195
- super().bulk_create(new_records, batch_size=batch_size)
255
+ new_closure_entries.extend(child_closure)
256
+ queue.append((child, child_closure))
257
+
258
+ # 5. Удаляем старые записи замыкания для обновляемых объектов
259
+ self.model.objects.filter(child_id__in=objs_ids).delete()
260
+
261
+ # 6. Сохраняем новые записи пакетно
262
+ super().bulk_create(new_closure_entries)
196
263
  self.model.clear_cache()
197
- return new_records
198
264
 
199
265
 
200
266
  class ClosureModelManager(models.Manager):
@@ -230,30 +296,38 @@ class TreeNodeQuerySet(models.QuerySet):
230
296
  super().__init__(model, query, using, hints)
231
297
 
232
298
  @transaction.atomic
233
- def bulk_create(self, objs, batch_size=1000, ignore_conflicts=False):
299
+ def bulk_create(self, objs, batch_size=1000, *args, **kwargs):
234
300
  """
235
301
  Bulk create.
236
302
 
237
303
  Method of bulk creation objects with updating and processing of
238
304
  the Closuse Model.
239
305
  """
240
- # Regular bulk_create for TreeNodeModel
241
- objs = super().bulk_create(objs, batch_size, ignore_conflicts)
242
- # Call ClosureModel to insert closure records
243
- self.closure_model.objects.bulk_create(objs, batch_size=batch_size)
244
- # Возвращаем результат
306
+ # 1. Массовая вставка узлов в Модели Смежности
307
+ objs = super().bulk_create(objs, batch_size, *args, **kwargs)
308
+
309
+ # 2. Синхронизация Модели Закрытия
310
+ self.closure_model.objects.bulk_create(objs)
311
+
312
+ # 3. Очиска кэша и возрат результата
245
313
  self.model.clear_cache()
246
- return self.filter(pk__in=[obj.pk for obj in objs])
314
+ return objs
247
315
 
248
316
  @transaction.atomic
249
317
  def bulk_update(self, objs, fields, batch_size=1000, **kwargs):
250
- """."""
251
- closure_model = self.model.closure_model
318
+ """Bulk update."""
319
+ # 1. Выполняем обновление Модели Смежности
320
+ result = super().bulk_update(objs, fields, batch_size, **kwargs)
321
+
322
+ # 2. Синхронизируем данные в Модели Закрытия
252
323
  if 'tn_parent' in fields:
253
324
  # Попросим ClosureModel обработать move
254
- closure_model.objects.bulk_update(objs)
255
- result = super().bulk_update(objs, fields, batch_size, **kwargs)
256
- closure_model.clear_cache()
325
+ self.closure_model.objects.bulk_update(
326
+ objs, ["tn_parent",], batch_size
327
+ )
328
+
329
+ # 3. Очиска кэша и возрат результата
330
+ self.model.clear_cache()
257
331
  return result
258
332
 
259
333
 
@@ -269,13 +343,77 @@ class TreeNodeModelManager(models.Manager):
269
343
  custom QuerySet.
270
344
  """
271
345
  self.model.clear_cache()
272
- return self.get_queryset().bulk_create(
346
+ result = self.get_queryset().bulk_create(
273
347
  objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts
274
348
  )
349
+ transaction.on_commit(lambda: self.update_auto_increment())
350
+ return result
351
+
352
+ def bulk_update(self, objs, fields=None, batch_size=1000):
353
+ """Bulk Update."""
354
+ self.model.clear_cache()
355
+ result = self.get_queryset().bulk_update(objs, fields, batch_size)
356
+ return result
275
357
 
276
358
  def get_queryset(self):
277
359
  """Return a QuerySet that sorts by 'tn_parent' and 'tn_priority'."""
278
360
  queryset = TreeNodeQuerySet(self.model, using=self._db)
279
- return queryset.order_by('tn_parent', 'tn_priority')
361
+ return queryset.order_by(
362
+ F('tn_parent').asc(nulls_first=True),
363
+ 'tn_parent',
364
+ 'tn_priority'
365
+ )
366
+
367
+ def get_auto_increment_sequence(self):
368
+ """Get auto increment sequence."""
369
+ table_name = self.model._meta.db_table
370
+ pk_column = self.model._meta.pk.column
371
+ with connection.cursor() as cursor:
372
+ query = "SELECT pg_get_serial_sequence(%s, %s)"
373
+ cursor.execute(query, [table_name, pk_column])
374
+ result = cursor.fetchone()
375
+ return result[0] if result else None
376
+
377
+ def update_auto_increment(self):
378
+ """Update auto increment."""
379
+ table_name = self.model._meta.db_table
380
+ with connection.cursor() as cursor:
381
+ db_engine = connection.vendor
382
+
383
+ if db_engine == "postgresql":
384
+ sequence_name = self.get_auto_increment_sequence()
385
+ # Получаем максимальный id из таблицы
386
+ cursor.execute(
387
+ f"SELECT COALESCE(MAX(id), 0) FROM {table_name};"
388
+ )
389
+ max_id = cursor.fetchone()[0]
390
+ next_id = max_id + 1
391
+ # Прямо указываем следующее значение последовательности
392
+ cursor.execute(
393
+ f"ALTER SEQUENCE {sequence_name} RESTART WITH {next_id};"
394
+ )
395
+ elif db_engine == "mysql":
396
+ cursor.execute(f"SELECT MAX(id) FROM {table_name};")
397
+ max_id = cursor.fetchone()[0] or 0
398
+ next_id = max_id + 1
399
+ cursor.execute(
400
+ f"ALTER TABLE {table_name} AUTO_INCREMENT = {next_id};"
401
+ )
402
+ elif db_engine == "sqlite":
403
+ cursor.execute(
404
+ f"UPDATE sqlite_sequence SET seq = (SELECT MAX(id) \
405
+ FROM {table_name}) WHERE name='{table_name}';"
406
+ )
407
+ elif db_engine == "mssql":
408
+ cursor.execute(f"SELECT MAX(id) FROM {table_name};")
409
+ max_id = cursor.fetchone()[0] or 0
410
+ cursor.execute(
411
+ f"DBCC CHECKIDENT ('{table_name}', RESEED, {max_id});"
412
+ )
413
+ else:
414
+ raise NotImplementedError(
415
+ f"Autoincrement for {db_engine} is not supported."
416
+ )
417
+
280
418
 
281
419
  # The End
@@ -11,7 +11,7 @@ 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.1
14
+ Version: 2.0.11
15
15
  Author: Timur Kady
16
16
  Email: timurkady@yandex.com
17
17
  """
@@ -69,46 +69,30 @@ class ClosureModel(models.Model):
69
69
 
70
70
  @classmethod
71
71
  @cached_method
72
- def get_ancestors_queryset(cls, node, include_self=True, depth=None):
73
- """Get the ancestors QuerySet (ordered from root to parent)."""
74
- filters = {"child__pk": node.pk}
75
- if depth is not None:
76
- filters["depth__lte"] = depth
77
- qs = cls.objects.all().filter(**filters)
78
- if not include_self:
79
- qs = qs.exclude(parent=node, child=node)
80
- return qs
72
+ def get_ancestors_pks(cls, node, include_self=True, depth=None):
73
+ """Get the ancestors pks list."""
74
+ options = dict(child_id=node.pk, depth__gte=0 if include_self else 1)
75
+ if depth:
76
+ options["depth__lte"] = depth
77
+ queryset = cls.objects.filter(**options).order_by('depth')
78
+ return list(queryset.values_list("parent_id", flat=True))
81
79
 
82
80
  @classmethod
83
81
  @cached_method
84
- def get_breadcrumbs(cls, node, attr=None):
85
- """Get the breadcrumbs to current node (included)."""
86
- qs = cls.get_ancestors_queryset(
87
- node, include_self=True).order_by('-depth')
88
- if attr:
89
- return [getattr(item.parent, attr)
90
- if hasattr(item.parent, attr) else None for item in qs]
91
- else:
92
- return [item.parent.pk for item in qs]
93
-
94
- @classmethod
95
- @cached_method
96
- def get_descendants_queryset(cls, node, include_self=False, depth=None):
97
- """Get the descendants QuerySet (ordered from parent to leaf)."""
98
- filters = {"parent__pk": node.pk}
99
- if depth is not None:
100
- filters["depth__lte"] = depth
101
- qs = cls.objects.all().filter(**filters).select_related('parent')
102
- if not include_self:
103
- qs = qs.exclude(parent=node, child=node)
104
- return qs
82
+ def get_descendants_pks(cls, node, include_self=False, depth=None):
83
+ """Get a list containing all descendants."""
84
+ options = dict(parent_id=node.pk, depth__gte=0 if include_self else 1)
85
+ if depth:
86
+ options.update({'depth__lte': depth})
87
+ queryset = cls.objects.filter(**options)
88
+ return list(queryset.values_list("child_id", flat=True))
105
89
 
106
90
  @classmethod
107
91
  @cached_method
108
92
  def get_root(cls, node):
109
- """Get the root node for the given node."""
110
- return cls.objects.filter(child=node).values_list(
111
- "parent", flat=True).order_by('-depth').first()
93
+ """Get the root node pk for the current node."""
94
+ queryset = cls.objects.filter(child=node).order_by('-depth')
95
+ return queryset.firts().parent if queryset.count() > 0 else None
112
96
 
113
97
  @classmethod
114
98
  @cached_method
@@ -126,15 +110,6 @@ class ClosureModel(models.Model):
126
110
  return cls.objects.filter(child__pk=node.pk).aggregate(
127
111
  models.Max("depth"))["depth__max"] + 1
128
112
 
129
- @classmethod
130
- @cached_method
131
- def get_path(cls, node, delimiter='.', format_str=""):
132
- """Return Materialized Path of node."""
133
- str_ = "{%s}" % format_str
134
- priorities = cls.get_breadcrumbs(node, attr='tn_priority')
135
- path = delimiter.join([str_.format(p) for p in priorities])
136
- return path
137
-
138
113
  @classmethod
139
114
  @transaction.atomic
140
115
  def insert_node(cls, node):
@@ -146,10 +121,10 @@ class ClosureModel(models.Model):
146
121
 
147
122
  @classmethod
148
123
  @transaction.atomic
149
- def move_node(cls, node):
150
- """Move a node (and its subtree) to a new parent."""
124
+ def move_node(cls, nodes):
125
+ """Move a nodes (node and its subtree) to a new parent."""
151
126
  # Call bulk_update passing a single object
152
- cls.objects.bulk_update([node], batch_size=1000)
127
+ cls.objects.bulk_update(nodes, batch_size=1000)
153
128
  # Clear cache
154
129
  cls.clear_cache()
155
130