django-fast-treenode 1.1.3__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.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
treenode/models.py DELETED
@@ -1,668 +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
-
147
- @classmethod
148
- def delete_tree(cls):
149
- """Delete the whole tree for the current node class"""
150
- treenode_cache.clear()
151
- cls.closure_model.objects.all().delete()
152
- cls.objects.all().delete()
153
-
154
- def get_ancestors(self, include_self=True, depth=None):
155
- """Get a list with all ancestors (ordered from root to self/parent)"""
156
-
157
- options = dict(child_id=self.pk, depth__gte=0 if include_self else 1)
158
- if depth:
159
- options.update({'depth__lte': depth})
160
-
161
- qs = self._closure_model.objects.filter(**options).order_by('-depth')
162
-
163
- return list(item.parent for item in qs)
164
-
165
- def get_ancestors_count(self, include_self=True, depth=None):
166
- """Get the ancestors count"""
167
-
168
- return self.get_ancestors_queryset(include_self, depth).count()
169
-
170
- def get_ancestors_pks(self, include_self=True, depth=None):
171
- """Get the ancestors pks list"""
172
-
173
- options = dict(child_id=self.pk, depth__gte=0 if include_self else 1)
174
- if depth:
175
- options.update({'depth__lte': depth})
176
-
177
- qs = self._closure_model.objects.filter(**options).order_by('-depth')
178
-
179
- return list(item.parent.pk for item in qs)
180
-
181
- @cached_tree_method
182
- def get_ancestors_queryset(self, include_self=True, depth=None):
183
- """Get the ancestors queryset (self, ordered from parent to root)"""
184
-
185
- options = dict(child_id=self.pk, depth__gte=0 if include_self else 1)
186
- if depth:
187
- options.update({'depth__lte': depth})
188
-
189
- qs = self._closure_model.objects.filter(**options).order_by('-depth')
190
- select = list(item.parent.pk for item in qs)
191
- result = self._meta.model.objects.filter(pk__in=select)
192
- return result
193
-
194
- @cached_tree_method
195
- def get_breadcrumbs(self, attr=None):
196
- """Get the breadcrumbs to current node (self, included)"""
197
-
198
- qs = self._closure_model.objects.filter(child=self).order_by('-depth')
199
- if attr:
200
- return list(getattr(item.parent, attr) for item in qs)
201
- else:
202
- return list(item.parent for item in qs)
203
-
204
- def get_children(self):
205
- """Get a list containing all children"""
206
-
207
- return list(self.self.get_children_queryset())
208
-
209
- def get_children_count(self):
210
- """Get the children count"""
211
-
212
- return self.get_children_queryset().count()
213
-
214
- def get_children_pks(self):
215
- """Get the children pks list"""
216
-
217
- return [ch.pk for ch in self.get_children_queryset()]
218
-
219
- @cached_tree_method
220
- def get_children_queryset(self):
221
- """Get the children queryset"""
222
- return self._meta.model.objects.filter(tn_parent=self.id)
223
-
224
- def get_depth(self):
225
- """Get the node depth (self, how many levels of descendants)"""
226
-
227
- depths = self._closure_model.objects.filter(parent=self).values_list(
228
- 'depth',
229
- flat=True
230
- )
231
- return max(depths)
232
-
233
- def get_descendants(self, include_self=False, depth=None):
234
- """Get a list containing all descendants"""
235
- return list(self.get_descendants_queryset(include_self, depth))
236
-
237
- def get_descendants_count(self, include_self=False, depth=None):
238
- """Get the descendants count"""
239
- return self.get_descendants_queryset().count()
240
-
241
- def get_descendants_pks(self, include_self=False, depth=None):
242
- """Get the descendants pks list"""
243
- options = dict(parent_id=self.pk, depth__gte=0 if include_self else 1)
244
- if depth:
245
- options.update({'depth__lte': depth})
246
-
247
- qs = self._closure_model.objects.filter(**options)
248
- return [ch.child.pk for ch in qs] if qs else []
249
-
250
- @cached_tree_method
251
- def get_descendants_queryset(self, include_self=False, depth=None):
252
- """Get the descendants queryset"""
253
-
254
- pks = self.get_descendants_pks(include_self, depth)
255
- return self._meta.model.objects.filter(pk__in=pks)
256
-
257
- def get_descendants_tree(self):
258
- """Get a n-dimensional dict representing the model tree"""
259
-
260
- return self._meta.model.get_tree(instance=self)
261
-
262
- def get_descendants_tree_display(self, include_self=False, depth=None):
263
- """Get a multiline string representing the model tree"""
264
-
265
- objs = self.get_descendants()
266
- return '\n'.join(['%s' % (obj,) for obj in objs])
267
-
268
- def get_first_child(self):
269
- """Get the first child node"""
270
- return self.get_children_queryset().first()
271
-
272
- def get_index(self):
273
- """Get the node index (self, index in node.parent.children list)"""
274
- source = self.get_siblings()
275
- return source.index(self)
276
-
277
- def get_last_child(self):
278
- """Get the last child node"""
279
- return self.get_children_queryset().last()
280
-
281
- def get_level(self):
282
- """Get the node level (self, starting from 1)"""
283
-
284
- levels = self._closure_model.objects.filter(child=self).values_list(
285
- 'depth',
286
- flat=True
287
- )
288
- return max(levels) + 1
289
-
290
- def get_path(self, prefix='', suffix='', delimiter='.', format_str=''):
291
- """Return Materialized Path of node"""
292
-
293
- str_ = '{%s}' % format_str
294
- return prefix + delimiter.join(
295
- str_.format(i) for i in self.get_breadcrumbs(attr='tn_priority')
296
- ) + suffix
297
-
298
- def get_parent(self):
299
- """Get the parent node"""
300
- return self.tn_parent
301
-
302
- def get_parent_pk(self):
303
- """Get the parent node pk"""
304
- return self.tn_parent.pk if self.tn_parent else None
305
-
306
- def set_parent(self, parent_obj):
307
- """Set the parent node"""
308
- self.tn_parent = parent_obj
309
-
310
- def get_priority(self):
311
- return self.tn_priority
312
-
313
- def set_priority(self, priority=0):
314
- """Set the node priority"""
315
- self.tn_priority = priority
316
-
317
- def get_root(self):
318
- """Get the root node for the current node"""
319
- qs = self._closure_model.objects.filter(child=self).order_by('depth')
320
- return qs.last().parent if qs.count() > 0 else None
321
-
322
- def get_root_pk(self):
323
- """Get the root node pk for the current node"""
324
- self.get_root().pk
325
-
326
- def get_siblings(self):
327
- """Get a list with all the siblings"""
328
- return list(self.get_siblings_queryset())
329
-
330
- def get_siblings_count(self):
331
- """Get the siblings count"""
332
- return self.get_siblings_queryset().count()
333
-
334
- def get_siblings_pks(self):
335
- """Get the siblings pks list"""
336
- return [item.pk for item in self.get_siblings_queryset()]
337
-
338
- @cached_tree_method
339
- def get_siblings_queryset(self):
340
- """Get the siblings queryset"""
341
- if self.tn_parent:
342
- queryset = self.tn_parent.tn_children.all()
343
- else:
344
- queryset = self._meta.model.objects.filter(tn_parent__isnull=True)
345
- return queryset.exclude(pk=self.pk)
346
-
347
- def is_ancestor_of(self, target_obj):
348
- """Return True if the current node is ancestor of target_obj"""
349
- return self.pk in target_obj.get_ancestors_pks()
350
-
351
- def is_child_of(self, target_obj):
352
- """Return True if the current node is child of target_obj"""
353
-
354
- return self.pk in target_obj.get_children_pks()
355
-
356
- def is_descendant_of(self, target_obj):
357
- """Return True if the current node is descendant of target_obj"""
358
- return self.pk in target_obj.get_descendants_pks()
359
-
360
- def is_first_child(self):
361
- """Return True if the current node is the first child"""
362
- return self.tn_priority == 0
363
-
364
- def is_last_child(self):
365
- """Return True if the current node is the last child"""
366
-
367
- return self.tn_priority == self.get_siblings_count() - 1
368
-
369
- def is_leaf(self):
370
- """Return True if the current node is leaf (self, it has not children)"""
371
- return self.tn_children.count() == 0
372
-
373
- def is_parent_of(self, target_obj):
374
- """Return True if the current node is parent of target_obj"""
375
- return self == target_obj.tn_parent
376
-
377
- def is_root(self):
378
- """Return True if the current node is root"""
379
- return self.tn_parent is None
380
-
381
- def is_root_of(self, target_obj):
382
- """Return True if the current node is root of target_obj"""
383
- return self == target_obj.get_root()
384
-
385
- def is_sibling_of(self, target_obj):
386
- """Return True if the current node is sibling of target_obj"""
387
- return self in target_obj.tn_parent.tn_children.all()
388
-
389
- # I think this method is not needed.
390
- # Clearing entries in the Closure Table will happen automatically
391
- # via cascading deletion
392
-
393
- # def delete(self, cascade=True, *args, **kwargs):
394
- # """Delete a node if cascade=True (default behaviour), children and
395
- # descendants will be deleted too, otherwise children's parent will be
396
- # set to None (then children become roots)"""
397
-
398
- # pass
399
-
400
- # Public properties
401
- # All properties map a get_{{property}}() method.
402
-
403
- @property
404
- def ancestors(self):
405
- return self.get_ancestors()
406
-
407
- @property
408
- def ancestors_count(self):
409
- return self.get_ancestors_count()
410
-
411
- @property
412
- def ancestors_pks(self):
413
- return self.get_ancestors_pks()
414
-
415
- @property
416
- def breadcrumbs(self):
417
- return self.get_breadcrumbs()
418
-
419
- @property
420
- def children(self):
421
- return self.get_children()
422
-
423
- @property
424
- def children_count(self):
425
- return self.get_children_count()
426
-
427
- @property
428
- def children_pks(self):
429
- return self.get_children_pks()
430
-
431
- @property
432
- def depth(self):
433
- return self.get_depth()
434
-
435
- @property
436
- def descendants(self):
437
- return self.get_descendants()
438
-
439
- @property
440
- def descendants_count(self):
441
- return self.get_descendants_count()
442
-
443
- @property
444
- def descendants_pks(self):
445
- return self.get_descendants_pks()
446
-
447
- @property
448
- def descendants_tree(self):
449
- return self.get_descendants_tree()
450
-
451
- @property
452
- def descendants_tree_display(self):
453
- return self.get_descendants_tree_display()
454
-
455
- @property
456
- def first_child(self):
457
- return self.get_first_child()
458
-
459
- @property
460
- def index(self):
461
- return self.get_index()
462
-
463
- @property
464
- def last_child(self):
465
- return self.get_last_child()
466
-
467
- @property
468
- def level(self):
469
- return self.get_level()
470
-
471
- @property
472
- def parent(self):
473
- return self.tn_parent
474
-
475
- @property
476
- def parent_pk(self):
477
- return self.get_parent_pk()
478
-
479
- @property
480
- def priority(self):
481
- return self.get_priority()
482
-
483
- @classproperty
484
- def roots(cls):
485
- return cls.get_roots()
486
-
487
- @property
488
- def root(self):
489
- return self.get_root()
490
-
491
- @property
492
- def root_pk(self):
493
- return self.get_root_pk()
494
-
495
- @property
496
- def siblings(self):
497
- return self.get_siblings()
498
-
499
- @property
500
- def siblings_count(self):
501
- return self.get_siblings_count()
502
-
503
- @property
504
- def siblings_pks(self):
505
- return self.get_siblings_pks()
506
-
507
- @classproperty
508
- def tree(cls):
509
- return cls.get_tree()
510
-
511
- @classproperty
512
- def tree_display(cls):
513
- return cls.get_tree_display()
514
-
515
- # ----------------------------------------------------------------------
516
- # Private methods
517
- # The usage of these methods is only allowed by developers. In future
518
- # versions, these methods may be changed or removed without any warning.
519
-
520
- @property
521
- def _closure_model(self):
522
- return self._meta.model.closure_model
523
-
524
- @property
525
- def tn_order(self):
526
- path = self.get_breadcrumbs(attr='tn_priority')
527
- return ''.join(['{:0>6g}'.format(i) for i in path])
528
-
529
- @cached_tree_method
530
- def object2dict(self, instance, exclude=[]):
531
- """Convert Class Object to python dict"""
532
-
533
- result = dict()
534
-
535
- if not hasattr(instance, '__dict__'):
536
- return instance
537
-
538
- new_subdic = dict(vars(instance))
539
- for key, value in new_subdic.items():
540
- if key.startswith('_') or key in exclude:
541
- continue
542
- result.update({key: self.object2dict(value, exclude)})
543
-
544
- childs = instance.tn_children.all()
545
- if childs.count() > 0:
546
- result.update({
547
- 'children': [
548
- obj.object2dict(obj, exclude)
549
- for obj in childs.all()]
550
- })
551
- result.update({'path': instance.get_path(format_str=':d')})
552
- return result
553
-
554
- @cached_tree_method
555
- def get_display(self, indent=True, mark='— '):
556
- indentation = (mark * self.tn_ancestors_count) if indent else ''
557
- indentation = force_str(indentation)
558
- text = self.get_display_text()
559
- text = force_str(text)
560
- return indentation + text
561
-
562
- @cached_tree_method
563
- def get_display_text(self):
564
- """
565
- Gets the text that will be indented in `get_display` method.
566
- Returns the `treenode_display_field` value if specified,
567
- otherwise falls back on the model's pk.
568
- Override this method to return another field or a computed value. #27
569
- """
570
- text = ''
571
- if (hasattr(self, 'treenode_display_field') and
572
- self.treenode_display_field is not None):
573
- field_name = getattr(self, 'treenode_display_field')
574
- text = getattr(self, field_name, '')
575
- if not text and self.pk:
576
- text = self.pk
577
- return force_str(text)
578
-
579
- @transaction.atomic
580
- def _insert(self):
581
- """Adds a new entry to the Adjacency Table and the Closure Table"""
582
-
583
- treenode_cache.clear()
584
-
585
- instance = self._closure_model.objects.create(
586
- parent=self,
587
- child=self,
588
- depth=0
589
- )
590
- instance.save()
591
-
592
- qs = self._closure_model.objects.all()
593
- parents = qs.filter(child=self.tn_parent).values('parent', 'depth')
594
- children = qs.filter(parent=self).values('child', 'depth')
595
- objects = [
596
- self._closure_model(
597
- parent_id=p['parent'],
598
- child_id=c['child'],
599
- depth=p['depth'] + c['depth'] + 1
600
- )
601
- for p in parents
602
- for c in children
603
- ]
604
- self._closure_model.objects.bulk_create(objects)
605
-
606
- @transaction.atomic
607
- def _move_to(self, old_parent):
608
- treenode_cache.clear()
609
-
610
- target = self.tn_parent
611
- qs = self._closure_model.objects.all()
612
- subtree = qs.filter(parent=self).values('child', 'depth')
613
- supertree = qs.filter(child=target).values('parent', 'depth')
614
-
615
- # Step 1. Delete
616
- subtree_pks = [node.child.pk for node in qs.filter(parent=self)]
617
- qs.filter(child_id__in=subtree_pks).exclude(
618
- parent_id__in=subtree_pks).delete()
619
-
620
- # Step 2. Insert
621
- objects = [
622
- self._closure_model(
623
- parent_id=p['parent'],
624
- child_id=c['child'],
625
- depth=p['depth'] + c['depth'] + 1
626
- )
627
- for p in supertree
628
- for c in subtree
629
- ]
630
- self._closure_model.objects.bulk_create(objects)
631
-
632
- def _order(self):
633
-
634
- treenode_cache.clear()
635
-
636
- queryset = self.get_siblings_queryset()
637
-
638
- if self.tn_priority > queryset.count():
639
- self.tn_priority = queryset.count()
640
-
641
- siblings = list(node for node in queryset)
642
- sorted_siblings = sorted(siblings, key=lambda x: x.tn_priority)
643
- sorted_siblings.insert(self.tn_priority, self)
644
-
645
- for index in range(len(sorted_siblings)):
646
- sorted_siblings[index].tn_priority = index
647
-
648
- self._meta.model.objects.bulk_update(
649
- sorted_siblings, ('tn_priority', ))
650
-
651
- def save(self, force_insert=False, *args, **kwargs):
652
- treenode_cache.clear()
653
-
654
- try:
655
- old = self._meta.model.objects.get(pk=self.pk)
656
- old_parent = old.tn_parent
657
- except self._meta.model.DoesNotExist:
658
- force_insert = True
659
-
660
- super().save(*args, **kwargs)
661
- self._order()
662
-
663
- if force_insert:
664
- self._insert()
665
- elif old_parent != self.tn_parent:
666
- self._move_to(old_parent)
667
-
668
- # The end
@@ -1 +0,0 @@
1
-