django-fast-treenode 1.1.2__py3-none-any.whl → 2.0.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. {django_fast_treenode-1.1.2.dist-info → django_fast_treenode-2.0.0.dist-info}/METADATA +156 -44
  2. django_fast_treenode-2.0.0.dist-info/RECORD +41 -0
  3. {django_fast_treenode-1.1.2.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.2.dist-info/RECORD +0 -33
  35. treenode/compat.py +0 -8
  36. treenode/factory.py +0 -68
  37. treenode/models.py +0 -669
  38. treenode/static/select2tree/.gitkeep +0 -1
  39. treenode/static/select2tree/select2tree.css +0 -176
  40. treenode/static/select2tree/select2tree.js +0 -171
  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.2.dist-info → django_fast_treenode-2.0.0.dist-info}/LICENSE +0 -0
  49. {django_fast_treenode-1.1.2.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
+ }