django-fast-treenode 2.1.1__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.
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: django-fast-treenode
3
- Version: 2.1.1
3
+ Version: 2.1.3
4
4
  Summary: Application for supporting tree (hierarchical) data structure in Django projects
5
- Home-page: https://github.com/TimurKady/django-fast-treenode
5
+ Home-page: https://django-fast-treenode.readthedocs.io/
6
6
  Author: Timur Kady
7
7
  Author-email: Timur Kady <timurkady@yandex.com>
8
8
  License: MIT License
@@ -28,7 +28,7 @@ License: MIT License
28
28
  SOFTWARE.
29
29
 
30
30
  Project-URL: Homepage, https://github.com/TimurKady/django-fast-treenode
31
- Project-URL: Documentation, https://django-fast-treenode.readthedocs.io/latest/
31
+ Project-URL: Documentation, https://django-fast-treenode.readthedocs.io/
32
32
  Project-URL: Source, https://github.com/TimurKady/django-fast-treenode
33
33
  Project-URL: Issues, https://github.com/TimurKady/django-fast-treenode/issues
34
34
  Classifier: Development Status :: 5 - Production/Stable
@@ -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
+ [![Tests](https://github.com/TimurKady/django-fast-treenode/actions/workflows/test.yaml/badge.svg?branch=main)](https://github.com/TimurKady/django-fast-treenode/actions/workflows/test.yaml)
67
+ [![Docs](https://readthedocs.org/projects/django-fast-treenode/badge/?version=latest)](https://django-fast-treenode.readthedocs.io/)
68
+ [![PyPI](https://img.shields.io/pypi/v/django-fast-treenode.svg)](https://pypi.org/project/django-fast-treenode/)
69
+ [![Published on Django Packages](https://img.shields.io/badge/Published%20on-Django%20Packages-0c3c26)](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
 
@@ -136,16 +140,16 @@ python manage.py migrate
136
140
  ```
137
141
 
138
142
  ## Documentation
139
- Full documentation is available at [this link](https://django-fast-treenode.readthedocs.io/latest/).
143
+ Full documentation is available at **[ReadTheDocs](https://django-fast-treenode.readthedocs.io/)**.
140
144
 
141
145
  Quick access links:
142
- * [Installation, configuration and fine tuning](https://django-fast-treenode.readthedocs.io/latest/installation/)
143
- * [Model Inheritance and Extensions](https://django-fast-treenode.readthedocs.io/latest/models/)
144
- * [Working with Admin Classes](https://django-fast-treenode.readthedocs.io/latest/admin/)
145
- * [API Reference](https://django-fast-treenode.readthedocs.io/latest/api/)
146
- * [Import & Export](https://django-fast-treenode.readthedocs.io/latest/import_export/)
147
- * [Caching and working with cache](https://django-fast-treenode.readthedocs.io/latest/cache/)
148
- * [Migration and upgrade guide](https://django-fast-treenode.readthedocs.io/latest/migration/)
146
+ * [Installation, configuration and fine tuning](https://django-fast-treenode.readthedocs.io/installation/)
147
+ * [Model Inheritance and Extensions](https://django-fast-treenode.readthedocs.io/models/)
148
+ * [Working with Admin Classes](https://django-fast-treenode.readthedocs.io/admin/)
149
+ * [API Reference](https://django-fast-treenode.readthedocs.io/api/)
150
+ * [Import & Export](https://django-fast-treenode.readthedocs.io/import_export/)
151
+ * [Caching and working with cache](https://django-fast-treenode.readthedocs.io/cache/)
152
+ * [Migration and upgrade guide](https://django-fast-treenode.readthedocs.io/migration/)
149
153
 
150
154
  Your wishes, objections, comments are welcome.
151
155
 
@@ -1,10 +1,10 @@
1
- treenode/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1
+ treenode/__init__.py,sha256=CZ-0uZyhi8OZJP8xmqbTqXrZUjcQ4SNAXhKFgM0qw2M,99
2
2
  treenode/apps.py,sha256=a7UasXiZZudPccjmHEudP79TkhR_53Mvnb-dBXLHRRQ,862
3
- treenode/cache.py,sha256=utTmMJ87fjbutaKbOcSp8bHqIDbI_Yr2nPXJoZLAqlQ,7213
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=JWmJNiw3wLkpTwygbmlmO1XucR3NTB_QQbdATOp71ug,222
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=E-eUgZoqKlyaCaDk0H7VQg6tDnfeufyGHn7hjTxIDqM,7694
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.1.dist-info/LICENSE,sha256=T0evsb7y-63fg18ovdNSx3wwWWRwyluQvN9J4zFSvfE,1093
61
- django_fast_treenode-2.1.1.dist-info/METADATA,sha256=E1ThWhY4yy_h8_28imc4CYkd4kR7e9aCvtvpa6Pyncs,7620
62
- django_fast_treenode-2.1.1.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
63
- django_fast_treenode-2.1.1.dist-info/top_level.txt,sha256=fmgxHbXyx1O2MPi_9kjx8aL9L-8TmV0gre4Go8XgqFk,9
64
- django_fast_treenode-2.1.1.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.2)
2
+ Generator: setuptools (76.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
treenode/__init__.py CHANGED
@@ -0,0 +1,5 @@
1
+ """
2
+ Django Fast TreeNode.
3
+
4
+ 📖 Documentation: https://django-fast-treenode.readthedocs.io/
5
+ """
treenode/cache.py CHANGED
@@ -12,7 +12,7 @@ Features:
12
12
  - Automatic cache eviction when memory limits are exceeded.
13
13
  - Decorator `@cached_method` for caching method results.
14
14
 
15
- Version: 2.0.0
15
+ Version: 2.1.0
16
16
  Author: Timur Kady
17
17
  Email: timurkady@yandex.com
18
18
  """
@@ -23,7 +23,7 @@ class TreeNodeNodeMixin(models.Model):
23
23
  abstract = True
24
24
 
25
25
  @cached_method
26
- def get_breadcrumbs(self, attr='pk'):
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
@@ -4,10 +4,10 @@ TreeNode Version Module
4
4
 
5
5
  This module defines the current version of the TreeNode package.
6
6
 
7
- Version: 2.1.0
7
+ Version: 2.1.2
8
8
  Author: Timur Kady
9
9
  Email: timurkady@yandex.com
10
10
  """
11
11
 
12
12
 
13
- __version__ = '2.1.0'
13
+ __version__ = '2.1.3'
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))