django-fast-treenode 2.0.10__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 (70) hide show
  1. {django_fast_treenode-2.0.10.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.10.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 +33 -22
  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 +39 -65
  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/tree_node_import.html +27 -9
  49. treenode/templates/admin/tree_node_import_report.html +32 -0
  50. treenode/templates/admin/treenode_ajax_rows.html +7 -0
  51. treenode/tests/tests.py +488 -0
  52. treenode/urls.py +10 -6
  53. treenode/utils/__init__.py +2 -0
  54. treenode/utils/aid.py +46 -0
  55. treenode/utils/base16.py +38 -0
  56. treenode/utils/base36.py +3 -1
  57. treenode/utils/db.py +116 -0
  58. treenode/utils/exporter.py +63 -36
  59. treenode/utils/importer.py +168 -161
  60. treenode/utils/radix.py +61 -0
  61. treenode/version.py +2 -2
  62. treenode/views.py +119 -38
  63. treenode/widgets.py +104 -40
  64. django_fast_treenode-2.0.10.dist-info/METADATA +0 -698
  65. django_fast_treenode-2.0.10.dist-info/RECORD +0 -41
  66. treenode/admin.py +0 -396
  67. treenode/docs/Documentation +0 -664
  68. treenode/managers.py +0 -281
  69. treenode/models/proxy.py +0 -650
  70. {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/top_level.txt +0 -0
treenode/forms.py CHANGED
@@ -1,22 +1,23 @@
1
1
  """
2
2
  TreeNode Form Module.
3
3
 
4
- This module defines the TreeNodeForm class, which dynamically determines the TreeNode model.
5
- It utilizes TreeWidget and automatically excludes the current node and its descendants
6
- from the parent choices.
4
+ This module defines the TreeNodeForm class, which dynamically determines
5
+ the TreeNode model.
6
+ It utilizes TreeWidget and automatically excludes the current node and its
7
+ descendants from the parent choices.
7
8
 
8
9
  Functions:
9
10
  - __init__: Initializes the form and filters out invalid parent choices.
10
11
  - factory: Dynamically creates a form class for a given TreeNode model.
11
12
 
12
- Version: 2.0.10
13
+ Version: 2.1.0
13
14
  Author: Timur Kady
14
15
  Email: timurkady@yandex.com
15
16
  """
16
17
 
17
18
  from django import forms
18
- import numpy as np
19
19
  from django.forms.models import ModelChoiceField, ModelChoiceIterator
20
+ from django.utils.translation import gettext_lazy as _
20
21
 
21
22
  from .widgets import TreeWidget
22
23
 
@@ -27,13 +28,12 @@ class SortedModelChoiceIterator(ModelChoiceIterator):
27
28
  def __iter__(self):
28
29
  """Return sorted choices based on tn_order."""
29
30
  qs_list = list(self.queryset.all())
30
- # Sort objects by their tn_order using NumPy.
31
- tn_orders = np.array([obj.tn_order for obj in qs_list])
32
- sorted_indices = np.argsort(tn_orders)
33
- # Iterate over sorted indices and yield (value, label) pairs.
34
- for idx in sorted_indices:
35
- # Cast the index to int if it is numpy.int64.
36
- obj = qs_list[int(idx)]
31
+
32
+ # Sort objects
33
+ sorted_objects = self.queryset.model._sort_node_list(qs_list)
34
+
35
+ # Iterate yield (value, label) pairs.
36
+ for obj in sorted_objects:
37
37
  yield (
38
38
  self.field.prepare_value(obj),
39
39
  self.field.label_from_instance(obj)
@@ -43,14 +43,18 @@ class SortedModelChoiceIterator(ModelChoiceIterator):
43
43
  class SortedModelChoiceField(ModelChoiceField):
44
44
  """ModelChoiceField Class for tn_paret field."""
45
45
 
46
+ to_field_name = None
47
+
46
48
  def _get_choices(self):
47
- """Get sorted choices."""
48
49
  if hasattr(self, '_choices'):
49
50
  return self._choices
50
- return SortedModelChoiceIterator(self)
51
+
52
+ choices = list(SortedModelChoiceIterator(self))
53
+ if self.empty_label is not None:
54
+ choices.insert(0, ("", self.empty_label))
55
+ return choices
51
56
 
52
57
  def _set_choices(self, value):
53
- """Set choices."""
54
58
  self._choices = value
55
59
 
56
60
  choices = property(_get_choices, _set_choices)
@@ -77,19 +81,26 @@ class TreeNodeForm(forms.ModelForm):
77
81
  """Init Method."""
78
82
  super().__init__(*args, **kwargs)
79
83
 
80
- # Use a model bound to a form
81
84
  model = self._meta.model
82
85
 
83
- if "tn_parent" in self.fields and self.instance.pk:
84
- excluded_ids = [self.instance.pk] + \
85
- list(self.instance.get_descendants_pks())
86
- queryset = model.objects.exclude(pk__in=excluded_ids)
86
+ if "tn_parent" in self.fields:
87
+ self.fields["tn_parent"].required = False
88
+ self.fields["tn_parent"].empty_label = _("Root")
89
+ queryset = model.objects.all()
90
+
87
91
  original_field = self.fields["tn_parent"]
88
92
  self.fields["tn_parent"] = SortedModelChoiceField(
89
93
  queryset=queryset,
90
- label=self.fields["tn_parent"].label,
91
- widget=original_field.widget
94
+ label=original_field.label,
95
+ widget=original_field.widget,
96
+ empty_label=original_field.empty_label,
97
+ required=False
92
98
  )
99
+ self.fields["tn_parent"].widget.model = queryset.model
100
+
101
+ # If there is a current value, set it
102
+ if self.instance and self.instance.pk and self.instance.tn_parent:
103
+ self.fields["tn_parent"].initial = self.instance.tn_parent
93
104
 
94
105
  @classmethod
95
106
  def factory(cls, model):
@@ -0,0 +1,21 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Managers and QuerySets
4
+
5
+ This module defines custom managers and query sets for the TreeNode model.
6
+ It includes optimized bulk operations for handling hierarchical data
7
+ using the Closure Table approach.
8
+
9
+ Features:
10
+ - `ClosureModelManager` for managing closure records.
11
+ - `TreeNodeModelManager` for adjacency model operations.
12
+
13
+ Version: 2.1.0
14
+ Author: Timur Kady
15
+ Email: timurkady@yandex.com
16
+ """
17
+
18
+ from .closure import ClosureModelManager
19
+ from .adjacency import TreeNodeModelManager
20
+
21
+ __all__ = ["TreeNodeModelManager", "ClosureModelManager"]
@@ -0,0 +1,203 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Adjacency List Manager and QuerySet
4
+
5
+ This module defines custom managers and query sets for the Adjacency List.
6
+ It includes operations for synchronizing with the model implementing
7
+ the Closure Table.
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
+ from django.db import connection
17
+
18
+
19
+ class TreeNodeQuerySet(models.QuerySet):
20
+ """TreeNodeModel QuerySet."""
21
+
22
+ def __init__(self, model=None, query=None, using=None, hints=None):
23
+ # First we call the parent class constructor
24
+ super().__init__(model, query, using, hints)
25
+
26
+ def create(self, **kwargs):
27
+ """Ensure that the save logic is executed when using create."""
28
+ obj = self.model(**kwargs)
29
+ obj.save()
30
+ return obj
31
+
32
+ def update(self, **kwargs):
33
+ """Update node with synchronization of tn_parent change."""
34
+ tn_parent_changed = 'tn_parent' in kwargs
35
+ # Save pks of updated objects
36
+ pks = list(self.values_list('pk', flat=True))
37
+ # Clone the query and clear the ordering to avoid an aggregation error
38
+ qs = self._clone()
39
+ qs.query.clear_ordering()
40
+ result = super(TreeNodeQuerySet, qs).update(**kwargs)
41
+ if tn_parent_changed and pks:
42
+ objs = list(self.model.objects.filter(pk__in=pks))
43
+ self.model.closure_model.objects.bulk_update(objs, ['tn_parent'])
44
+ return result
45
+
46
+ def get_or_create(self, defaults=None, **kwargs):
47
+ """Ensure that the save logic is executed when using get_or_create."""
48
+ defaults = defaults or {}
49
+ created = False
50
+ obj = self.filter(**kwargs).first()
51
+ if obj is None:
52
+ params = {k: v for k, v in kwargs.items() if "__" not in k}
53
+ params.update(
54
+ {k: v() if callable(v) else v for k, v in defaults.items()}
55
+ )
56
+ obj = self.create(**params)
57
+ created = True
58
+ return obj, created
59
+
60
+ def update_or_create(self, defaults=None, create_defaults=None, **kwargs):
61
+ """Update or create."""
62
+ defaults = defaults or {}
63
+ create_defaults = create_defaults or {}
64
+
65
+ with transaction.atomic():
66
+ obj = self.filter(**kwargs).first()
67
+ params = {k: v for k, v in kwargs.items() if "__" not in k}
68
+ if obj is None:
69
+ params.update({k: v() if callable(v) else v for k,
70
+ v in create_defaults.items()})
71
+ obj = self.create(**params)
72
+ created = True
73
+ else:
74
+ params.update(
75
+ {k: v() if callable(v) else v for k, v in defaults.items()})
76
+ for field, value in params.items():
77
+ setattr(obj, field, value)
78
+ obj.save(update_fields=params.keys())
79
+ created = False
80
+ return obj, created
81
+
82
+ def bulk_create(self, objs, batch_size=1000, *args, **kwargs):
83
+ """
84
+ Bulk create.
85
+
86
+ Method of bulk creation objects with updating and processing of
87
+ the Closuse Model.
88
+ """
89
+ # 1. Bulk Insertion of Nodes in Adjacency Models
90
+ objs = super().bulk_create(objs, batch_size, *args, **kwargs)
91
+ # 2. Synchronization of the Closing Model
92
+ self.model.closure_model.objects.bulk_create(objs)
93
+ # 3. Clear cache and return result
94
+ self.model.clear_cache()
95
+ return objs
96
+
97
+ def bulk_update(self, objs, fields, batch_size=1000):
98
+ """Bulk update with synchronization of tn_parent change."""
99
+ # Clone the query and clear the ordering to avoid an aggregation error
100
+ qs = self._clone()
101
+ qs.query.clear_ordering()
102
+ # Perform an Adjacency Model Update
103
+ result = super(TreeNodeQuerySet, qs).bulk_update(
104
+ objs, fields, batch_size
105
+ )
106
+ # Synchronize data in the Closing Model
107
+ if 'tn_parent' in fields:
108
+ self.model.closure_model.objects.bulk_update(
109
+ objs, ['tn_parent'], batch_size
110
+ )
111
+ return result
112
+
113
+
114
+ class TreeNodeModelManager(models.Manager):
115
+ """TreeNodeModel Manager."""
116
+
117
+ def bulk_create(self, objs, batch_size=1000, ignore_conflicts=False):
118
+ """
119
+ Bulk Create.
120
+
121
+ Override bulk_create for the adjacency model.
122
+ Here we first clear the cache, then delegate the creation via our
123
+ custom QuerySet.
124
+ """
125
+ self.model.clear_cache()
126
+ result = self.get_queryset().bulk_create(
127
+ objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts
128
+ )
129
+ transaction.on_commit(lambda: self._update_auto_increment())
130
+ return result
131
+
132
+ def bulk_update(self, objs, fields=None, batch_size=1000):
133
+ """Bulk Update."""
134
+ self.model.clear_cache()
135
+ result = self.get_queryset().bulk_update(objs, fields, batch_size)
136
+ return result
137
+
138
+ def get_queryset(self):
139
+ """Return a sorted QuerySet."""
140
+ queryset = TreeNodeQuerySet(self.model, using=self._db)\
141
+ .annotate(_depth_db=models.Max("parents_set__depth"))\
142
+ .order_by("_depth_db", "tn_parent", "tn_priority")
143
+ return queryset
144
+
145
+ # Service methods -------------------
146
+
147
+ def _bulk_update_tn_closure(self, objs, fields=None, batch_size=1000):
148
+ """Update tn_closure in bulk."""
149
+ self.model.clear_cache()
150
+ super().bulk_update(objs, fields, batch_size)
151
+
152
+ def _get_auto_increment_sequence(self):
153
+ """Get auto increment sequence."""
154
+ table_name = self.model._meta.db_table
155
+ pk_column = self.model._meta.pk.column
156
+ with connection.cursor() as cursor:
157
+ query = "SELECT pg_get_serial_sequence(%s, %s)"
158
+ cursor.execute(query, [table_name, pk_column])
159
+ result = cursor.fetchone()
160
+ return result[0] if result else None
161
+
162
+ def _update_auto_increment(self):
163
+ """Update auto increment."""
164
+ table_name = self.model._meta.db_table
165
+ with connection.cursor() as cursor:
166
+ db_engine = connection.vendor
167
+
168
+ if db_engine == "postgresql":
169
+ sequence_name = self._get_auto_increment_sequence()
170
+ # Get the max id from the table
171
+ cursor.execute(
172
+ f"SELECT COALESCE(MAX(id), 0) FROM {table_name};"
173
+ )
174
+ max_id = cursor.fetchone()[0]
175
+ next_id = max_id + 1
176
+ # Directly specify the next value of the sequence
177
+ cursor.execute(
178
+ f"ALTER SEQUENCE {sequence_name} RESTART WITH {next_id};"
179
+ )
180
+ elif db_engine == "mysql":
181
+ cursor.execute(f"SELECT MAX(id) FROM {table_name};")
182
+ max_id = cursor.fetchone()[0] or 0
183
+ next_id = max_id + 1
184
+ cursor.execute(
185
+ f"ALTER TABLE {table_name} AUTO_INCREMENT = {next_id};"
186
+ )
187
+ elif db_engine == "sqlite":
188
+ cursor.execute(
189
+ f"UPDATE sqlite_sequence SET seq = (SELECT MAX(id) \
190
+ FROM {table_name}) WHERE name='{table_name}';"
191
+ )
192
+ elif db_engine == "mssql":
193
+ cursor.execute(f"SELECT MAX(id) FROM {table_name};")
194
+ max_id = cursor.fetchone()[0] or 0
195
+ cursor.execute(
196
+ f"DBCC CHECKIDENT ('{table_name}', RESEED, {max_id});"
197
+ )
198
+ else:
199
+ raise NotImplementedError(
200
+ f"Autoincrement for {db_engine} is not supported."
201
+ )
202
+
203
+ # The End
@@ -0,0 +1,278 @@
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,4 +1,5 @@
1
- from .proxy import TreeNodeModel
1
+ # -*- coding: utf-8 -*-
2
+ from .adjacency import TreeNodeModel
2
3
 
3
4
 
4
5
  __all__ = ["TreeNodeModel",]