django-fast-treenode 2.0.10__py3-none-any.whl → 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/LICENSE +2 -2
  2. django_fast_treenode-2.1.0.dist-info/METADATA +161 -0
  3. django_fast_treenode-2.1.0.dist-info/RECORD +75 -0
  4. {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/WHEEL +1 -1
  5. treenode/admin/__init__.py +9 -0
  6. treenode/admin/admin.py +295 -0
  7. treenode/admin/changelist.py +65 -0
  8. treenode/admin/mixins.py +302 -0
  9. treenode/apps.py +12 -1
  10. treenode/cache.py +2 -2
  11. treenode/docs/.gitignore +0 -0
  12. treenode/docs/about.md +36 -0
  13. treenode/docs/admin.md +104 -0
  14. treenode/docs/api.md +739 -0
  15. treenode/docs/cache.md +187 -0
  16. treenode/docs/import_export.md +35 -0
  17. treenode/docs/index.md +30 -0
  18. treenode/docs/installation.md +74 -0
  19. treenode/docs/migration.md +145 -0
  20. treenode/docs/models.md +128 -0
  21. treenode/docs/roadmap.md +45 -0
  22. treenode/forms.py +33 -22
  23. treenode/managers/__init__.py +21 -0
  24. treenode/managers/adjacency.py +203 -0
  25. treenode/managers/closure.py +278 -0
  26. treenode/models/__init__.py +2 -1
  27. treenode/models/adjacency.py +343 -0
  28. treenode/models/classproperty.py +3 -0
  29. treenode/models/closure.py +39 -65
  30. treenode/models/factory.py +12 -2
  31. treenode/models/mixins/__init__.py +23 -0
  32. treenode/models/mixins/ancestors.py +65 -0
  33. treenode/models/mixins/children.py +81 -0
  34. treenode/models/mixins/descendants.py +66 -0
  35. treenode/models/mixins/family.py +63 -0
  36. treenode/models/mixins/logical.py +68 -0
  37. treenode/models/mixins/node.py +210 -0
  38. treenode/models/mixins/properties.py +156 -0
  39. treenode/models/mixins/roots.py +96 -0
  40. treenode/models/mixins/siblings.py +99 -0
  41. treenode/models/mixins/tree.py +344 -0
  42. treenode/signals.py +26 -0
  43. treenode/static/treenode/css/tree_widget.css +201 -31
  44. treenode/static/treenode/css/treenode_admin.css +48 -41
  45. treenode/static/treenode/js/tree_widget.js +269 -131
  46. treenode/static/treenode/js/treenode_admin.js +131 -171
  47. treenode/templates/admin/tree_node_changelist.html +6 -0
  48. treenode/templates/admin/tree_node_import.html +27 -9
  49. treenode/templates/admin/tree_node_import_report.html +32 -0
  50. treenode/templates/admin/treenode_ajax_rows.html +7 -0
  51. treenode/tests/tests.py +488 -0
  52. treenode/urls.py +10 -6
  53. treenode/utils/__init__.py +2 -0
  54. treenode/utils/aid.py +46 -0
  55. treenode/utils/base16.py +38 -0
  56. treenode/utils/base36.py +3 -1
  57. treenode/utils/db.py +116 -0
  58. treenode/utils/exporter.py +63 -36
  59. treenode/utils/importer.py +168 -161
  60. treenode/utils/radix.py +61 -0
  61. treenode/version.py +2 -2
  62. treenode/views.py +119 -38
  63. treenode/widgets.py +104 -40
  64. django_fast_treenode-2.0.10.dist-info/METADATA +0 -698
  65. django_fast_treenode-2.0.10.dist-info/RECORD +0 -41
  66. treenode/admin.py +0 -396
  67. treenode/docs/Documentation +0 -664
  68. treenode/managers.py +0 -281
  69. treenode/models/proxy.py +0 -650
  70. {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,488 @@
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))
treenode/urls.py CHANGED
@@ -8,17 +8,17 @@ autocomplete and retrieving child node counts.
8
8
 
9
9
  Routes:
10
10
  - `tree-autocomplete/`: Returns JSON data for Select2 hierarchical selection.
11
- - `get-children-count/`: Retrieves the number of children for a given
11
+ - `get-children-count/`: Retrieves the number of children for a given
12
12
  parent node.
13
13
 
14
- Version: 2.0.0
14
+ Version: 2.1.0
15
15
  Author: Timur Kady
16
16
  Email: timurkady@yandex.com
17
17
  """
18
18
 
19
19
 
20
20
  from django.urls import path
21
- from .views import TreeNodeAutocompleteView, GetChildrenCountView
21
+ from .views import TreeNodeAutocompleteView, ChildrenView
22
22
 
23
23
  urlpatterns = [
24
24
  path(
@@ -26,9 +26,13 @@ urlpatterns = [
26
26
  TreeNodeAutocompleteView.as_view(),
27
27
  name="tree_autocomplete"
28
28
  ),
29
+
29
30
  path(
30
- "get-children-count/",
31
- GetChildrenCountView.as_view(),
32
- name="get_children_count"
31
+ "tree-children/",
32
+ ChildrenView.as_view(),
33
+ name="tree_children"
33
34
  ),
34
35
  ]
36
+
37
+
38
+ # The End
@@ -11,3 +11,5 @@ if extra:
11
11
  __all__ = ["TreeNodeExporter", "TreeNodeImporter"]
12
12
  else:
13
13
  __all__ = []
14
+
15
+ # The End
treenode/utils/aid.py ADDED
@@ -0,0 +1,46 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Aid Utility Module
4
+
5
+ This module provides various helper functions.
6
+
7
+ Version: 2.1.0
8
+ Author: Timur Kady
9
+ Email: timurkady@yandex.com
10
+ """
11
+
12
+
13
+ from django.utils.safestring import mark_safe
14
+
15
+
16
+ def object_to_content(obj):
17
+ """Convert object data to widget options string."""
18
+ level = obj.get_depth()
19
+ icon = "📄 " if obj.is_leaf() else "📁 "
20
+ obj_str = str(obj)
21
+ content = (
22
+ f'<span class="treenode-option" style="padding-left: {level * 1.5}em;">'
23
+ f'{icon}{obj_str}</span>'
24
+ )
25
+ return mark_safe(content)
26
+
27
+
28
+ def to_base16(num):
29
+ """
30
+ Convert an integer to a base16 string.
31
+
32
+ For example: 10 -> 'A', 11 -> 'B', etc.
33
+ """
34
+ digits = "0123456789ABCDEF"
35
+
36
+ if num == 0:
37
+ return '0'
38
+ sign = '-' if num < 0 else ''
39
+ num = abs(num)
40
+ result = []
41
+ while num:
42
+ num, rem = divmod(num, 16)
43
+ result.append(digits[rem])
44
+ return sign + ''.join(reversed(result))
45
+
46
+ # The End
@@ -0,0 +1,38 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Base16 Utility Module
4
+
5
+ This module provides a utility function for converting integers
6
+ to Base16 string representation.
7
+
8
+ Features:
9
+ - Converts integers into a more compact Base36 format.
10
+ - Maintains lexicographic order when padded with leading zeros.
11
+ - Supports negative numbers.
12
+
13
+ Version: 2.1.0
14
+ Author: Timur Kady
15
+ Email: timurkady@yandex.com
16
+ """
17
+
18
+
19
+ def to_base16(num):
20
+ """
21
+ Convert an integer to a base16 string.
22
+
23
+ For example: 10 -> 'A', 11 -> 'B', etc.
24
+ """
25
+ digits = "0123456789ABCDEF"
26
+
27
+ if num == 0:
28
+ return '0'
29
+ sign = '-' if num < 0 else ''
30
+ num = abs(num)
31
+ result = []
32
+ while num:
33
+ num, rem = divmod(num, 16)
34
+ result.append(digits[rem])
35
+ return sign + ''.join(reversed(result))
36
+
37
+ # The End
38
+
treenode/utils/base36.py CHANGED
@@ -10,7 +10,7 @@ Features:
10
10
  - Maintains lexicographic order when padded with leading zeros.
11
11
  - Supports negative numbers.
12
12
 
13
- Version: 2.0.0
13
+ Version: 2.1.0
14
14
  Author: Timur Kady
15
15
  Email: timurkady@yandex.com
16
16
  """
@@ -33,3 +33,5 @@ def to_base36(num):
33
33
  num, rem = divmod(num, 36)
34
34
  result.append(digits[rem])
35
35
  return sign + ''.join(reversed(result))
36
+
37
+ # The End