django-fast-treenode 1.1.3__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.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
-