django-fast-treenode 2.1.2__py3-none-any.whl → 2.1.3__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.1.2.dist-info → django_fast_treenode-2.1.3.dist-info}/METADATA +5 -1
- {django_fast_treenode-2.1.2.dist-info → django_fast_treenode-2.1.3.dist-info}/RECORD +8 -9
- {django_fast_treenode-2.1.2.dist-info → django_fast_treenode-2.1.3.dist-info}/WHEEL +1 -1
- treenode/cache.py +1 -1
- treenode/models/mixins/node.py +1 -1
- treenode/version.py +1 -1
- treenode/tests/tests.py +0 -488
- {django_fast_treenode-2.1.2.dist-info → django_fast_treenode-2.1.3.dist-info}/LICENSE +0 -0
- {django_fast_treenode-2.1.2.dist-info → django_fast_treenode-2.1.3.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: django-fast-treenode
|
3
|
-
Version: 2.1.
|
3
|
+
Version: 2.1.3
|
4
4
|
Summary: Application for supporting tree (hierarchical) data structure in Django projects
|
5
5
|
Home-page: https://django-fast-treenode.readthedocs.io/
|
6
6
|
Author: Timur Kady
|
@@ -63,6 +63,10 @@ Requires-Dist: xlsxwriter; extra == "import-export"
|
|
63
63
|
# Django-fast-treenode
|
64
64
|
**Combining Adjacency List and Closure Table for Optimal Performance**
|
65
65
|
|
66
|
+
[](https://github.com/TimurKady/django-fast-treenode/actions/workflows/test.yaml)
|
67
|
+
[](https://django-fast-treenode.readthedocs.io/)
|
68
|
+
[](https://pypi.org/project/django-fast-treenode/)
|
69
|
+
[](https://djangopackages.org/packages/p/django-fast-treenode/)
|
66
70
|
|
67
71
|
**Django Fast TreeNode** is a high-performance Django application for working with tree structures, combining **Adjacency List** and **Closure Table** models. Each **TreeNodeModel** instance maintains two synchronized tables, enabling most operations to be performed with a single database query.
|
68
72
|
|
@@ -1,10 +1,10 @@
|
|
1
1
|
treenode/__init__.py,sha256=CZ-0uZyhi8OZJP8xmqbTqXrZUjcQ4SNAXhKFgM0qw2M,99
|
2
2
|
treenode/apps.py,sha256=a7UasXiZZudPccjmHEudP79TkhR_53Mvnb-dBXLHRRQ,862
|
3
|
-
treenode/cache.py,sha256=
|
3
|
+
treenode/cache.py,sha256=JLbI0EWq1XmH24SF46glC6e7sdizmfObbFA5vQJvMiY,7213
|
4
4
|
treenode/forms.py,sha256=Mjrpuyd1CPsitcElDVagE3k-p2kU4xIlRuy1f5Zgt3c,3800
|
5
5
|
treenode/signals.py,sha256=ERrlKjGqhYaPYVKKRk1JBBlPFOmJKpJ6bXsJavcTlo0,518
|
6
6
|
treenode/urls.py,sha256=CsgX0hRyDVrMS8YnRlr_CxmDlgGIhDpqZ9ldoMYZCac,866
|
7
|
-
treenode/version.py,sha256=
|
7
|
+
treenode/version.py,sha256=PBeSQ_6jPFoS_aURwvCBCsE-VlTS5vbGFlvnO41XfMY,222
|
8
8
|
treenode/views.py,sha256=rEZEgdbEA3AJDHrvtrAm-t60QTJcJ4JEhNsNMR1Y_I4,5549
|
9
9
|
treenode/widgets.py,sha256=Mi0F-AK_UcmU6C50ENK9vv6xGQNuDtrtzXSnXSOXhLM,4760
|
10
10
|
treenode/admin/__init__.py,sha256=TdlPIyRW8i9qTVqGLmLWiBw4DyoGHUYZErE6rCyGOPE,119
|
@@ -25,7 +25,7 @@ treenode/models/mixins/children.py,sha256=OchaH6m6pOr6uuiZRRBHoZXoCSWM-ENTNWu1iL
|
|
25
25
|
treenode/models/mixins/descendants.py,sha256=2mhnIhC8VJomTqntzzAwFFW_CcMiwujzQoD5_mfMsK0,2208
|
26
26
|
treenode/models/mixins/family.py,sha256=h2IRRADkQxve97QqBHKv0evVz4cFQtcNR8CbPi9Ri_w,1645
|
27
27
|
treenode/models/mixins/logical.py,sha256=jlhBSq3AfCYNyNjqyKM9siyioS3SYcGD-aG2b4MV2RM,2169
|
28
|
-
treenode/models/mixins/node.py,sha256=
|
28
|
+
treenode/models/mixins/node.py,sha256=wgLbFKA99QuLV4l32GR3JZ5gYIVKUbfDzM8iDw4C8Bs,7694
|
29
29
|
treenode/models/mixins/properties.py,sha256=pfv80KLXcPeGx00IFCBcst1_cf0AmzhjshFjq1XQWMY,3876
|
30
30
|
treenode/models/mixins/roots.py,sha256=MoFQq1fph70awc26UMUbfeTpt0ToUOvMz1c7LlDyIP8,2956
|
31
31
|
treenode/models/mixins/siblings.py,sha256=fh0ZrlFXKxOQ4Qrp6sElTMvRhU5PyRRykLHDcbH-3Rk,3113
|
@@ -48,7 +48,6 @@ treenode/templates/admin/tree_node_import_report.html,sha256=azHJ8JFrSRu60lF1Uh2
|
|
48
48
|
treenode/templates/admin/treenode_ajax_rows.html,sha256=zFyPaTbSyxRjOqQ85SMv__qTIYDjEna6chYODBypDZA,224
|
49
49
|
treenode/templates/widgets/tree_widget.css,sha256=2bEaxu1x7QJZ7erbs2SLMaxeaiMkjQXadfcDEW8wfok,551
|
50
50
|
treenode/templates/widgets/tree_widget.html,sha256=GKcCU-B2FkkJ2BSOuXOw9e_PdYTtADcvyITEXqOlZ9Y,723
|
51
|
-
treenode/tests/tests.py,sha256=9Bd2BhvwtVhYBp5DEtkzKPpAP1iFo4asMsydzuIRASM,19316
|
52
51
|
treenode/utils/__init__.py,sha256=B4bv96ivtHELPv0_DllJa5z-k1QMo7z-MKuvj-3NdtI,356
|
53
52
|
treenode/utils/aid.py,sha256=o8Jgc1vDRtQpx4XYdv0qR5Lqvens55Jfbdca1nr-EOA,1013
|
54
53
|
treenode/utils/base16.py,sha256=U1PMit2aZOpYusG_u1c7eVpXO-cFrFPyVyk9zdHrehg,817
|
@@ -57,8 +56,8 @@ treenode/utils/db.py,sha256=36q4OckKmEd6uHTbMTxdKpV9nOIZ55DAantRWR9bxWg,4297
|
|
57
56
|
treenode/utils/exporter.py,sha256=mV6Gch7XfW8f_1x3WqWgtV0qekMLdo-_n9gz6GJjXjw,7259
|
58
57
|
treenode/utils/importer.py,sha256=Hvirbd6NyZ2MHa56_jOrUF3kYFeby1DbSLR3mhHy-9s,12891
|
59
58
|
treenode/utils/radix.py,sha256=zHpOuDxsebiv9Gza6snNhAtBKiex6CDrAVRtB6esaWo,1642
|
60
|
-
django_fast_treenode-2.1.
|
61
|
-
django_fast_treenode-2.1.
|
62
|
-
django_fast_treenode-2.1.
|
63
|
-
django_fast_treenode-2.1.
|
64
|
-
django_fast_treenode-2.1.
|
59
|
+
django_fast_treenode-2.1.3.dist-info/LICENSE,sha256=T0evsb7y-63fg18ovdNSx3wwWWRwyluQvN9J4zFSvfE,1093
|
60
|
+
django_fast_treenode-2.1.3.dist-info/METADATA,sha256=glnnVS6RVwKFZkbQOLVkcow74o-MKFS_AsPTY_smBd4,8165
|
61
|
+
django_fast_treenode-2.1.3.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
62
|
+
django_fast_treenode-2.1.3.dist-info/top_level.txt,sha256=fmgxHbXyx1O2MPi_9kjx8aL9L-8TmV0gre4Go8XgqFk,9
|
63
|
+
django_fast_treenode-2.1.3.dist-info/RECORD,,
|
treenode/cache.py
CHANGED
treenode/models/mixins/node.py
CHANGED
@@ -23,7 +23,7 @@ class TreeNodeNodeMixin(models.Model):
|
|
23
23
|
abstract = True
|
24
24
|
|
25
25
|
@cached_method
|
26
|
-
def get_breadcrumbs(self, attr='
|
26
|
+
def get_breadcrumbs(self, attr='id'):
|
27
27
|
"""Optimized breadcrumbs retrieval with direct cache check."""
|
28
28
|
try:
|
29
29
|
self._meta.get_field(attr)
|
treenode/version.py
CHANGED
treenode/tests/tests.py
DELETED
@@ -1,488 +0,0 @@
|
|
1
|
-
import time
|
2
|
-
import traceback
|
3
|
-
from django.test import TestCase, TransactionTestCase
|
4
|
-
from devapp.models import Entity
|
5
|
-
|
6
|
-
|
7
|
-
class BasicOperationsTest(TestCase):
|
8
|
-
"""
|
9
|
-
Tests basic operations with nodes: insertion (using three methods), deletion,
|
10
|
-
moving, as well as the functionality of QuerySet methods (e.g. retrieving children).
|
11
|
-
"""
|
12
|
-
|
13
|
-
def runTest(self):
|
14
|
-
errors = []
|
15
|
-
print("\n=== BasicOperationsTest ===")
|
16
|
-
|
17
|
-
# 1. Insertion via node.save()
|
18
|
-
try:
|
19
|
-
node1 = Entity(name='Node via save()')
|
20
|
-
node1.save()
|
21
|
-
if not node1.pk:
|
22
|
-
errors.append("Insertion via save() did not assign a PK.")
|
23
|
-
except Exception as e:
|
24
|
-
errors.append("Insertion via save() raised an exception: " + str(e))
|
25
|
-
|
26
|
-
# 2. Insertion via objects.create()
|
27
|
-
try:
|
28
|
-
node2 = Entity.objects.create(name='Node via create()')
|
29
|
-
if not node1.pk:
|
30
|
-
errors.append(
|
31
|
-
"Insertion via objects.create() did not assign a PK.")
|
32
|
-
except Exception as e:
|
33
|
-
errors.append(
|
34
|
-
"Insertion via objects.create() raised an exception: " + str(e))
|
35
|
-
|
36
|
-
# 3. Alternative method (insert_at or get_or_create)
|
37
|
-
try:
|
38
|
-
# For demonstration, use node1 as the parent if the insert_at method exists
|
39
|
-
if hasattr(Entity, 'insert_at'):
|
40
|
-
node3 = Entity(name='Node via insert_at()')
|
41
|
-
node3.insert_at(node1)
|
42
|
-
elif hasattr(Entity.objects, 'get_or_create'):
|
43
|
-
node3, created = Entity.objects.get_or_create(
|
44
|
-
name='Node via get_or_create()', defaults={'tn_parent': node1})
|
45
|
-
else:
|
46
|
-
errors.append(
|
47
|
-
"No alternative insertion method found (insert_at/get_or_create).")
|
48
|
-
except Exception as e:
|
49
|
-
errors.append(
|
50
|
-
"Alternative insertion raised an exception: " + str(e))
|
51
|
-
|
52
|
-
# Checking access methods (e.g. get_children)
|
53
|
-
try:
|
54
|
-
# Create a small tree
|
55
|
-
root = Entity.objects.create(name='Root')
|
56
|
-
child = Entity.objects.create(name='Child', tn_parent=root)
|
57
|
-
# If the model defines a get_children() method, check its functionality
|
58
|
-
if hasattr(root, 'get_children'):
|
59
|
-
children = root.get_children()
|
60
|
-
if child not in children:
|
61
|
-
errors.append(
|
62
|
-
"Method get_children() did not return the added child.")
|
63
|
-
else:
|
64
|
-
# If the method does not exist, perform a simple filter by parent
|
65
|
-
children = Entity.objects.filter(tn_parent=root)
|
66
|
-
if child not in children:
|
67
|
-
errors.append(
|
68
|
-
"Filter by parent did not return the added child.")
|
69
|
-
except Exception as e:
|
70
|
-
errors.append("Access methods check raised an exception: " + str(e))
|
71
|
-
|
72
|
-
# Tests for node deletion and moving can be added in a similar manner
|
73
|
-
|
74
|
-
# Output report for basic operations
|
75
|
-
if errors:
|
76
|
-
print("BasicOperationsTest: FAILED. Errors detected:")
|
77
|
-
for err in errors:
|
78
|
-
print(" -", err)
|
79
|
-
else:
|
80
|
-
print("BasicOperationsTest: PASSED.")
|
81
|
-
|
82
|
-
self.assertEqual(
|
83
|
-
len(errors), 0, "BasicOperationsTest: errors - " + ", ".join(errors))
|
84
|
-
|
85
|
-
|
86
|
-
class DeepTreePerformanceTest(TransactionTestCase):
|
87
|
-
"""
|
88
|
-
Performance test for a deep tree:
|
89
|
-
- A tree is created with one root and 100 levels of nesting.
|
90
|
-
- The insertion time for nodes is measured at levels: 0 (root), 1, 10, 50, 100.
|
91
|
-
- Results are printed to the console with absolute values and as a percentage of the root insertion time.
|
92
|
-
"""
|
93
|
-
|
94
|
-
def runTest(self):
|
95
|
-
print("\n=== DeepTreePerformanceTest ===")
|
96
|
-
timings = {} # dictionary: level -> insertion time (in seconds)
|
97
|
-
nodes = [] # storing references to nodes for potential read tests
|
98
|
-
|
99
|
-
try:
|
100
|
-
# Insertion of the root node
|
101
|
-
start = time.time()
|
102
|
-
root = Entity.objects.create(name="Deep Root")
|
103
|
-
end = time.time()
|
104
|
-
timings[0] = end - start
|
105
|
-
nodes.append(root)
|
106
|
-
|
107
|
-
current_parent = root
|
108
|
-
|
109
|
-
for level in range(1, 201):
|
110
|
-
start = time.time()
|
111
|
-
new_node = Entity.objects.create(
|
112
|
-
name=f"Node Level {level}", tn_parent=current_parent)
|
113
|
-
end = time.time()
|
114
|
-
timings[level] = end - start
|
115
|
-
nodes.append(new_node)
|
116
|
-
current_parent = new_node # the next node will be a child of the one just created
|
117
|
-
|
118
|
-
# Generate report for levels: 0, 1, 10, 20, 30, 40, 50, 70, 80, 90, 100, 150, 200
|
119
|
-
levels_to_report = [0, 1, 10, 20, 30,
|
120
|
-
40, 50, 70, 80, 90, 100, 150, 200]
|
121
|
-
# to avoid division by zero
|
122
|
-
root_time = timings[0] if timings[0] > 0 else 1e-6
|
123
|
-
print("Deep tree - node insertion times:")
|
124
|
-
print("Level\tInsertion time (s)\t% of root time")
|
125
|
-
for lvl in levels_to_report:
|
126
|
-
t = timings.get(lvl, None)
|
127
|
-
if t is not None:
|
128
|
-
perc = (t / root_time) * 100
|
129
|
-
print(f"{lvl}\t{t:.6f}\t\t{perc:.2f}%")
|
130
|
-
else:
|
131
|
-
print(f"{lvl}\tNo data")
|
132
|
-
|
133
|
-
# Additionally, one can measure node reading times using a similar approach.
|
134
|
-
except Exception as e:
|
135
|
-
print("DeepTreePerformanceTest: exception:", e)
|
136
|
-
traceback.print_exc()
|
137
|
-
|
138
|
-
# Since the test is exploratory in nature, simply assert True.
|
139
|
-
self.assertTrue(True)
|
140
|
-
|
141
|
-
|
142
|
-
class WideTreePerformanceTest(TransactionTestCase):
|
143
|
-
"""
|
144
|
-
Performance test for a wide tree:
|
145
|
-
- 100 roots are created, each with a tree of up to 5 levels of nesting.
|
146
|
-
- The total tree creation time is measured, then the average time per tree is calculated.
|
147
|
-
- Results are printed to the console.
|
148
|
-
"""
|
149
|
-
|
150
|
-
def runTest(self):
|
151
|
-
print("\n=== WideTreePerformanceTest ===")
|
152
|
-
tree_times = []
|
153
|
-
try:
|
154
|
-
# For 100 trees
|
155
|
-
for root_index in range(1, 101):
|
156
|
-
tree_start = time.time()
|
157
|
-
root = Entity.objects.create(name=f"Wide Root {root_index}")
|
158
|
-
current_parent = root
|
159
|
-
# Create 5 levels of nesting for each root
|
160
|
-
for level in range(1, 6):
|
161
|
-
new_node = Entity.objects.create(
|
162
|
-
name=f"Node {root_index}-{level}", tn_parent=current_parent)
|
163
|
-
current_parent = new_node
|
164
|
-
tree_end = time.time()
|
165
|
-
tree_times.append(tree_end - tree_start)
|
166
|
-
|
167
|
-
avg_time = sum(tree_times) / len(tree_times)
|
168
|
-
print("Wide tree:")
|
169
|
-
print(
|
170
|
-
f"100 trees (each with 5 levels) created with an average creation time of: {avg_time:.6f} s")
|
171
|
-
except Exception as e:
|
172
|
-
print("WideTreePerformanceTest: exception:", e)
|
173
|
-
traceback.print_exc()
|
174
|
-
|
175
|
-
self.assertTrue(True)
|
176
|
-
|
177
|
-
|
178
|
-
class MassInsertionTest(TestCase):
|
179
|
-
"""
|
180
|
-
Mass insertion test:
|
181
|
-
- Inserts nodes from 50 to 75 using all three methods (save, create, alternative method).
|
182
|
-
- If an error occurs with any method, it is recorded but the test continues.
|
183
|
-
- A detailed report is printed at the end.
|
184
|
-
"""
|
185
|
-
|
186
|
-
def runTest(self):
|
187
|
-
print("\n=== MassInsertionTest ===")
|
188
|
-
errors = []
|
189
|
-
try:
|
190
|
-
for i in range(50, 76):
|
191
|
-
# 1. Insertion via save()
|
192
|
-
try:
|
193
|
-
node = Entity(name=f"Mass Node {i} via save()")
|
194
|
-
node.save()
|
195
|
-
except Exception as e:
|
196
|
-
errors.append(f"Error inserting node {i} via save(): {e}")
|
197
|
-
|
198
|
-
# 2. Insertion via objects.create()
|
199
|
-
try:
|
200
|
-
Entity.objects.create(name=f"Mass Node {i} via create()")
|
201
|
-
except Exception as e:
|
202
|
-
errors.append(f"Error inserting node {i} via create(): {e}")
|
203
|
-
|
204
|
-
# 3. Alternative method (insert_at or get_or_create)
|
205
|
-
try:
|
206
|
-
# Take the first node in the database or create a parent
|
207
|
-
parent = Entity.objects.first()
|
208
|
-
if not parent:
|
209
|
-
parent = Entity.objects.create(name="Default Parent")
|
210
|
-
|
211
|
-
if hasattr(Entity, 'insert_at'):
|
212
|
-
node = Entity(name=f"Mass Node {i} via insert_at()")
|
213
|
-
node.insert_at(parent)
|
214
|
-
elif hasattr(Entity.objects, 'get_or_create'):
|
215
|
-
Entity.objects.get_or_create(
|
216
|
-
name=f"Mass Node {i} via get_or_create()", defaults={'tn_parent': parent})
|
217
|
-
else:
|
218
|
-
errors.append(
|
219
|
-
f"No alternative insertion method found for node {i}.")
|
220
|
-
except Exception as e:
|
221
|
-
errors.append(
|
222
|
-
f"Error in alternative insertion for node {i}: {e}")
|
223
|
-
except Exception as e:
|
224
|
-
errors.append("General exception in MassInsertionTest: " + str(e))
|
225
|
-
|
226
|
-
if errors:
|
227
|
-
print("MassInsertionTest: FAILED. Errors detected:")
|
228
|
-
for err in errors:
|
229
|
-
print(" -", err)
|
230
|
-
else:
|
231
|
-
print("MassInsertionTest: PASSED.")
|
232
|
-
|
233
|
-
self.assertEqual(
|
234
|
-
len(errors), 0, "MassInsertionTest: errors - " + ", ".join(errors))
|
235
|
-
|
236
|
-
|
237
|
-
class NodeDeletionTest(TestCase):
|
238
|
-
"""
|
239
|
-
Tests node deletion:
|
240
|
-
- A parent and children are created.
|
241
|
-
- One of the children is deleted and it is verified that it is absent from the query.
|
242
|
-
- Then the parent is deleted and it is verified that cascading deletion worked (if provided).
|
243
|
-
"""
|
244
|
-
|
245
|
-
def runTest(self):
|
246
|
-
errors = []
|
247
|
-
print("\n=== NodeDeletionTest ===")
|
248
|
-
try:
|
249
|
-
parent = Entity.objects.create(name="Deletion Parent")
|
250
|
-
child1 = Entity.objects.create(name="Child 1", tn_parent=parent)
|
251
|
-
child2 = Entity.objects.create(name="Child 2", tn_parent=parent)
|
252
|
-
|
253
|
-
# Delete child1
|
254
|
-
child1.delete()
|
255
|
-
remaining_children = Entity.objects.filter(tn_parent=parent)
|
256
|
-
if child1 in remaining_children:
|
257
|
-
errors.append(
|
258
|
-
"The deleted child1 is still present among the parent's children.")
|
259
|
-
|
260
|
-
# Delete the parent and check cascading deletion
|
261
|
-
parent.delete()
|
262
|
-
if Entity.objects.filter(pk=child2.pk).exists():
|
263
|
-
errors.append(
|
264
|
-
"Child2 was not deleted after the parent was deleted (cascading deletion was expected).")
|
265
|
-
except Exception as e:
|
266
|
-
errors.append("Exception in NodeDeletionTest: " + str(e))
|
267
|
-
|
268
|
-
if errors:
|
269
|
-
print("NodeDeletionTest: FAILED. Errors detected:")
|
270
|
-
for err in errors:
|
271
|
-
print(" -", err)
|
272
|
-
else:
|
273
|
-
print("NodeDeletionTest: PASSED.")
|
274
|
-
|
275
|
-
self.assertEqual(
|
276
|
-
len(errors), 0, "NodeDeletionTest: errors - " + ", ".join(errors))
|
277
|
-
|
278
|
-
|
279
|
-
class NodeMovingTest(TestCase):
|
280
|
-
"""
|
281
|
-
Tests moving of nodes:
|
282
|
-
- Two parents are created.
|
283
|
-
- A node is moved from the first parent to the second.
|
284
|
-
- It is verified that the node appears in the new parent's children list and is absent from the old parent's list.
|
285
|
-
"""
|
286
|
-
|
287
|
-
def runTest(self):
|
288
|
-
errors = []
|
289
|
-
print("\n=== NodeMovingTest ===")
|
290
|
-
try:
|
291
|
-
parent1 = Entity.objects.create(name="Original Parent")
|
292
|
-
parent2 = Entity.objects.create(name="New Parent")
|
293
|
-
child = Entity.objects.create(
|
294
|
-
name="Movable Child", tn_parent=parent1)
|
295
|
-
|
296
|
-
# Moving the node
|
297
|
-
child.set_parent(parent2)
|
298
|
-
child.save()
|
299
|
-
|
300
|
-
children1 = Entity.objects.filter(tn_parent=parent1)
|
301
|
-
children2 = Entity.objects.filter(tn_parent=parent2)
|
302
|
-
if child in children1:
|
303
|
-
errors.append(
|
304
|
-
"The node is still present in the original parent's children after moving.")
|
305
|
-
if child not in children2:
|
306
|
-
errors.append(
|
307
|
-
"The node was not found among the new parent's children after moving.")
|
308
|
-
except Exception as e:
|
309
|
-
errors.append("Exception in NodeMovingTest: " + str(e))
|
310
|
-
|
311
|
-
if errors:
|
312
|
-
print("NodeMovingTest: FAILED. Errors detected:")
|
313
|
-
for err in errors:
|
314
|
-
print(" -", err)
|
315
|
-
else:
|
316
|
-
print("NodeMovingTest: PASSED.")
|
317
|
-
|
318
|
-
self.assertEqual(
|
319
|
-
len(errors), 0, "NodeMovingTest: errors - " + ", ".join(errors))
|
320
|
-
|
321
|
-
|
322
|
-
class NodeUpdateTest(TestCase):
|
323
|
-
"""
|
324
|
-
Tests node update:
|
325
|
-
- A node is created, then its attribute (e.g. name) is updated.
|
326
|
-
- It is verified that the change is saved in the database.
|
327
|
-
"""
|
328
|
-
|
329
|
-
def runTest(self):
|
330
|
-
errors = []
|
331
|
-
print("\n=== NodeUpdateTest ===")
|
332
|
-
try:
|
333
|
-
node = Entity.objects.create(name="Original Name")
|
334
|
-
node.name = "Updated Name"
|
335
|
-
node.save()
|
336
|
-
updated_node = Entity.objects.get(pk=node.pk)
|
337
|
-
if updated_node.name != "Updated Name":
|
338
|
-
errors.append("The node's name was not updated correctly.")
|
339
|
-
except Exception as e:
|
340
|
-
errors.append("Exception in NodeUpdateTest: " + str(e))
|
341
|
-
|
342
|
-
if errors:
|
343
|
-
print("NodeUpdateTest: FAILED. Errors detected:")
|
344
|
-
for err in errors:
|
345
|
-
print(" -", err)
|
346
|
-
else:
|
347
|
-
print("NodeUpdateTest: PASSED.")
|
348
|
-
|
349
|
-
self.assertEqual(
|
350
|
-
len(errors), 0, "NodeUpdateTest: errors - " + ", ".join(errors))
|
351
|
-
|
352
|
-
|
353
|
-
class DataIntegrityTest(TestCase):
|
354
|
-
"""
|
355
|
-
Tests data integrity:
|
356
|
-
- A small tree is created.
|
357
|
-
- Operations of moving, updating, and deletion are performed.
|
358
|
-
- The correctness of relationships between parents and children after the operations is verified.
|
359
|
-
"""
|
360
|
-
|
361
|
-
def runTest(self):
|
362
|
-
errors = []
|
363
|
-
print("\n=== DataIntegrityTest ===")
|
364
|
-
try:
|
365
|
-
root = Entity.objects.create(name="Integrity Root")
|
366
|
-
child1 = Entity.objects.create(
|
367
|
-
name="Integrity Child 1", tn_parent=root)
|
368
|
-
child2 = Entity.objects.create(
|
369
|
-
name="Integrity Child 2", tn_parent=root)
|
370
|
-
grandchild = Entity.objects.create(
|
371
|
-
name="Integrity Grandchild", tn_parent=child1)
|
372
|
-
|
373
|
-
# Move child2 under child1
|
374
|
-
child2.set_parent(child1)
|
375
|
-
|
376
|
-
# Update child1's name
|
377
|
-
child1.name = "Integrity Child 1 Updated"
|
378
|
-
child1.save()
|
379
|
-
|
380
|
-
# Delete grandchild
|
381
|
-
grandchild.delete()
|
382
|
-
|
383
|
-
# Verify relationships
|
384
|
-
root_children = Entity.objects.filter(tn_parent=root)
|
385
|
-
if child1 not in root_children:
|
386
|
-
errors.append(
|
387
|
-
"Child1 not found among the root node's children.")
|
388
|
-
if child2 in root_children:
|
389
|
-
errors.append(
|
390
|
-
"Child2 is present among the root node's children after moving.")
|
391
|
-
|
392
|
-
child1_children = Entity.objects.filter(tn_parent=child1)
|
393
|
-
if child2 not in child1_children:
|
394
|
-
errors.append(
|
395
|
-
"Child2 not found among child1's children after moving.")
|
396
|
-
|
397
|
-
if Entity.objects.filter(name="Integrity Grandchild").exists():
|
398
|
-
errors.append("Grandchild still exists after deletion.")
|
399
|
-
except Exception as e:
|
400
|
-
errors.append("Exception in DataIntegrityTest: " + str(e))
|
401
|
-
|
402
|
-
if errors:
|
403
|
-
print("DataIntegrityTest: FAILED. Errors detected:")
|
404
|
-
for err in errors:
|
405
|
-
print(" -", err)
|
406
|
-
else:
|
407
|
-
print("DataIntegrityTest: PASSED.")
|
408
|
-
|
409
|
-
self.assertEqual(
|
410
|
-
len(errors), 0, "DataIntegrityTest: errors - " + ", ".join(errors))
|
411
|
-
|
412
|
-
|
413
|
-
class LargeVolumeTest(TransactionTestCase):
|
414
|
-
"""
|
415
|
-
Test with large data volume:
|
416
|
-
- Performs mass insertion of 1000 nodes.
|
417
|
-
- Verifies that the number of inserted nodes matches the expected count.
|
418
|
-
"""
|
419
|
-
|
420
|
-
def runTest(self):
|
421
|
-
errors = []
|
422
|
-
print("\n=== LargeVolumeTest ===")
|
423
|
-
try:
|
424
|
-
initial_count = Entity.objects.count()
|
425
|
-
for i in range(1000):
|
426
|
-
try:
|
427
|
-
Entity.objects.create(name=f"Large Node {i}")
|
428
|
-
except Exception as e:
|
429
|
-
errors.append(
|
430
|
-
f"Error inserting node {i} in large data volume: {e}")
|
431
|
-
final_count = Entity.objects.count()
|
432
|
-
if final_count - initial_count != 1000:
|
433
|
-
errors.append(
|
434
|
-
f"Expected 1000 new nodes, but got {final_count - initial_count}.")
|
435
|
-
except Exception as e:
|
436
|
-
errors.append("Exception in LargeVolumeTest: " + str(e))
|
437
|
-
|
438
|
-
if errors:
|
439
|
-
print("LargeVolumeTest: FAILED. Errors detected:")
|
440
|
-
for err in errors:
|
441
|
-
print(" -", err)
|
442
|
-
else:
|
443
|
-
print("LargeVolumeTest: PASSED.")
|
444
|
-
|
445
|
-
self.assertEqual(
|
446
|
-
len(errors), 0, "LargeVolumeTest: errors - " + ", ".join(errors))
|
447
|
-
|
448
|
-
|
449
|
-
class InvalidDataTest(TestCase):
|
450
|
-
"""
|
451
|
-
Tests handling of invalid data:
|
452
|
-
- Attempts to create a node with None in a required field (e.g. name).
|
453
|
-
- Verifies that the expected exception is raised.
|
454
|
-
"""
|
455
|
-
|
456
|
-
def runTest(self):
|
457
|
-
errors = []
|
458
|
-
print("\n=== InvalidDataTest ===")
|
459
|
-
try:
|
460
|
-
try:
|
461
|
-
# Attempt to create a node with invalid data
|
462
|
-
Entity.objects.create(name=None)
|
463
|
-
errors.append(
|
464
|
-
"Creating a node with None for name did not raise an exception as expected.")
|
465
|
-
except Exception:
|
466
|
-
# Expected behavior: an exception should occur
|
467
|
-
pass
|
468
|
-
|
469
|
-
# Additionally, one can test creating a node with an empty string if that is not allowed
|
470
|
-
try:
|
471
|
-
node = Entity(name="")
|
472
|
-
node.save()
|
473
|
-
# If an empty string is allowed, then no error should be recorded
|
474
|
-
except Exception as e:
|
475
|
-
# If an exception occurs, that may also be considered acceptable
|
476
|
-
pass
|
477
|
-
except Exception as e:
|
478
|
-
errors.append("Exception in InvalidDataTest: " + str(e))
|
479
|
-
|
480
|
-
if errors:
|
481
|
-
print("InvalidDataTest: FAILED. Errors detected:")
|
482
|
-
for err in errors:
|
483
|
-
print(" -", err)
|
484
|
-
else:
|
485
|
-
print("InvalidDataTest: PASSED.")
|
486
|
-
|
487
|
-
self.assertEqual(
|
488
|
-
len(errors), 0, "InvalidDataTest: errors - " + ", ".join(errors))
|
File without changes
|
File without changes
|