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
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
-