django-fast-treenode 1.1.3__py3-none-any.whl → 2.0.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 (50) hide show
  1. {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.0.dist-info}/METADATA +156 -46
  2. django_fast_treenode-2.0.0.dist-info/RECORD +41 -0
  3. {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.0.dist-info}/WHEEL +1 -1
  4. treenode/__init__.py +0 -7
  5. treenode/admin.py +327 -82
  6. treenode/apps.py +20 -3
  7. treenode/cache.py +231 -0
  8. treenode/docs/Documentation +130 -54
  9. treenode/forms.py +75 -19
  10. treenode/managers.py +260 -48
  11. treenode/models/__init__.py +7 -0
  12. treenode/models/classproperty.py +24 -0
  13. treenode/models/closure.py +168 -0
  14. treenode/models/factory.py +71 -0
  15. treenode/models/proxy.py +650 -0
  16. treenode/static/treenode/css/tree_widget.css +62 -0
  17. treenode/static/treenode/css/treenode_admin.css +106 -0
  18. treenode/static/treenode/js/tree_widget.js +161 -0
  19. treenode/static/treenode/js/treenode_admin.js +171 -0
  20. treenode/templates/admin/export_success.html +26 -0
  21. treenode/templates/admin/tree_node_changelist.html +11 -0
  22. treenode/templates/admin/tree_node_export.html +27 -0
  23. treenode/templates/admin/tree_node_import.html +27 -0
  24. treenode/templates/widgets/tree_widget.css +23 -0
  25. treenode/templates/widgets/tree_widget.html +21 -0
  26. treenode/urls.py +34 -0
  27. treenode/utils/__init__.py +4 -0
  28. treenode/utils/base36.py +35 -0
  29. treenode/utils/exporter.py +141 -0
  30. treenode/utils/importer.py +296 -0
  31. treenode/version.py +11 -1
  32. treenode/views.py +102 -2
  33. treenode/widgets.py +49 -27
  34. django_fast_treenode-1.1.3.dist-info/RECORD +0 -33
  35. treenode/compat.py +0 -8
  36. treenode/factory.py +0 -68
  37. treenode/models.py +0 -668
  38. treenode/static/select2tree/.gitkeep +0 -1
  39. treenode/static/select2tree/select2tree.css +0 -176
  40. treenode/static/select2tree/select2tree.js +0 -181
  41. treenode/static/treenode/css/treenode.css +0 -85
  42. treenode/static/treenode/js/treenode.js +0 -201
  43. treenode/templates/widgets/.gitkeep +0 -1
  44. treenode/templates/widgets/attrs.html +0 -7
  45. treenode/templates/widgets/options.html +0 -1
  46. treenode/templates/widgets/select2tree.html +0 -22
  47. treenode/tests.py +0 -3
  48. {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.0.dist-info}/LICENSE +0 -0
  49. {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.0.dist-info}/top_level.txt +0 -0
  50. /treenode/{docs → templates/admin}/.gitkeep +0 -0
@@ -0,0 +1,650 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ TreeNode Proxy Model
4
+
5
+ This module defines an abstract base model `TreeNodeModel` that
6
+ implements hierarchical data storage using the Adjacency Table method.
7
+ It integrates with a Closure Table for optimized tree operations.
8
+
9
+ Features:
10
+ - Supports adjacency list representation with parent-child relationships.
11
+ - Integrates with a Closure Table for efficient ancestor and descendant
12
+ queries.
13
+ - Provides a caching mechanism for performance optimization.
14
+ - Includes methods for tree traversal, manipulation, and serialization.
15
+
16
+ Version: 2.0.0
17
+ Author: Timur Kady
18
+ Email: timurkady@yandex.com
19
+ """
20
+
21
+
22
+ # proxy.py
23
+
24
+ from django.db import models, transaction
25
+
26
+ from .factory import TreeFactory
27
+ from .classproperty import classproperty
28
+ from ..utils.base36 import to_base36
29
+ from ..managers import TreeNodeModelManager
30
+ from ..cache import cached_method, treenode_cache
31
+ import logging
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ class TreeNodeModel(models.Model, metaclass=TreeFactory):
37
+ """
38
+ Abstract TreeNode Model.
39
+
40
+ Implements hierarchy storage using the Adjacency Table method.
41
+ To increase performance, it has an additional attribute - a model
42
+ that stores data from the Adjacency Table in the form of
43
+ a Closure Table.
44
+ """
45
+
46
+ treenode_display_field = None
47
+ closure_model = None
48
+
49
+ tn_parent = models.ForeignKey(
50
+ 'self',
51
+ related_name='tn_children',
52
+ on_delete=models.CASCADE,
53
+ null=True,
54
+ blank=True
55
+ )
56
+
57
+ tn_priority = models.PositiveIntegerField(default=0)
58
+
59
+ objects = TreeNodeModelManager()
60
+
61
+ class Meta:
62
+ """Meta Class."""
63
+
64
+ abstract = True
65
+
66
+ def __str__(self):
67
+ """Display information about a class object."""
68
+ if self.treenode_display_field:
69
+ return str(getattr(self, self.treenode_display_field))
70
+ else:
71
+ return 'Node %d' % self.pk
72
+
73
+ # ---------------------------------------------------
74
+ # Public methods
75
+ # ---------------------------------------------------
76
+
77
+ @classmethod
78
+ def clear_cache(cls):
79
+ """Clear cache for this model only."""
80
+ treenode_cache.invalidate(cls._meta.label)
81
+
82
+ @classmethod
83
+ def get_closure_model(cls):
84
+ """Return ClosureModel for class."""
85
+ return cls.closure_model
86
+
87
+ @classmethod
88
+ def get_roots(cls):
89
+ """Get a list with all root nodes."""
90
+ qs = cls.get_roots_queryset()
91
+ return list(item for item in qs)
92
+
93
+ @classmethod
94
+ @cached_method
95
+ def get_roots_queryset(cls):
96
+ """Get root nodes queryset with preloaded children."""
97
+ qs = cls.objects.filter(tn_parent=None).prefetch_related('tn_children')
98
+ return qs
99
+
100
+ @classmethod
101
+ def get_tree(cls, instance=None):
102
+ """Get an n-dimensional dict representing the model tree."""
103
+ objs_list = [instance] if instance else cls.get_roots()
104
+ return [item._object2dict(item, []) for item in objs_list]
105
+
106
+ @classmethod
107
+ @cached_method
108
+ def get_tree_display(cls):
109
+ """Get a multiline string representing the model tree."""
110
+ objs = list(cls.objects.all())
111
+ return '\n'.join(['%s' % (obj,) for obj in objs])
112
+
113
+ @classmethod
114
+ @transaction.atomic
115
+ def update_tree(cls):
116
+ """Rebuilds the closure table."""
117
+ # Clear cache
118
+ cls.closure_model.delete_all()
119
+ objs = list(cls.objects.all())
120
+ cls.closure_model.objects.bulk_create(objs, batch_size=1000)
121
+ cls.clear_cache()
122
+
123
+ @classmethod
124
+ def delete_tree(cls):
125
+ """Delete the whole tree for the current node class."""
126
+ cls.clear_cache()
127
+ cls.objects.all().delete()
128
+ cls.closure_model.delete_all()
129
+
130
+ # Ancestors -------------------
131
+
132
+ def get_ancestors_queryset(self, include_self=True, depth=None):
133
+ """Get the ancestors queryset (ordered from parent to root)."""
134
+ return self.closure_model.get_ancestors_queryset(
135
+ self, include_self, depth)
136
+
137
+ def get_ancestors(self, include_self=True, depth=None):
138
+ """Get a list with all ancestors (ordered from root to self/parent)."""
139
+ return list(
140
+ self.get_ancestors_queryset(include_self, depth).iterator()
141
+ )
142
+
143
+ def get_ancestors_count(self, include_self=True, depth=None):
144
+ """Get the ancestors count."""
145
+ return self.get_ancestors_queryset(include_self, depth).count()
146
+
147
+ def get_ancestors_pks(self, include_self=True, depth=None):
148
+ """Get the ancestors pks list."""
149
+ qs = self.get_ancestors_queryset(include_self, depth).only('pk')
150
+ return [ch.pk for ch in qs] if qs else []
151
+
152
+ # Children --------------------
153
+
154
+ @cached_method
155
+ def get_children_queryset(self):
156
+ """Get the children queryset with prefetch."""
157
+ return self.tn_children.prefetch_related('tn_children')
158
+
159
+ def get_children(self):
160
+ """Get a list containing all children."""
161
+ return list(self.get_children_queryset().iterator())
162
+
163
+ def get_children_count(self):
164
+ """Get the children count."""
165
+ return self.get_children_queryset().count()
166
+
167
+ def get_children_pks(self):
168
+ """Get the children pks list."""
169
+ return [ch.pk for ch in self.get_children_queryset().only('pk')]
170
+
171
+ # Descendants -----------------
172
+
173
+ def get_descendants_queryset(self, include_self=False, depth=None):
174
+ """Get the descendants queryset."""
175
+ return self.closure_model.get_descendants_queryset(
176
+ self, include_self, depth
177
+ )
178
+
179
+ def get_descendants(self, include_self=False, depth=None):
180
+ """Get a list containing all descendants."""
181
+ return list(
182
+ self.get_descendants_queryset(include_self, depth).iterator()
183
+ )
184
+
185
+ def get_descendants_count(self, include_self=False, depth=None):
186
+ """Get the descendants count."""
187
+ return self.get_descendants_queryset(include_self, depth).count()
188
+
189
+ def get_descendants_pks(self, include_self=False, depth=None):
190
+ """Get the descendants pks list."""
191
+ qs = self.get_descendants_queryset(include_self, depth)
192
+ return [ch.pk for ch in qs] if qs else []
193
+
194
+ # Siblings --------------------
195
+
196
+ @cached_method
197
+ def get_siblings_queryset(self):
198
+ """Get the siblings queryset with prefetch."""
199
+ if self.tn_parent:
200
+ qs = self.tn_parent.tn_children.prefetch_related('tn_children')
201
+ else:
202
+ qs = self._meta.model.objects.filter(tn_parent__isnull=True)
203
+ return qs.exclude(pk=self.pk)
204
+
205
+ def get_siblings(self):
206
+ """Get a list with all the siblings."""
207
+ return list(self.get_siblings_queryset())
208
+
209
+ def get_siblings_count(self):
210
+ """Get the siblings count."""
211
+ return self.get_siblings_queryset().count()
212
+
213
+ def get_siblings_pks(self):
214
+ """Get the siblings pks list."""
215
+ return [item.pk for item in self.get_siblings_queryset()]
216
+
217
+ # -----------------------------
218
+
219
+ def get_breadcrumbs(self, attr=None):
220
+ """Get the breadcrumbs to current node (self, included)."""
221
+ return self.closure_model.get_breadcrumbs(self, attr)
222
+
223
+ def get_depth(self):
224
+ """Get the node depth (self, how many levels of descendants)."""
225
+ return self.closure_model.get_depth(self)
226
+
227
+ def get_first_child(self):
228
+ """Get the first child node."""
229
+ return self.get_children_queryset().first()
230
+
231
+ @cached_method
232
+ def get_index(self):
233
+ """Get the node index (self, index in node.parent.children list)."""
234
+ if self.tn_parent is None:
235
+ return self.tn_priority
236
+ source = list(self.tn_parent.tn_children.all())
237
+ return source.index(self) if self in source else self.tn_priority
238
+
239
+ def get_order(self):
240
+ """Return the materialized path."""
241
+ path = self.closure_model.get_breadcrumbs(self, attr='tn_priority')
242
+ segments = [to_base36(i).rjust(6, '0') for i in path]
243
+ return ''.join(segments)
244
+
245
+ def get_last_child(self):
246
+ """Get the last child node."""
247
+ return self.get_children_queryset().last()
248
+
249
+ def get_level(self):
250
+ """Get the node level (self, starting from 1)."""
251
+ return self.closure_model.get_level(self)
252
+
253
+ def get_path(self, prefix='', suffix='', delimiter='.', format_str=''):
254
+ """Return Materialized Path of node."""
255
+ path = self.closure_model.get_path(self, delimiter, format_str)
256
+ return prefix+path+suffix
257
+
258
+ @cached_method
259
+ def get_parent(self):
260
+ """Get the parent node."""
261
+ return self.tn_parent
262
+
263
+ def set_parent(self, parent_obj):
264
+ """Set the parent node."""
265
+ self._meta.model.clear_cache()
266
+ self.tn_parent = parent_obj
267
+ self.save()
268
+
269
+ def get_parent_pk(self):
270
+ """Get the parent node pk."""
271
+ return self.get_parent().pk if self.tn_parent else None
272
+
273
+ @cached_method
274
+ def get_priority(self):
275
+ """Get the node priority."""
276
+ return self.tn_priority
277
+
278
+ def set_priority(self, priority=0):
279
+ """Set the node priority."""
280
+ self._meta.model.clear_cache()
281
+ self.tn_priority = priority
282
+ self.save()
283
+
284
+ def get_root(self):
285
+ """Get the root node for the current node."""
286
+ return self.closure_model.get_root(self)
287
+
288
+ def get_root_pk(self):
289
+ """Get the root node pk for the current node."""
290
+ root = self.get_root()
291
+ return root.pk if root else None
292
+
293
+ # Logics ----------------------
294
+
295
+ def is_ancestor_of(self, target_obj):
296
+ """Return True if the current node is ancestor of target_obj."""
297
+ return self in target_obj.get_ancestors(include_self=False)
298
+
299
+ def is_child_of(self, target_obj):
300
+ """Return True if the current node is child of target_obj."""
301
+ return self in target_obj.get_children()
302
+
303
+ def is_descendant_of(self, target_obj):
304
+ """Return True if the current node is descendant of target_obj."""
305
+ return self in target_obj.get_descendants()
306
+
307
+ def is_first_child(self):
308
+ """Return True if the current node is the first child."""
309
+ return self.tn_priority == 0
310
+
311
+ def is_last_child(self):
312
+ """Return True if the current node is the last child."""
313
+ return self.tn_priority == self.get_siblings_count() - 1
314
+
315
+ def is_leaf(self):
316
+ """Return True if the current node is a leaf."""
317
+ return self.tn_children.count() == 0
318
+
319
+ def is_parent_of(self, target_obj):
320
+ """Return True if the current node is parent of target_obj."""
321
+ return self == target_obj.tn_parent
322
+
323
+ def is_root(self):
324
+ """Return True if the current node is root."""
325
+ return self.tn_parent is None
326
+
327
+ def is_root_of(self, target_obj):
328
+ """Return True if the current node is root of target_obj."""
329
+ return self == target_obj.get_root()
330
+
331
+ def is_sibling_of(self, target_obj):
332
+ """Return True if the current node is sibling of target_obj."""
333
+ if target_obj.tn_parent is None and self.tn_parent is None:
334
+ # Both objects are roots
335
+ return True
336
+ return (self.tn_parent == target_obj.tn_parent)
337
+
338
+ def delete(self, cascade=True):
339
+ """Delete node."""
340
+ model = self._meta.model
341
+
342
+ if not cascade:
343
+ # Get a list of children
344
+ children = self.get_children()
345
+ # Move them to one level up
346
+ for child in children:
347
+ child.tn_parent = self.tn_parent
348
+ # Udate both models in bulk
349
+ model.objects.bulk_update(
350
+ children,
351
+ ("tn_parent",),
352
+ batch_size=1000
353
+ )
354
+ # All descendants and related records in the ClosingModel will be
355
+ # cleared by cascading the removal of ForeignKeys.
356
+ super().delete()
357
+ # Can be excluded. The cache has already been cleared by the manager.
358
+ model.clear_cache()
359
+
360
+ def save(self, force_insert=False, *args, **kwargs):
361
+ """Save method."""
362
+ # --- 1. Preparations -------------------------------------------------
363
+ is_new = self.pk is None
364
+ is_move = False
365
+ old_parent = None
366
+ old_priority = None
367
+ model = self._meta.model
368
+ closure_model = self.closure_model
369
+
370
+ # --- 2. Check mode _-------------------------------------------------
371
+ # If the object already exists in the DB, we'll extract its old parent
372
+ if not is_new:
373
+ ql = model.objects.filter(pk=self.pk).values_list(
374
+ 'tn_parent',
375
+ 'tn_priority').first()
376
+ old_parent = ql[0]
377
+ old_priority = ql[1]
378
+ is_move = old_priority != self.tn_priority
379
+
380
+ # Check if we are moving the node into itself (child).
381
+ # If old parent != self.tn_parent, "moving" is possible.
382
+ if old_parent and old_parent != self.tn_parent:
383
+ # Let's make sure we don't move into our descendant
384
+ descendants = self.get_descendants_queryset()
385
+ if self.tn_parent and self.tn_parent.pk in {
386
+ d.pk for d in descendants}:
387
+ raise ValueError("You cannot move a node into its own child.")
388
+
389
+ # --- 3. Saving ------------------------------------------------------
390
+ super().save(force_insert=force_insert, *args, **kwargs)
391
+
392
+ # --- 4. Synchronization with Closure Model --------------------------
393
+ if is_new:
394
+ closure_model.insert_node(self)
395
+
396
+ # If the parent has changed, we move it
397
+ if (old_parent != self.tn_parent):
398
+ closure_model.move_node(self)
399
+
400
+ # --- 5. Update siblings ---------------------------------------------
401
+ if is_new or is_move:
402
+ # Now we need recalculate tn_priority
403
+ self._update_priority()
404
+ else:
405
+ self._meta.model.clear_cache()
406
+
407
+ # ---------------------------------------------------
408
+ # Public properties
409
+ #
410
+ # All properties map a get_{{property}}() method.
411
+ # ---------------------------------------------------
412
+
413
+ @property
414
+ def ancestors(self):
415
+ """Get a list with all ancestors; self included."""
416
+ return self.get_ancestors()
417
+
418
+ @property
419
+ def ancestors_count(self):
420
+ """Get the ancestors count."""
421
+ return self.get_ancestors_count()
422
+
423
+ @property
424
+ def ancestors_pks(self):
425
+ """Get the ancestors pks list; self included."""
426
+ return self.get_ancestors_pks()
427
+
428
+ @property
429
+ def breadcrumbs(self):
430
+ """Get the breadcrumbs to current node (self, included)."""
431
+ return self.get_breadcrumbs()
432
+
433
+ @property
434
+ def children(self):
435
+ """Get a list containing all children; self included."""
436
+ return self.get_children()
437
+
438
+ @property
439
+ def children_count(self):
440
+ """Get the children count."""
441
+ return self.get_children_count()
442
+
443
+ @property
444
+ def children_pks(self):
445
+ """Get the children pks list."""
446
+ return self.get_children_pks()
447
+
448
+ @property
449
+ def depth(self):
450
+ """Get the node depth."""
451
+ return self.get_depth()
452
+
453
+ @property
454
+ def descendants(self):
455
+ """Get a list containing all descendants; self not included."""
456
+ return self.get_descendants()
457
+
458
+ @property
459
+ def descendants_count(self):
460
+ """Get the descendants count; self not included."""
461
+ return self.get_descendants_count()
462
+
463
+ @property
464
+ def descendants_pks(self):
465
+ """Get the descendants pks list; self not included."""
466
+ return self.get_descendants_pks()
467
+
468
+ @property
469
+ def descendants_tree(self):
470
+ """Get a n-dimensional dict representing the model tree."""
471
+ return self.get_descendants_tree()
472
+
473
+ @property
474
+ def descendants_tree_display(self):
475
+ """Get a multiline string representing the model tree."""
476
+ return self.get_descendants_tree_display()
477
+
478
+ @property
479
+ def first_child(self):
480
+ """Get the first child node."""
481
+ return self.get_first_child()
482
+
483
+ @property
484
+ def index(self):
485
+ """Get the node index."""
486
+ return self.get_index()
487
+
488
+ @property
489
+ def last_child(self):
490
+ """Get the last child node."""
491
+ return self.get_last_child()
492
+
493
+ @property
494
+ def level(self):
495
+ """Get the node level."""
496
+ return self.get_level()
497
+
498
+ @property
499
+ def parent(self):
500
+ """Get node parent."""
501
+ return self.tn_parent
502
+
503
+ @property
504
+ def parent_pk(self):
505
+ """Get node parent pk."""
506
+ return self.get_parent_pk()
507
+
508
+ @property
509
+ def priority(self):
510
+ """Get node priority."""
511
+ return self.get_priority()
512
+
513
+ @classproperty
514
+ def roots(cls):
515
+ """Get a list with all root nodes."""
516
+ return cls.get_roots()
517
+
518
+ @property
519
+ def root(self):
520
+ """Get the root node for the current node."""
521
+ return self.get_root()
522
+
523
+ @property
524
+ def root_pk(self):
525
+ """Get the root node pk for the current node."""
526
+ return self.get_root_pk()
527
+
528
+ @property
529
+ def siblings(self):
530
+ """Get a list with all the siblings."""
531
+ return self.get_siblings()
532
+
533
+ @property
534
+ def siblings_count(self):
535
+ """Get the siblings count."""
536
+ return self.get_siblings_count()
537
+
538
+ @property
539
+ def siblings_pks(self):
540
+ """Get the siblings pks list."""
541
+ return self.get_siblings_pks()
542
+
543
+ @classproperty
544
+ def tree(cls):
545
+ """Get an n-dimensional dict representing the model tree."""
546
+ return cls.get_tree()
547
+
548
+ @classproperty
549
+ def tree_display(cls):
550
+ """Get a multiline string representing the model tree."""
551
+ return cls.get_tree_display()
552
+
553
+ @property
554
+ def tn_order(self):
555
+ """Return the materialized path."""
556
+ return self.get_order()
557
+
558
+ # ---------------------------------------------------
559
+ # Prived methods
560
+ #
561
+ # The usage of these methods is only allowed by developers. In future
562
+ # versions, these methods may be changed or removed without any warning.
563
+ # ---------------------------------------------------
564
+
565
+ def _update_priority(self):
566
+ """Update tn_priority field for siblings."""
567
+ if self.tn_parent is None:
568
+ # Node is a root
569
+ parent = None
570
+ queryset = self._meta.model.get_roots_queryset()
571
+ else:
572
+ # Node isn't a root
573
+ parent = self.tn_parent
574
+ queryset = parent.tn_children.all()
575
+
576
+ siblings = list(queryset.exclude(pk=self.pk))
577
+ sorted_siblings = sorted(siblings, key=lambda x: x.tn_priority)
578
+ insert_pos = min(self.tn_priority, len(sorted_siblings))
579
+ sorted_siblings.insert(insert_pos, self)
580
+ for index, node in enumerate(sorted_siblings):
581
+ node.tn_priority = index
582
+ # Save changes
583
+ model = self._meta.model
584
+ with transaction.atomic():
585
+ model.objects.bulk_update(sorted_siblings, ('tn_priority',), 1000)
586
+ super().save(update_fields=['tn_priority'])
587
+ model.clear_cache()
588
+
589
+ def _object2dict(self, instance, exclude=None, visited=None):
590
+ """
591
+ Convert a class instance to a dictionary.
592
+
593
+ :param instance: The object instance to convert.
594
+ :param exclude: List of attribute names to exclude.
595
+ :param visited: Set of visited objects to prevent circular references.
596
+ :return: A dictionary representation of the object.
597
+ """
598
+ if exclude is None:
599
+ exclude = set()
600
+ if visited is None:
601
+ visited = set()
602
+
603
+ # Prevent infinite recursion by tracking visited objects
604
+ if id(instance) in visited:
605
+ raise RecursionError("Cycle detected in tree structure.")
606
+
607
+ visited.add(id(instance))
608
+
609
+ # Если объект не является моделью Django, просто вернуть его
610
+ if not isinstance(instance, models.Model):
611
+ return instance
612
+
613
+ # If the object has no `__dict__`, return its direct value
614
+ if not hasattr(instance, '__dict__'):
615
+ return instance
616
+
617
+ result = {}
618
+
619
+ for key, value in vars(instance).items():
620
+ if key.startswith('_') or key in exclude:
621
+ continue
622
+
623
+ # Recursively process nested objects
624
+ if isinstance(value, (list, tuple, set)):
625
+ result[key] = [
626
+ self._object2dict(v, exclude, visited) for v in value
627
+ ]
628
+ elif isinstance(value, dict):
629
+ result[key] = {
630
+ k: self._object2dict(v, exclude, visited)
631
+ for k, v in value.items()
632
+ }
633
+ else:
634
+ result[key] = self._object2dict(value, exclude, visited)
635
+
636
+ # Include children
637
+ children = instance.tn_children.all()
638
+ if children.exists():
639
+ result['children'] = [
640
+ self._object2dict(child, exclude, visited)
641
+ for child in children
642
+ ]
643
+
644
+ # Add path information
645
+ result['path'] = instance.get_path(format_str=':d')
646
+
647
+ return result
648
+
649
+
650
+ # The end
@@ -0,0 +1,62 @@
1
+ /*
2
+ TreeNode Select2 Widget Stylesheet
3
+
4
+ This stylesheet customizes the Select2 dropdown widget for hierarchical
5
+ data representation in Django admin. It ensures compatibility with both
6
+ light and dark themes.
7
+
8
+ Features:
9
+ - Dark theme styling for the dropdown and selected items.
10
+ - Consistent color scheme for better readability.
11
+ - Custom styling for search fields and selection indicators.
12
+ - Enhances usability in tree-based data selection.
13
+
14
+ Version: 2.0.0
15
+ Author: Timur Kady
16
+ Email: timurkady@yandex.com
17
+ */
18
+
19
+ /* Select2 dropdown */
20
+ .select2-dropdown.dark-theme {
21
+ background-color: #2c2c2c !important;
22
+ border: 1px solid #444 !important;
23
+ color: #ddd !important;
24
+ }
25
+
26
+ /* List of options */
27
+ .select2-dropdown.dark-theme .select2-results__option {
28
+ background-color: transparent !important;
29
+ color: #ddd !important;
30
+ }
31
+
32
+ /* Hover/selected option */
33
+ .select2-dropdown.dark-theme .select2-results__option--highlighted,
34
+ .select2-dropdown.dark-theme .select2-results__option--selected {
35
+ background-color: #555 !important;
36
+ color: #fff !important;
37
+ }
38
+
39
+ /* Search field */
40
+ .select2-dropdown.dark-theme .select2-search.select2-search--dropdown .select2-search__field {
41
+ background-color: #2c2c2c !important;
42
+ color: #ddd !important;
43
+ border: 1px solid #444 !important;
44
+ outline: none !important;
45
+ }
46
+
47
+ /* Container of the selected element (if we want to darken it too) */
48
+ .select2-container--default .select2-selection--single.dark-theme {
49
+ background-color: #2c2c2c !important;
50
+ border: 1px solid #444 !important;
51
+ color: #ddd !important;
52
+ }
53
+
54
+ /* Arrow */
55
+ .select2-container--default .select2-selection--single.dark-theme .select2-selection__arrow b {
56
+ border-color: #ddd transparent transparent transparent !important;
57
+ }
58
+
59
+ /* Text of the selected element */
60
+ .select2-container--default .select2-selection--single.dark-theme .select2-selection__rendered {
61
+ color: #ddd !important;
62
+ }