django-fast-treenode 2.0.11__py3-none-any.whl → 2.1.1__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 (57) hide show
  1. {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.dist-info}/LICENSE +2 -2
  2. django_fast_treenode-2.1.1.dist-info/METADATA +158 -0
  3. django_fast_treenode-2.1.1.dist-info/RECORD +64 -0
  4. {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.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/forms.py +8 -10
  12. treenode/managers/__init__.py +21 -0
  13. treenode/managers/adjacency.py +203 -0
  14. treenode/managers/closure.py +278 -0
  15. treenode/models/__init__.py +2 -1
  16. treenode/models/adjacency.py +343 -0
  17. treenode/models/classproperty.py +3 -0
  18. treenode/models/closure.py +23 -24
  19. treenode/models/factory.py +12 -2
  20. treenode/models/mixins/__init__.py +23 -0
  21. treenode/models/mixins/ancestors.py +65 -0
  22. treenode/models/mixins/children.py +81 -0
  23. treenode/models/mixins/descendants.py +66 -0
  24. treenode/models/mixins/family.py +63 -0
  25. treenode/models/mixins/logical.py +68 -0
  26. treenode/models/mixins/node.py +210 -0
  27. treenode/models/mixins/properties.py +156 -0
  28. treenode/models/mixins/roots.py +96 -0
  29. treenode/models/mixins/siblings.py +99 -0
  30. treenode/models/mixins/tree.py +344 -0
  31. treenode/signals.py +26 -0
  32. treenode/static/treenode/css/tree_widget.css +201 -31
  33. treenode/static/treenode/css/treenode_admin.css +48 -41
  34. treenode/static/treenode/js/tree_widget.js +269 -131
  35. treenode/static/treenode/js/treenode_admin.js +131 -171
  36. treenode/templates/admin/tree_node_changelist.html +6 -0
  37. treenode/templates/admin/treenode_ajax_rows.html +7 -0
  38. treenode/tests/tests.py +488 -0
  39. treenode/urls.py +10 -6
  40. treenode/utils/__init__.py +2 -0
  41. treenode/utils/aid.py +46 -0
  42. treenode/utils/base16.py +38 -0
  43. treenode/utils/base36.py +3 -1
  44. treenode/utils/db.py +116 -0
  45. treenode/utils/exporter.py +2 -0
  46. treenode/utils/importer.py +0 -1
  47. treenode/utils/radix.py +61 -0
  48. treenode/version.py +2 -2
  49. treenode/views.py +118 -43
  50. treenode/widgets.py +91 -43
  51. django_fast_treenode-2.0.11.dist-info/METADATA +0 -698
  52. django_fast_treenode-2.0.11.dist-info/RECORD +0 -42
  53. treenode/admin.py +0 -439
  54. treenode/docs/Documentation +0 -636
  55. treenode/managers.py +0 -419
  56. treenode/models/proxy.py +0 -669
  57. {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.dist-info}/top_level.txt +0 -0
treenode/models/proxy.py DELETED
@@ -1,669 +0,0 @@
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.11
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
- ancestors_pks = self.get_ancestors_pks(include_self, depth)
135
- result = self._meta.model.objects.filter(pk__in=ancestors_pks)
136
- return result
137
-
138
- def get_ancestors(self, include_self=True, depth=None):
139
- """Get a list with all ancestors (ordered from root to self/parent)."""
140
- queryset = self.get_ancestors_queryset(include_self, depth)
141
- return list(queryset.iterator())
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
- pks = self.closure_model.get_ancestors_pks(self, include_self, depth)
150
- return pks
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
- descendants_pks = self.get_descendants_pks(include_self, depth)
176
- result = self._meta.model.objects.filter(pk__in=descendants_pks)
177
- return result
178
-
179
- def get_descendants(self, include_self=False, depth=None):
180
- """Get a list containing all descendants."""
181
- queryset = self.get_descendants_queryset(include_self, depth).iterator()
182
- return list(queryset)
183
-
184
- def get_descendants_count(self, include_self=False, depth=None):
185
- """Get the descendants count."""
186
- return self.get_descendants_queryset(include_self, depth).count()
187
-
188
- def get_descendants_pks(self, include_self=False, depth=None):
189
- """Get the descendants pks list."""
190
- pks = self.closure_model.get_descendants_pks(self, include_self, depth)
191
- return pks
192
-
193
- # Siblings --------------------
194
-
195
- @cached_method
196
- def get_siblings_queryset(self):
197
- """Get the siblings queryset with prefetch."""
198
- if self.tn_parent:
199
- qs = self.tn_parent.tn_children.prefetch_related('tn_children')
200
- else:
201
- qs = self._meta.model.objects.filter(tn_parent__isnull=True)
202
- return qs.exclude(pk=self.pk)
203
-
204
- def get_siblings(self):
205
- """Get a list with all the siblings."""
206
- return list(self.get_siblings_queryset())
207
-
208
- def get_siblings_count(self):
209
- """Get the siblings count."""
210
- return self.get_siblings_queryset().count()
211
-
212
- def get_siblings_pks(self):
213
- """Get the siblings pks list."""
214
- return [item.pk for item in self.get_siblings_queryset()]
215
-
216
- # -----------------------------
217
-
218
- def get_breadcrumbs(self, attr='pk'):
219
- """Get the breadcrumbs to current node (self, included)."""
220
- queryset = self.get_ancestors_queryset(include_self=True)
221
-
222
- breadcrumbs = [
223
- getattr(item, attr)
224
- if hasattr(item, attr) else None
225
- for item in queryset
226
- ]
227
- return breadcrumbs
228
-
229
- def get_depth(self):
230
- """Get the node depth (self, how many levels of descendants)."""
231
- return self.closure_model.get_depth(self)
232
-
233
- def get_first_child(self):
234
- """Get the first child node."""
235
- return self.get_children_queryset().first()
236
-
237
- @cached_method
238
- def get_index(self):
239
- """Get the node index (self, index in node.parent.children list)."""
240
- if self.tn_parent is None:
241
- return self.tn_priority
242
- source = list(self.tn_parent.tn_children.all())
243
- return source.index(self) if self in source else self.tn_priority
244
-
245
- def get_order(self):
246
- """Return the materialized path."""
247
- path = self.get_breadcrumbs(attr='tn_priority')
248
- segments = [to_base36(i).rjust(6, '0') for i in path]
249
- return ''.join(segments)
250
-
251
- def get_last_child(self):
252
- """Get the last child node."""
253
- return self.get_children_queryset().last()
254
-
255
- def get_level(self):
256
- """Get the node level (self, starting from 1)."""
257
- return self.closure_model.get_level(self)
258
-
259
- def get_path(self, prefix='', suffix='', delimiter='.', format_str=''):
260
- """Return Materialized Path of node."""
261
- priorities = self.get_breadcrumbs(attr='tn_priority')
262
- # Проверяем, что список не пуст
263
- if not priorities or all(p is None for p in priorities):
264
- return prefix + suffix
265
-
266
- str_ = "{%s}" % format_str
267
- path = delimiter.join([
268
- str_.format(p)
269
- for p in priorities
270
- if p is not None
271
- ])
272
- return prefix + path + suffix
273
-
274
- @cached_method
275
- def get_parent(self):
276
- """Get the parent node."""
277
- return self.tn_parent
278
-
279
- def set_parent(self, parent_obj):
280
- """Set the parent node."""
281
- self._meta.model.clear_cache()
282
- self.tn_parent = parent_obj
283
- self.save()
284
-
285
- def get_parent_pk(self):
286
- """Get the parent node pk."""
287
- return self.get_parent().pk if self.tn_parent else None
288
-
289
- @cached_method
290
- def get_priority(self):
291
- """Get the node priority."""
292
- return self.tn_priority
293
-
294
- def set_priority(self, priority=0):
295
- """Set the node priority."""
296
- self._meta.model.clear_cache()
297
- self.tn_priority = priority
298
- self.save()
299
-
300
- def get_root(self):
301
- """Get the root node for the current node."""
302
- return self.closure_model.get_root(self)
303
-
304
- def get_root_pk(self):
305
- """Get the root node pk for the current node."""
306
- root = self.get_root()
307
- return root.pk if root else None
308
-
309
- # Logics ----------------------
310
-
311
- def is_ancestor_of(self, target_obj):
312
- """Return True if the current node is ancestor of target_obj."""
313
- return self in target_obj.get_ancestors(include_self=False)
314
-
315
- def is_child_of(self, target_obj):
316
- """Return True if the current node is child of target_obj."""
317
- return self in target_obj.get_children()
318
-
319
- def is_descendant_of(self, target_obj):
320
- """Return True if the current node is descendant of target_obj."""
321
- return self in target_obj.get_descendants()
322
-
323
- def is_first_child(self):
324
- """Return True if the current node is the first child."""
325
- return self.tn_priority == 0
326
-
327
- def is_last_child(self):
328
- """Return True if the current node is the last child."""
329
- return self.tn_priority == self.get_siblings_count() - 1
330
-
331
- def is_leaf(self):
332
- """Return True if the current node is a leaf."""
333
- return self.tn_children.count() == 0
334
-
335
- def is_parent_of(self, target_obj):
336
- """Return True if the current node is parent of target_obj."""
337
- return self == target_obj.tn_parent
338
-
339
- def is_root(self):
340
- """Return True if the current node is root."""
341
- return self.tn_parent is None
342
-
343
- def is_root_of(self, target_obj):
344
- """Return True if the current node is root of target_obj."""
345
- return self == target_obj.get_root()
346
-
347
- def is_sibling_of(self, target_obj):
348
- """Return True if the current node is sibling of target_obj."""
349
- if target_obj.tn_parent is None and self.tn_parent is None:
350
- # Both objects are roots
351
- return True
352
- return (self.tn_parent == target_obj.tn_parent)
353
-
354
- def delete(self, cascade=True):
355
- """Delete node."""
356
- model = self._meta.model
357
-
358
- if not cascade:
359
- # Get a list of children
360
- children = self.get_children()
361
- # Move them to one level up
362
- for child in children:
363
- child.tn_parent = self.tn_parent
364
- # Udate both models in bulk
365
- model.objects.bulk_update(
366
- children,
367
- ("tn_parent",),
368
- batch_size=1000
369
- )
370
- # All descendants and related records in the ClosingModel will be
371
- # cleared by cascading the removal of ForeignKeys.
372
- super().delete()
373
- # Can be excluded. The cache has already been cleared by the manager.
374
- model.clear_cache()
375
-
376
- def save(self, force_insert=False, *args, **kwargs):
377
- """Save method."""
378
- # --- 1. Preparations -------------------------------------------------
379
- is_new = self.pk is None
380
- is_move = False
381
- old_parent = None
382
- old_priority = None
383
- model = self._meta.model
384
- closure_model = self.closure_model
385
-
386
- # --- 2. Check mode _-------------------------------------------------
387
- # If the object already exists in the DB, we'll extract its old parent
388
- if is_new:
389
- force_insert = True
390
- else:
391
- ql = model.objects.filter(pk=self.pk).values_list(
392
- 'tn_parent',
393
- 'tn_priority').first()
394
- old_parent = ql[0]
395
- old_priority = ql[1]
396
- is_move = old_priority != self.tn_priority
397
-
398
- # Check if we are moving the node into itself (child).
399
- # If old parent != self.tn_parent, "moving" is possible.
400
- if old_parent and old_parent != self.tn_parent:
401
- # Let's make sure we don't move into our descendant
402
- descendants = self.get_descendants_queryset()
403
- if self.tn_parent and self.tn_parent.pk in {
404
- d.pk for d in descendants}:
405
- raise ValueError("You cannot move a node into its own child.")
406
-
407
- # --- 3. Saving ------------------------------------------------------
408
- super().save(force_insert=force_insert, *args, **kwargs)
409
-
410
- # --- 4. Synchronization with Closure Model --------------------------
411
- if is_new:
412
- closure_model.insert_node(self)
413
-
414
- # If the parent has changed, we move it
415
- if (old_parent != self.tn_parent):
416
- subtree_nodes = self.get_descendants(include_self=True)
417
- self.closure_model.move_node(subtree_nodes)
418
-
419
- # --- 5. Update siblings ---------------------------------------------
420
- if is_new or is_move:
421
- # Now we need recalculate tn_priority
422
- self._update_priority()
423
- else:
424
- self._meta.model.clear_cache()
425
-
426
- # ---------------------------------------------------
427
- # Public properties
428
- #
429
- # All properties map a get_{{property}}() method.
430
- # ---------------------------------------------------
431
-
432
- @property
433
- def ancestors(self):
434
- """Get a list with all ancestors; self included."""
435
- return self.get_ancestors()
436
-
437
- @property
438
- def ancestors_count(self):
439
- """Get the ancestors count."""
440
- return self.get_ancestors_count()
441
-
442
- @property
443
- def ancestors_pks(self):
444
- """Get the ancestors pks list; self included."""
445
- return self.get_ancestors_pks()
446
-
447
- @property
448
- def breadcrumbs(self):
449
- """Get the breadcrumbs to current node (self, included)."""
450
- return self.get_breadcrumbs()
451
-
452
- @property
453
- def children(self):
454
- """Get a list containing all children; self included."""
455
- return self.get_children()
456
-
457
- @property
458
- def children_count(self):
459
- """Get the children count."""
460
- return self.get_children_count()
461
-
462
- @property
463
- def children_pks(self):
464
- """Get the children pks list."""
465
- return self.get_children_pks()
466
-
467
- @property
468
- def depth(self):
469
- """Get the node depth."""
470
- return self.get_depth()
471
-
472
- @property
473
- def descendants(self):
474
- """Get a list containing all descendants; self not included."""
475
- return self.get_descendants()
476
-
477
- @property
478
- def descendants_count(self):
479
- """Get the descendants count; self not included."""
480
- return self.get_descendants_count()
481
-
482
- @property
483
- def descendants_pks(self):
484
- """Get the descendants pks list; self not included."""
485
- return self.get_descendants_pks()
486
-
487
- @property
488
- def descendants_tree(self):
489
- """Get a n-dimensional dict representing the model tree."""
490
- return self.get_descendants_tree()
491
-
492
- @property
493
- def descendants_tree_display(self):
494
- """Get a multiline string representing the model tree."""
495
- return self.get_descendants_tree_display()
496
-
497
- @property
498
- def first_child(self):
499
- """Get the first child node."""
500
- return self.get_first_child()
501
-
502
- @property
503
- def index(self):
504
- """Get the node index."""
505
- return self.get_index()
506
-
507
- @property
508
- def last_child(self):
509
- """Get the last child node."""
510
- return self.get_last_child()
511
-
512
- @property
513
- def level(self):
514
- """Get the node level."""
515
- return self.get_level()
516
-
517
- @property
518
- def parent(self):
519
- """Get node parent."""
520
- return self.tn_parent
521
-
522
- @property
523
- def parent_pk(self):
524
- """Get node parent pk."""
525
- return self.get_parent_pk()
526
-
527
- @property
528
- def priority(self):
529
- """Get node priority."""
530
- return self.get_priority()
531
-
532
- @classproperty
533
- def roots(cls):
534
- """Get a list with all root nodes."""
535
- return cls.get_roots()
536
-
537
- @property
538
- def root(self):
539
- """Get the root node for the current node."""
540
- return self.get_root()
541
-
542
- @property
543
- def root_pk(self):
544
- """Get the root node pk for the current node."""
545
- return self.get_root_pk()
546
-
547
- @property
548
- def siblings(self):
549
- """Get a list with all the siblings."""
550
- return self.get_siblings()
551
-
552
- @property
553
- def siblings_count(self):
554
- """Get the siblings count."""
555
- return self.get_siblings_count()
556
-
557
- @property
558
- def siblings_pks(self):
559
- """Get the siblings pks list."""
560
- return self.get_siblings_pks()
561
-
562
- @classproperty
563
- def tree(cls):
564
- """Get an n-dimensional dict representing the model tree."""
565
- return cls.get_tree()
566
-
567
- @classproperty
568
- def tree_display(cls):
569
- """Get a multiline string representing the model tree."""
570
- return cls.get_tree_display()
571
-
572
- @property
573
- def tn_order(self):
574
- """Return the materialized path."""
575
- return self.get_order()
576
-
577
- # ---------------------------------------------------
578
- # Prived methods
579
- #
580
- # The usage of these methods is only allowed by developers. In future
581
- # versions, these methods may be changed or removed without any warning.
582
- # ---------------------------------------------------
583
-
584
- def _update_priority(self):
585
- """Update tn_priority field for siblings."""
586
- if self.tn_parent is None:
587
- # Node is a root
588
- parent = None
589
- queryset = self._meta.model.get_roots_queryset()
590
- else:
591
- # Node isn't a root
592
- parent = self.tn_parent
593
- queryset = parent.tn_children.all()
594
-
595
- siblings = list(queryset.exclude(pk=self.pk))
596
- sorted_siblings = sorted(siblings, key=lambda x: x.tn_priority)
597
- insert_pos = min(self.tn_priority, len(sorted_siblings))
598
- sorted_siblings.insert(insert_pos, self)
599
- for index, node in enumerate(sorted_siblings):
600
- node.tn_priority = index
601
- # Save changes
602
- model = self._meta.model
603
- with transaction.atomic():
604
- model.objects.bulk_update(sorted_siblings, ('tn_priority',))
605
- super().save(update_fields=['tn_priority'])
606
- model.clear_cache()
607
-
608
- def _object2dict(self, instance, exclude=None, visited=None):
609
- """
610
- Convert a class instance to a dictionary.
611
-
612
- :param instance: The object instance to convert.
613
- :param exclude: List of attribute names to exclude.
614
- :param visited: Set of visited objects to prevent circular references.
615
- :return: A dictionary representation of the object.
616
- """
617
- if exclude is None:
618
- exclude = set()
619
- if visited is None:
620
- visited = set()
621
-
622
- # Prevent infinite recursion by tracking visited objects
623
- if id(instance) in visited:
624
- raise RecursionError("Cycle detected in tree structure.")
625
-
626
- visited.add(id(instance))
627
-
628
- # Если объект не является моделью Django, просто вернуть его
629
- if not isinstance(instance, models.Model):
630
- return instance
631
-
632
- # If the object has no `__dict__`, return its direct value
633
- if not hasattr(instance, '__dict__'):
634
- return instance
635
-
636
- result = {}
637
-
638
- for key, value in vars(instance).items():
639
- if key.startswith('_') or key in exclude:
640
- continue
641
-
642
- # Recursively process nested objects
643
- if isinstance(value, (list, tuple, set)):
644
- result[key] = [
645
- self._object2dict(v, exclude, visited) for v in value
646
- ]
647
- elif isinstance(value, dict):
648
- result[key] = {
649
- k: self._object2dict(v, exclude, visited)
650
- for k, v in value.items()
651
- }
652
- else:
653
- result[key] = self._object2dict(value, exclude, visited)
654
-
655
- # Include children
656
- children = instance.tn_children.all()
657
- if children.exists():
658
- result['children'] = [
659
- self._object2dict(child, exclude, visited)
660
- for child in children
661
- ]
662
-
663
- # Add path information
664
- result['path'] = instance.get_path(format_str=':d')
665
-
666
- return result
667
-
668
-
669
- # The end