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
@@ -0,0 +1,96 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Roots Mixin
|
4
|
+
|
5
|
+
Version: 2.1.0
|
6
|
+
Author: Timur Kady
|
7
|
+
Email: timurkady@yandex.com
|
8
|
+
"""
|
9
|
+
|
10
|
+
from django.db import models
|
11
|
+
from treenode.cache import cached_method
|
12
|
+
|
13
|
+
|
14
|
+
class TreeNodeRootsMixin(models.Model):
|
15
|
+
"""TreeNode Roots Mixin."""
|
16
|
+
|
17
|
+
class Meta:
|
18
|
+
"""Moxin Meta Class."""
|
19
|
+
|
20
|
+
abstract = True
|
21
|
+
|
22
|
+
@classmethod
|
23
|
+
def add_root(cls, position=None, **kwargs):
|
24
|
+
"""
|
25
|
+
Add a root node to the tree.
|
26
|
+
|
27
|
+
Adds a new root node at the specified position. If no position is
|
28
|
+
specified, the new node will be the last element in the root.
|
29
|
+
Parameters:
|
30
|
+
position: can be 'first-root', 'last-root', 'sorted-root' or integer
|
31
|
+
value.
|
32
|
+
**kwargs – Object creation data that will be passed to the inherited
|
33
|
+
Node model
|
34
|
+
instance – Instead of passing object creation data, you can pass
|
35
|
+
an already-constructed (but not yet saved) model instance to be
|
36
|
+
inserted into the tree.
|
37
|
+
|
38
|
+
Returns:
|
39
|
+
The created node object. It will be save()d by this method.
|
40
|
+
"""
|
41
|
+
if isinstance(position, int):
|
42
|
+
priority = position
|
43
|
+
else:
|
44
|
+
if position not in ['first-root', 'last-root', 'sorted-root']:
|
45
|
+
raise ValueError(f"Invalid position format: {position}")
|
46
|
+
|
47
|
+
parent, priority = cls._get_place(None, position)
|
48
|
+
|
49
|
+
instance = kwargs.get("instance")
|
50
|
+
if instance is None:
|
51
|
+
instance = cls(**kwargs)
|
52
|
+
|
53
|
+
parent, priority = cls._get_place(None, position)
|
54
|
+
instance.tn_parent = None
|
55
|
+
instance.tn_priority = priority
|
56
|
+
instance.save()
|
57
|
+
return instance
|
58
|
+
|
59
|
+
@classmethod
|
60
|
+
@cached_method
|
61
|
+
def get_roots_queryset(cls):
|
62
|
+
"""Get root nodes queryset with preloaded children."""
|
63
|
+
qs = cls.objects.filter(tn_parent=None).prefetch_related('tn_children')
|
64
|
+
return qs
|
65
|
+
|
66
|
+
@classmethod
|
67
|
+
@cached_method
|
68
|
+
def get_roots_pks(cls):
|
69
|
+
"""Get a list with all root nodes."""
|
70
|
+
pks = cls.objects.filter(tn_parent=None).values_list("id", flat=True)
|
71
|
+
return list(pks)
|
72
|
+
|
73
|
+
@classmethod
|
74
|
+
def get_roots(cls):
|
75
|
+
"""Get a list with all root nodes."""
|
76
|
+
qs = cls.get_roots_queryset()
|
77
|
+
return list(qs)
|
78
|
+
|
79
|
+
@classmethod
|
80
|
+
def get_roots_count(cls):
|
81
|
+
"""Get a list with all root nodes."""
|
82
|
+
return len(cls.get_roots_pks())
|
83
|
+
|
84
|
+
@classmethod
|
85
|
+
def get_first_root(cls):
|
86
|
+
"""Return the first root node in the tree or None if it is empty."""
|
87
|
+
roots = cls.get_roots_queryset()
|
88
|
+
return roots.fiest() if roots.count() > 0 else None
|
89
|
+
|
90
|
+
@classmethod
|
91
|
+
def get_last_root(cls):
|
92
|
+
"""Return the last root node in the tree or None if it is empty."""
|
93
|
+
roots = cls.get_roots_queryset()
|
94
|
+
return roots.last() if roots.count() > 0 else None
|
95
|
+
|
96
|
+
# The End
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Siblings Mixin
|
4
|
+
|
5
|
+
Version: 2.1.0
|
6
|
+
Author: Timur Kady
|
7
|
+
Email: timurkady@yandex.com
|
8
|
+
"""
|
9
|
+
|
10
|
+
from django.db import models
|
11
|
+
from treenode.cache import cached_method
|
12
|
+
|
13
|
+
|
14
|
+
class TreeNodeSiblingsMixin(models.Model):
|
15
|
+
"""TreeNode Siblings Mixin."""
|
16
|
+
|
17
|
+
class Meta:
|
18
|
+
"""Moxin Meta Class."""
|
19
|
+
|
20
|
+
abstract = True
|
21
|
+
|
22
|
+
def add_sibling(self, position=None, **kwargs):
|
23
|
+
"""
|
24
|
+
Add a new node as a sibling to the current node object.
|
25
|
+
|
26
|
+
Returns the created node object or None if failed. It will be saved
|
27
|
+
by this method.
|
28
|
+
"""
|
29
|
+
if isinstance(position, int):
|
30
|
+
priority = position
|
31
|
+
parent = self.tn_parent
|
32
|
+
else:
|
33
|
+
if position not in [
|
34
|
+
'first-sibling', 'left-sibling', 'right-sibling',
|
35
|
+
'last-sibling', 'sorted-sibling']:
|
36
|
+
raise ValueError(f"Invalid position format: {position}")
|
37
|
+
parent, priority = self._meta.model._get_place(self, position)
|
38
|
+
|
39
|
+
instance = kwargs.get("instance")
|
40
|
+
if instance is None:
|
41
|
+
instance = self._meta.model(**kwargs)
|
42
|
+
instance.tn_priority = priority
|
43
|
+
instance.tn_priority = parent
|
44
|
+
instance.save()
|
45
|
+
return instance
|
46
|
+
|
47
|
+
@cached_method
|
48
|
+
def get_siblings_queryset(self):
|
49
|
+
"""Get the siblings queryset with prefetch."""
|
50
|
+
if self.tn_parent:
|
51
|
+
qs = self.tn_parent.tn_children.prefetch_related('tn_children')
|
52
|
+
else:
|
53
|
+
qs = self._meta.model.objects.filter(tn_parent__isnull=True)
|
54
|
+
return qs.exclude(pk=self.pk)
|
55
|
+
|
56
|
+
def get_siblings(self):
|
57
|
+
"""Get a list with all the siblings."""
|
58
|
+
return list(self.get_siblings_queryset())
|
59
|
+
|
60
|
+
def get_siblings_count(self):
|
61
|
+
"""Get the siblings count."""
|
62
|
+
return self.get_siblings_queryset().count()
|
63
|
+
|
64
|
+
def get_siblings_pks(self):
|
65
|
+
"""Get the siblings pks list."""
|
66
|
+
return [item.pk for item in self.get_siblings_queryset()]
|
67
|
+
|
68
|
+
def get_first_sibling(self):
|
69
|
+
"""
|
70
|
+
Return the fist node’s sibling.
|
71
|
+
|
72
|
+
Method can return the node itself if it was the leftmost sibling.
|
73
|
+
"""
|
74
|
+
return self.get_siblings_queryset().fist()
|
75
|
+
|
76
|
+
def get_previous_sibling(self):
|
77
|
+
"""Return the previous sibling in the tree, or None."""
|
78
|
+
priority = self.tn_priority - 1
|
79
|
+
if priority < 0:
|
80
|
+
return None
|
81
|
+
return self.get_siblings_queryset.filter(tn_priority=priority)
|
82
|
+
|
83
|
+
def get_next_sibling(self):
|
84
|
+
"""Return the next sibling in the tree, or None."""
|
85
|
+
priority = self.tn_priority = 1
|
86
|
+
queryset = self.get_siblings_queryset()
|
87
|
+
if priority == queryset.count():
|
88
|
+
return None
|
89
|
+
return queryset.filter(tn_priority=priority)
|
90
|
+
|
91
|
+
def get_last_sibling(self):
|
92
|
+
"""
|
93
|
+
Return the fist node’s sibling.
|
94
|
+
|
95
|
+
Method can return the node itself if it was the leftmost sibling.
|
96
|
+
"""
|
97
|
+
return self.get_siblings_queryset().last()
|
98
|
+
|
99
|
+
# The End
|
@@ -0,0 +1,344 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Tree Mixin
|
4
|
+
|
5
|
+
Version: 2.1.0
|
6
|
+
Author: Timur Kady
|
7
|
+
Email: timurkady@yandex.com
|
8
|
+
"""
|
9
|
+
|
10
|
+
import json
|
11
|
+
from django.db import models, transaction
|
12
|
+
from collections import OrderedDict
|
13
|
+
from django.core.serializers.json import DjangoJSONEncoder
|
14
|
+
|
15
|
+
from ...cache import cached_method
|
16
|
+
|
17
|
+
|
18
|
+
class TreeNodeTreeMixin(models.Model):
|
19
|
+
"""TreeNode Tree Mixin."""
|
20
|
+
|
21
|
+
class Meta:
|
22
|
+
"""Moxin Meta Class."""
|
23
|
+
|
24
|
+
abstract = True
|
25
|
+
|
26
|
+
@classmethod
|
27
|
+
def dump_tree(cls, instance=None):
|
28
|
+
"""
|
29
|
+
Return an n-dimensional dictionary representing the model tree.
|
30
|
+
|
31
|
+
Introduced for compatibility with other packages.
|
32
|
+
"""
|
33
|
+
return cls.get_tree(cls, instance)
|
34
|
+
|
35
|
+
@classmethod
|
36
|
+
@cached_method
|
37
|
+
def get_tree(cls, instance=None):
|
38
|
+
"""
|
39
|
+
Return an n-dimensional dictionary representing the model tree.
|
40
|
+
|
41
|
+
If instance is passed, returns a subtree rooted at instance (using
|
42
|
+
get_descendants_queryset), if not passed, builds a tree for all nodes
|
43
|
+
(loads all records in one query).
|
44
|
+
"""
|
45
|
+
# If instance is passed, we get all its descendants (including itself)
|
46
|
+
if instance:
|
47
|
+
queryset = instance.get_descendants_queryset(include_self=True)\
|
48
|
+
.annotate(depth=models.Max("parents_set__depth"))
|
49
|
+
else:
|
50
|
+
# Load all records at once
|
51
|
+
queryset = cls.objects.all()
|
52
|
+
|
53
|
+
# Dictionary for quick access to nodes by id and list for iteration
|
54
|
+
nodes_by_id = {}
|
55
|
+
nodes_list = []
|
56
|
+
|
57
|
+
# Loop through all nodes using an iterator
|
58
|
+
for node in queryset.iterator(chunk_size=1000):
|
59
|
+
# 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
|
+
node_dict = OrderedDict()
|
66
|
+
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')
|
71
|
+
|
72
|
+
# Add the rest of the model fields.
|
73
|
+
# Iterate over all the fields obtained via _meta.get_fields()
|
74
|
+
for field in node._meta.get_fields():
|
75
|
+
# Skipping fields that are already added or not required
|
76
|
+
# (e.g. tn_closure or virtual links)
|
77
|
+
if field.name in [
|
78
|
+
'id', 'tn_parent', 'tn_priority', 'tn_closure',
|
79
|
+
'children']:
|
80
|
+
continue
|
81
|
+
|
82
|
+
try:
|
83
|
+
value = getattr(node, field.name)
|
84
|
+
except Exception:
|
85
|
+
value = None
|
86
|
+
|
87
|
+
# If the field is many-to-many, we get a list of IDs of
|
88
|
+
# related objects
|
89
|
+
if hasattr(value, 'all'):
|
90
|
+
value = list(value.all().values_list('id', flat=True))
|
91
|
+
|
92
|
+
node_dict[field.name] = value
|
93
|
+
|
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
|
+
# Save the node both in the list and in the dictionary by id
|
103
|
+
# for quick access
|
104
|
+
nodes_by_id[node.id] = node_dict
|
105
|
+
nodes_list.append(node_dict)
|
106
|
+
|
107
|
+
# Build a tree: assign each node a list of its children
|
108
|
+
tree = []
|
109
|
+
for node_dict in nodes_list:
|
110
|
+
parent_id = node_dict['tn_parent']
|
111
|
+
# If there is a parent and it is present in nodes_by_id, then
|
112
|
+
# add the current node to the list of its children
|
113
|
+
if parent_id and parent_id in nodes_by_id:
|
114
|
+
parent_node = nodes_by_id[parent_id]
|
115
|
+
parent_node['children'].append(node_dict)
|
116
|
+
else:
|
117
|
+
# If there is no parent, this is the root node of the tree
|
118
|
+
tree.append(node_dict)
|
119
|
+
|
120
|
+
return tree
|
121
|
+
|
122
|
+
@classmethod
|
123
|
+
def get_tree_json(cls, instance=None):
|
124
|
+
"""Represent the tree as a JSON-compatible string."""
|
125
|
+
tree = cls.dump_tree(instance)
|
126
|
+
return DjangoJSONEncoder().encode(tree)
|
127
|
+
|
128
|
+
@classmethod
|
129
|
+
def load_tree(cls, tree_data):
|
130
|
+
"""
|
131
|
+
Load a tree from a list of dictionaries.
|
132
|
+
|
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.
|
136
|
+
"""
|
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
|
+
"""
|
145
|
+
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
|
+
return flat
|
159
|
+
|
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:
|
207
|
+
cls.objects.bulk_update(
|
208
|
+
nodes_to_update,
|
209
|
+
fields=field_names,
|
210
|
+
batch_size=1000
|
211
|
+
)
|
212
|
+
cls.clear_cache()
|
213
|
+
|
214
|
+
@classmethod
|
215
|
+
def load_tree_json(cls, json_str):
|
216
|
+
"""
|
217
|
+
Decode a JSON string into a dictionary.
|
218
|
+
|
219
|
+
Takes a JSON-compatible string and decodes it into a tree structure.
|
220
|
+
"""
|
221
|
+
try:
|
222
|
+
tree_data = json.loads(json_str)
|
223
|
+
except json.JSONDecodeError as e:
|
224
|
+
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
|
252
|
+
|
253
|
+
@classmethod
|
254
|
+
@cached_method
|
255
|
+
def get_tree_annotated(cls):
|
256
|
+
"""
|
257
|
+
Get an annotated list from a tree branch.
|
258
|
+
|
259
|
+
Something like this will be returned:
|
260
|
+
[
|
261
|
+
(a, {'open':True, 'close':[], 'level': 0})
|
262
|
+
(ab, {'open':True, 'close':[], 'level': 1})
|
263
|
+
(aba, {'open':True, 'close':[], 'level': 2})
|
264
|
+
(abb, {'open':False, 'close':[], 'level': 2})
|
265
|
+
(abc, {'open':False, 'close':[0,1], 'level': 2})
|
266
|
+
(ac, {'open':False, 'close':[0], 'level': 1})
|
267
|
+
]
|
268
|
+
|
269
|
+
All nodes are ordered by materialized path.
|
270
|
+
This can be used with a template like this:
|
271
|
+
|
272
|
+
{% for item, info in annotated_list %}
|
273
|
+
{% if info.open %}
|
274
|
+
<ul><li>
|
275
|
+
{% else %}
|
276
|
+
</li><li>
|
277
|
+
{% endif %}
|
278
|
+
|
279
|
+
{{ item }}
|
280
|
+
|
281
|
+
{% for close in info.close %}
|
282
|
+
</li></ul>
|
283
|
+
{% endfor %}
|
284
|
+
{% endfor %}
|
285
|
+
|
286
|
+
"""
|
287
|
+
# 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")
|
292
|
+
# Convert queryset to list for indexed access
|
293
|
+
nodes = list(queryset)
|
294
|
+
total_nodes = len(nodes)
|
295
|
+
sorted_nodes = cls._sort_node_list(nodes)
|
296
|
+
|
297
|
+
result = []
|
298
|
+
|
299
|
+
for i, node in enumerate(sorted_nodes):
|
300
|
+
# Get the value to display the node
|
301
|
+
value = str(node)
|
302
|
+
# Determine if there are descendants (use prefetch_related to avoid
|
303
|
+
# additional queries)
|
304
|
+
value_open = len(node.tn_children.all()) > 0
|
305
|
+
level = node.depth
|
306
|
+
|
307
|
+
# Calculate the "close" field
|
308
|
+
if i + 1 < total_nodes:
|
309
|
+
next_node = nodes[i + 1]
|
310
|
+
depth_diff = level - next_node.depth
|
311
|
+
# If the next node is at a lower level, then some open
|
312
|
+
# levels need to be closed
|
313
|
+
value_close = list(range(next_node.depth, level)
|
314
|
+
) if depth_diff > 0 else []
|
315
|
+
else:
|
316
|
+
# For the last node, close all open levels
|
317
|
+
value_close = list(range(0, level + 1))
|
318
|
+
|
319
|
+
result.append(
|
320
|
+
(value, {
|
321
|
+
"open": value_open,
|
322
|
+
"close": value_close,
|
323
|
+
"level": level
|
324
|
+
})
|
325
|
+
)
|
326
|
+
return result
|
327
|
+
|
328
|
+
@classmethod
|
329
|
+
@transaction.atomic
|
330
|
+
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)
|
337
|
+
|
338
|
+
@classmethod
|
339
|
+
def delete_tree(cls):
|
340
|
+
"""Delete the whole tree for the current node class."""
|
341
|
+
cls.clear_cache()
|
342
|
+
cls.objects.all().delete()
|
343
|
+
|
344
|
+
# The end
|
treenode/signals.py
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Signals Module
|
4
|
+
|
5
|
+
Version: 2.1.0
|
6
|
+
Author: Timur Kady
|
7
|
+
Email: timurkady@yandex.com
|
8
|
+
"""
|
9
|
+
|
10
|
+
from contextlib import contextmanager
|
11
|
+
|
12
|
+
|
13
|
+
@contextmanager
|
14
|
+
def disable_signals(signal, sender):
|
15
|
+
"""Temporarily disable execution of signal generation."""
|
16
|
+
# Save current signal handlers
|
17
|
+
old_receivers = signal.receivers[:]
|
18
|
+
signal.receivers = []
|
19
|
+
try:
|
20
|
+
yield
|
21
|
+
finally:
|
22
|
+
# Restore handlers
|
23
|
+
signal.receivers = old_receivers
|
24
|
+
|
25
|
+
|
26
|
+
# Tne End
|