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.
- {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/LICENSE +2 -2
- django_fast_treenode-2.1.0.dist-info/METADATA +161 -0
- django_fast_treenode-2.1.0.dist-info/RECORD +75 -0
- {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/WHEEL +1 -1
- treenode/admin/__init__.py +9 -0
- treenode/admin/admin.py +295 -0
- treenode/admin/changelist.py +65 -0
- treenode/admin/mixins.py +302 -0
- treenode/apps.py +12 -1
- treenode/cache.py +2 -2
- treenode/docs/.gitignore +0 -0
- treenode/docs/about.md +36 -0
- treenode/docs/admin.md +104 -0
- treenode/docs/api.md +739 -0
- treenode/docs/cache.md +187 -0
- treenode/docs/import_export.md +35 -0
- treenode/docs/index.md +30 -0
- treenode/docs/installation.md +74 -0
- treenode/docs/migration.md +145 -0
- treenode/docs/models.md +128 -0
- treenode/docs/roadmap.md +45 -0
- treenode/forms.py +33 -22
- treenode/managers/__init__.py +21 -0
- treenode/managers/adjacency.py +203 -0
- treenode/managers/closure.py +278 -0
- treenode/models/__init__.py +2 -1
- treenode/models/adjacency.py +343 -0
- treenode/models/classproperty.py +3 -0
- treenode/models/closure.py +39 -65
- treenode/models/factory.py +12 -2
- treenode/models/mixins/__init__.py +23 -0
- treenode/models/mixins/ancestors.py +65 -0
- treenode/models/mixins/children.py +81 -0
- treenode/models/mixins/descendants.py +66 -0
- treenode/models/mixins/family.py +63 -0
- treenode/models/mixins/logical.py +68 -0
- treenode/models/mixins/node.py +210 -0
- treenode/models/mixins/properties.py +156 -0
- treenode/models/mixins/roots.py +96 -0
- treenode/models/mixins/siblings.py +99 -0
- treenode/models/mixins/tree.py +344 -0
- treenode/signals.py +26 -0
- treenode/static/treenode/css/tree_widget.css +201 -31
- treenode/static/treenode/css/treenode_admin.css +48 -41
- treenode/static/treenode/js/tree_widget.js +269 -131
- treenode/static/treenode/js/treenode_admin.js +131 -171
- treenode/templates/admin/tree_node_changelist.html +6 -0
- treenode/templates/admin/tree_node_import.html +27 -9
- treenode/templates/admin/tree_node_import_report.html +32 -0
- treenode/templates/admin/treenode_ajax_rows.html +7 -0
- treenode/tests/tests.py +488 -0
- treenode/urls.py +10 -6
- treenode/utils/__init__.py +2 -0
- treenode/utils/aid.py +46 -0
- treenode/utils/base16.py +38 -0
- treenode/utils/base36.py +3 -1
- treenode/utils/db.py +116 -0
- treenode/utils/exporter.py +63 -36
- treenode/utils/importer.py +168 -161
- treenode/utils/radix.py +61 -0
- treenode/version.py +2 -2
- treenode/views.py +119 -38
- treenode/widgets.py +104 -40
- django_fast_treenode-2.0.10.dist-info/METADATA +0 -698
- django_fast_treenode-2.0.10.dist-info/RECORD +0 -41
- treenode/admin.py +0 -396
- treenode/docs/Documentation +0 -664
- treenode/managers.py +0 -281
- treenode/models/proxy.py +0 -650
- {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
|
5
|
-
|
6
|
-
|
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
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
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
|
84
|
-
|
85
|
-
|
86
|
-
queryset = model.objects.
|
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=
|
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
|
+
)
|
treenode/models/__init__.py
CHANGED