django-fast-treenode 1.1.2__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.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
treenode/models.py DELETED
@@ -1,669 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- TreeNode Models Module
4
-
5
- """
6
-
7
-
8
- from django.db import models
9
- from django.db import transaction
10
- from django.core.cache import caches
11
- from django.utils.translation import gettext_lazy as _
12
- from six import with_metaclass
13
- from . import classproperty
14
- from .compat import force_str
15
- from .factory import TreeFactory
16
- from .managers import TreeNodeManager
17
-
18
-
19
- treenode_cache = caches['treenode']
20
-
21
- def cached_tree_method(func):
22
- """
23
- Decorator to cache the results of tree methods
24
-
25
- The decorator caches the results of the decorated method using the
26
- closure_path of the node as part of the cache key. If the cache is
27
- cleared or invalidated, the cached results will be recomputed.
28
-
29
- Usage:
30
- @cached_tree_method
31
- def my_tree_method(self):
32
- # Tree method logic
33
- """
34
-
35
- def wrapper(self, *args, **kwargs):
36
- cache_key = f"{self.__class__.__name__}_{self.pk}_tree_{func.__name__}"
37
- result = treenode_cache.get(cache_key)
38
-
39
- if result is None:
40
- result = func(self, *args, **kwargs)
41
- treenode_cache.set(cache_key, result)
42
-
43
- return result
44
-
45
- return wrapper
46
-
47
-
48
- class TreeNodeModel(with_metaclass(TreeFactory, models.Model)):
49
-
50
- treenode_display_field = None
51
-
52
- tn_parent = models.ForeignKey(
53
- 'self',
54
- related_name='tn_children',
55
- on_delete=models.CASCADE,
56
- null=True,
57
- blank=True,
58
- verbose_name=_('Parent'),
59
- )
60
-
61
- tn_priority = models.PositiveIntegerField(
62
- default=0,
63
- verbose_name=_('Priority'),
64
- )
65
-
66
- objects = TreeNodeManager()
67
-
68
- class Meta:
69
- abstract = True
70
-
71
- def __str__(self):
72
- if self.treenode_display_field:
73
- return str(getattr(self, self.treenode_display_field))
74
- else:
75
- return 'Node %d' % self.pk
76
-
77
- # Public methods
78
-
79
- @classmethod
80
- def get_roots(cls):
81
- """Get a list with all root nodes"""
82
- return list(item for item in cls.get_roots_queryset())
83
-
84
- @classmethod
85
- @cached_tree_method
86
- def get_roots_queryset(cls):
87
- """Get root nodes queryset"""
88
- return cls.objects.filter(tn_parent=None)
89
-
90
- @classmethod
91
- @cached_tree_method
92
- def get_tree(cls, instance=None):
93
- """Get a n-dimensional dict representing the model tree"""
94
-
95
- objs_list = list(instance,) if instance else cls.get_roots()
96
- objs_tree = list()
97
- for item in objs_list:
98
- objs_tree.append(item.object2dict(item, []))
99
- return objs_tree
100
-
101
- @classmethod
102
- @cached_tree_method
103
- def get_tree_display(cls, cache=True):
104
- """Get a multiline string representing the model tree"""
105
-
106
- objs = list(cls.objects.all())
107
- return '\n'.join(['%s' % (obj,) for obj in objs])
108
-
109
- @classmethod
110
- def update_tree(cls):
111
- """Update tree manually, useful after bulk updates"""
112
-
113
- treenode_cache.clear()
114
-
115
- cls.closure_model.objects.all().delete()
116
-
117
- # Apparently later, you should think about using iterators to reduce
118
- # the amount of memory used.
119
- # I'm sorry, I don't have time for that right now.
120
-
121
- cls.closure_model.objects.bulk_create([
122
- cls.closure_model(
123
- parent_id=item.pk,
124
- child_id=item.pk,
125
- depth=0
126
- )
127
- for item in cls.objects.all()
128
- ])
129
-
130
- for node in cls.objects.all():
131
- queryset = cls.closure_model.objects.all()
132
- parents = queryset.filter(
133
- child=node.parent).values('parent', 'depth')
134
- children = queryset.filter(
135
- parent=node).values('child', 'depth')
136
- objects = [
137
- cls.closure_model(
138
- parent_id=p['parent'],
139
- child_id=c['child'],
140
- depth=p['depth'] + c['depth'] + 1
141
- )
142
- for p in parents
143
- for c in children
144
- ]
145
- cls.closure_model.objects.bulk_create(objects)
146
- cls._update_orders()
147
-
148
- @classmethod
149
- def delete_tree(cls):
150
- """Delete the whole tree for the current node class"""
151
- treenode_cache.clear()
152
- cls.closure_model.objects.all().delete()
153
- cls.objects.all().delete()
154
-
155
- def get_ancestors(self, include_self=True, depth=None):
156
- """Get a list with all ancestors (ordered from root to self/parent)"""
157
-
158
- options = dict(child_id=self.pk, depth__gte=0 if include_self else 1)
159
- if depth:
160
- options.update({'depth__lte': depth})
161
-
162
- qs = self._closure_model.objects.filter(**options).order_by('-depth')
163
-
164
- return list(item.parent for item in qs)
165
-
166
- def get_ancestors_count(self, include_self=True, depth=None):
167
- """Get the ancestors count"""
168
-
169
- return self.get_ancestors_queryset(include_self, depth).count()
170
-
171
- def get_ancestors_pks(self, include_self=True, depth=None):
172
- """Get the ancestors pks list"""
173
-
174
- options = dict(child_id=self.pk, depth__gte=0 if include_self else 1)
175
- if depth:
176
- options.update({'depth__lte': depth})
177
-
178
- qs = self._closure_model.objects.filter(**options).order_by('-depth')
179
-
180
- return list(item.parent.pk for item in qs)
181
-
182
- @cached_tree_method
183
- def get_ancestors_queryset(self, include_self=True, depth=None):
184
- """Get the ancestors queryset (self, ordered from parent to root)"""
185
-
186
- options = dict(child_id=self.pk, depth__gte=0 if include_self else 1)
187
- if depth:
188
- options.update({'depth__lte': depth})
189
-
190
- qs = self._closure_model.objects.filter(**options).order_by('-depth')
191
- select = list(item.parent.pk for item in qs)
192
- result = self._meta.model.objects.filter(pk__in=select)
193
- return result
194
-
195
- @cached_tree_method
196
- def get_breadcrumbs(self, attr=None):
197
- """Get the breadcrumbs to current node (self, included)"""
198
-
199
- qs = self._closure_model.objects.filter(child=self).order_by('-depth')
200
- if attr:
201
- return list(getattr(item.parent, attr) for item in qs)
202
- else:
203
- return list(item.parent for item in qs)
204
-
205
- def get_children(self):
206
- """Get a list containing all children"""
207
-
208
- return list(self.self.get_children_queryset())
209
-
210
- def get_children_count(self):
211
- """Get the children count"""
212
-
213
- return self.get_children_queryset().count()
214
-
215
- def get_children_pks(self):
216
- """Get the children pks list"""
217
-
218
- return [ch.pk for ch in self.get_children_queryset()]
219
-
220
- @cached_tree_method
221
- def get_children_queryset(self):
222
- """Get the children queryset"""
223
- return self._meta.model.objects.filter(tn_parent=self.id)
224
-
225
- def get_depth(self):
226
- """Get the node depth (self, how many levels of descendants)"""
227
-
228
- depths = self._closure_model.objects.filter(parent=self).values_list(
229
- 'depth',
230
- flat=True
231
- )
232
- return max(depths)
233
-
234
- def get_descendants(self, include_self=False, depth=None):
235
- """Get a list containing all descendants"""
236
- return list(self.get_descendants_queryset(include_self, depth))
237
-
238
- def get_descendants_count(self, include_self=False, depth=None):
239
- """Get the descendants count"""
240
- return self.get_descendants_queryset().count()
241
-
242
- def get_descendants_pks(self, include_self=False, depth=None):
243
- """Get the descendants pks list"""
244
- options = dict(parent_id=self.pk, depth__gte=0 if include_self else 1)
245
- if depth:
246
- options.update({'depth__lte': depth})
247
-
248
- qs = self._closure_model.objects.filter(**options)
249
- return [ch.child.pk for ch in qs] if qs else []
250
-
251
- @cached_tree_method
252
- def get_descendants_queryset(self, include_self=False, depth=None):
253
- """Get the descendants queryset"""
254
-
255
- pks = self.get_descendants_pks(include_self, depth)
256
- return self._meta.model.objects.filter(pk__in=pks)
257
-
258
- def get_descendants_tree(self):
259
- """Get a n-dimensional dict representing the model tree"""
260
-
261
- return self._meta.model.get_tree(instance=self)
262
-
263
- def get_descendants_tree_display(self, include_self=False, depth=None):
264
- """Get a multiline string representing the model tree"""
265
-
266
- objs = self.get_descendants()
267
- return '\n'.join(['%s' % (obj,) for obj in objs])
268
-
269
- def get_first_child(self):
270
- """Get the first child node"""
271
- return self.get_children_queryset().first()
272
-
273
- def get_index(self):
274
- """Get the node index (self, index in node.parent.children list)"""
275
- source = self.get_siblings()
276
- return source.index(self)
277
-
278
- def get_last_child(self):
279
- """Get the last child node"""
280
- return self.get_children_queryset().last()
281
-
282
- def get_level(self):
283
- """Get the node level (self, starting from 1)"""
284
-
285
- levels = self._closure_model.objects.filter(child=self).values_list(
286
- 'depth',
287
- flat=True
288
- )
289
- return max(levels) + 1
290
-
291
- def get_path(self, prefix='', suffix='', delimiter='.', format_str=''):
292
- """Return Materialized Path of node"""
293
-
294
- str_ = '{%s}' % format_str
295
- return prefix + delimiter.join(
296
- str_.format(i) for i in self.get_breadcrumbs(attr='tn_priority')
297
- ) + suffix
298
-
299
- def get_parent(self):
300
- """Get the parent node"""
301
- return self.tn_parent
302
-
303
- def get_parent_pk(self):
304
- """Get the parent node pk"""
305
- return self.tn_parent.pk if self.tn_parent else None
306
-
307
- def set_parent(self, parent_obj):
308
- """Set the parent node"""
309
- self.tn_parent = parent_obj
310
-
311
- def get_priority(self):
312
- return self.tn_priority
313
-
314
- def set_priority(self, priority=0):
315
- """Set the node priority"""
316
- self.tn_priority = priority
317
-
318
- def get_root(self):
319
- """Get the root node for the current node"""
320
- qs = self._closure_model.objects.filter(child=self).order_by('depth')
321
- return qs.last().parent if qs.count() > 0 else None
322
-
323
- def get_root_pk(self):
324
- """Get the root node pk for the current node"""
325
- self.get_root().pk
326
-
327
- def get_siblings(self):
328
- """Get a list with all the siblings"""
329
- return list(self.get_siblings_queryset())
330
-
331
- def get_siblings_count(self):
332
- """Get the siblings count"""
333
- return self.get_siblings_queryset().count()
334
-
335
- def get_siblings_pks(self):
336
- """Get the siblings pks list"""
337
- return [item.pk for item in self.get_siblings_queryset()]
338
-
339
- @cached_tree_method
340
- def get_siblings_queryset(self):
341
- """Get the siblings queryset"""
342
- if self.tn_parent:
343
- queryset = self.tn_parent.tn_children.all()
344
- else:
345
- queryset = self._meta.model.objects.filter(tn_parent__isnull=True)
346
- return queryset.exclude(pk=self.pk)
347
-
348
- def is_ancestor_of(self, target_obj):
349
- """Return True if the current node is ancestor of target_obj"""
350
- return self.pk in target_obj.get_ancestors_pks()
351
-
352
- def is_child_of(self, target_obj):
353
- """Return True if the current node is child of target_obj"""
354
-
355
- return self.pk in target_obj.get_children_pks()
356
-
357
- def is_descendant_of(self, target_obj):
358
- """Return True if the current node is descendant of target_obj"""
359
- return self.pk in target_obj.get_descendants_pks()
360
-
361
- def is_first_child(self):
362
- """Return True if the current node is the first child"""
363
- return self.tn_priority == 0
364
-
365
- def is_last_child(self):
366
- """Return True if the current node is the last child"""
367
-
368
- return self.tn_priority == self.get_siblings_count() - 1
369
-
370
- def is_leaf(self):
371
- """Return True if the current node is leaf (self, it has not children)"""
372
- return self.tn_children.count() == 0
373
-
374
- def is_parent_of(self, target_obj):
375
- """Return True if the current node is parent of target_obj"""
376
- return self == target_obj.tn_parent
377
-
378
- def is_root(self):
379
- """Return True if the current node is root"""
380
- return self.tn_parent is None
381
-
382
- def is_root_of(self, target_obj):
383
- """Return True if the current node is root of target_obj"""
384
- return self == target_obj.get_root()
385
-
386
- def is_sibling_of(self, target_obj):
387
- """Return True if the current node is sibling of target_obj"""
388
- return self in target_obj.tn_parent.tn_children.all()
389
-
390
- # I think this method is not needed.
391
- # Clearing entries in the Closure Table will happen automatically
392
- # via cascading deletion
393
-
394
- # def delete(self, cascade=True, *args, **kwargs):
395
- # """Delete a node if cascade=True (default behaviour), children and
396
- # descendants will be deleted too, otherwise children's parent will be
397
- # set to None (then children become roots)"""
398
-
399
- # pass
400
-
401
- # Public properties
402
- # All properties map a get_{{property}}() method.
403
-
404
- @property
405
- def ancestors(self):
406
- return self.get_ancestors()
407
-
408
- @property
409
- def ancestors_count(self):
410
- return self.get_ancestors_count()
411
-
412
- @property
413
- def ancestors_pks(self):
414
- return self.get_ancestors_pks()
415
-
416
- @property
417
- def breadcrumbs(self):
418
- return self.get_breadcrumbs()
419
-
420
- @property
421
- def children(self):
422
- return self.get_children()
423
-
424
- @property
425
- def children_count(self):
426
- return self.get_children_count()
427
-
428
- @property
429
- def children_pks(self):
430
- return self.get_children_pks()
431
-
432
- @property
433
- def depth(self):
434
- return self.get_depth()
435
-
436
- @property
437
- def descendants(self):
438
- return self.get_descendants()
439
-
440
- @property
441
- def descendants_count(self):
442
- return self.get_descendants_count()
443
-
444
- @property
445
- def descendants_pks(self):
446
- return self.get_descendants_pks()
447
-
448
- @property
449
- def descendants_tree(self):
450
- return self.get_descendants_tree()
451
-
452
- @property
453
- def descendants_tree_display(self):
454
- return self.get_descendants_tree_display()
455
-
456
- @property
457
- def first_child(self):
458
- return self.get_first_child()
459
-
460
- @property
461
- def index(self):
462
- return self.get_index()
463
-
464
- @property
465
- def last_child(self):
466
- return self.get_last_child()
467
-
468
- @property
469
- def level(self):
470
- return self.get_level()
471
-
472
- @property
473
- def parent(self):
474
- return self.tn_parent
475
-
476
- @property
477
- def parent_pk(self):
478
- return self.get_parent_pk()
479
-
480
- @property
481
- def priority(self):
482
- return self.get_priority()
483
-
484
- @classproperty
485
- def roots(cls):
486
- return cls.get_roots()
487
-
488
- @property
489
- def root(self):
490
- return self.get_root()
491
-
492
- @property
493
- def root_pk(self):
494
- return self.get_root_pk()
495
-
496
- @property
497
- def siblings(self):
498
- return self.get_siblings()
499
-
500
- @property
501
- def siblings_count(self):
502
- return self.get_siblings_count()
503
-
504
- @property
505
- def siblings_pks(self):
506
- return self.get_siblings_pks()
507
-
508
- @classproperty
509
- def tree(cls):
510
- return cls.get_tree()
511
-
512
- @classproperty
513
- def tree_display(cls):
514
- return cls.get_tree_display()
515
-
516
- # ----------------------------------------------------------------------
517
- # Private methods
518
- # The usage of these methods is only allowed by developers. In future
519
- # versions, these methods may be changed or removed without any warning.
520
-
521
- @property
522
- def _closure_model(self):
523
- return self._meta.model.closure_model
524
-
525
- @property
526
- def tn_order(self):
527
- path = self.get_breadcrumbs(attr='tn_priority')
528
- return ''.join(['{:0>6g}'.format(i) for i in path])
529
-
530
- @cached_tree_method
531
- def object2dict(self, instance, exclude=[]):
532
- """Convert Class Object to python dict"""
533
-
534
- result = dict()
535
-
536
- if not hasattr(instance, '__dict__'):
537
- return instance
538
-
539
- new_subdic = dict(vars(instance))
540
- for key, value in new_subdic.items():
541
- if key.startswith('_') or key in exclude:
542
- continue
543
- result.update({key: self.object2dict(value, exclude)})
544
-
545
- childs = instance.tn_children.all()
546
- if childs.count() > 0:
547
- result.update({
548
- 'children': [
549
- obj.object2dict(obj, exclude)
550
- for obj in childs.all()]
551
- })
552
- result.update({'path': instance.get_path(format_str=':d')})
553
- return result
554
-
555
- @cached_tree_method
556
- def get_display(self, indent=True, mark='— '):
557
- indentation = (mark * self.tn_ancestors_count) if indent else ''
558
- indentation = force_str(indentation)
559
- text = self.get_display_text()
560
- text = force_str(text)
561
- return indentation + text
562
-
563
- @cached_tree_method
564
- def get_display_text(self):
565
- """
566
- Gets the text that will be indented in `get_display` method.
567
- Returns the `treenode_display_field` value if specified,
568
- otherwise falls back on the model's pk.
569
- Override this method to return another field or a computed value. #27
570
- """
571
- text = ''
572
- if (hasattr(self, 'treenode_display_field') and
573
- self.treenode_display_field is not None):
574
- field_name = getattr(self, 'treenode_display_field')
575
- text = getattr(self, field_name, '')
576
- if not text and self.pk:
577
- text = self.pk
578
- return force_str(text)
579
-
580
- @transaction.atomic
581
- def _insert(self):
582
- """Adds a new entry to the Adjacency Table and the Closure Table"""
583
-
584
- treenode_cache.clear()
585
-
586
- instance = self._closure_model.objects.create(
587
- parent=self,
588
- child=self,
589
- depth=0
590
- )
591
- instance.save()
592
-
593
- qs = self._closure_model.objects.all()
594
- parents = qs.filter(child=self.tn_parent).values('parent', 'depth')
595
- children = qs.filter(parent=self).values('child', 'depth')
596
- objects = [
597
- self._closure_model(
598
- parent_id=p['parent'],
599
- child_id=c['child'],
600
- depth=p['depth'] + c['depth'] + 1
601
- )
602
- for p in parents
603
- for c in children
604
- ]
605
- self._closure_model.objects.bulk_create(objects)
606
-
607
- @transaction.atomic
608
- def _move_to(self, old_parent):
609
- treenode_cache.clear()
610
-
611
- target = self.tn_parent
612
- qs = self._closure_model.objects.all()
613
- subtree = qs.filter(parent=self).values('child', 'depth')
614
- supertree = qs.filter(child=target).values('parent', 'depth')
615
-
616
- # Step 1. Delete
617
- subtree_pks = [node.child.pk for node in qs.filter(parent=self)]
618
- qs.filter(child_id__in=subtree_pks).exclude(
619
- parent_id__in=subtree_pks).delete()
620
-
621
- # Step 2. Insert
622
- objects = [
623
- self._closure_model(
624
- parent_id=p['parent'],
625
- child_id=c['child'],
626
- depth=p['depth'] + c['depth'] + 1
627
- )
628
- for p in supertree
629
- for c in subtree
630
- ]
631
- self._closure_model.objects.bulk_create(objects)
632
-
633
- def _order(self):
634
-
635
- treenode_cache.clear()
636
-
637
- queryset = self.get_siblings_queryset()
638
-
639
- if self.tn_priority > queryset.count():
640
- self.tn_priority = queryset.count()
641
-
642
- siblings = list(node for node in queryset)
643
- sorted_siblings = sorted(siblings, key=lambda x: x.tn_priority)
644
- sorted_siblings.insert(self.tn_priority, self)
645
-
646
- for index in range(len(sorted_siblings)):
647
- sorted_siblings[index].tn_priority = index
648
-
649
- self._meta.model.objects.bulk_update(
650
- sorted_siblings, ('tn_priority', ))
651
-
652
- def save(self, force_insert=False, *args, **kwargs):
653
- treenode_cache.clear()
654
-
655
- try:
656
- old = self._meta.model.objects.get(pk=self.pk)
657
- old_parent = old.tn_parent
658
- except self._meta.model.DoesNotExist:
659
- force_insert = True
660
-
661
- super().save(*args, **kwargs)
662
- self._order()
663
-
664
- if force_insert:
665
- self._insert()
666
- elif old_parent != self.tn_parent:
667
- self._move_to(old_parent)
668
-
669
- # The end
@@ -1 +0,0 @@
1
-