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
@@ -0,0 +1,210 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ TreeNode Node Mixin
4
+
5
+ Version: 2.1.0
6
+ Author: Timur Kady
7
+ Email: timurkady@yandex.com
8
+ """
9
+
10
+ from django.db import models
11
+ from django.core.exceptions import FieldDoesNotExist
12
+
13
+ from ...cache import cached_method, treenode_cache
14
+ from ...utils.base36 import to_base36
15
+
16
+
17
+ class TreeNodeNodeMixin(models.Model):
18
+ """TreeNode Node Mixin."""
19
+
20
+ class Meta:
21
+ """Moxin Meta Class."""
22
+
23
+ abstract = True
24
+
25
+ @cached_method
26
+ def get_breadcrumbs(self, attr='pk'):
27
+ """Optimized breadcrumbs retrieval with direct cache check."""
28
+ try:
29
+ self._meta.get_field(attr)
30
+ except FieldDoesNotExist:
31
+ raise ValueError(f"Invalid attribute name: {attr}")
32
+
33
+ # Easy logics for roots
34
+ if self.tn_parent is None:
35
+ return [getattr(self, attr)]
36
+
37
+ # Generate parents cache key
38
+ cache_key = treenode_cache.generate_cache_key(
39
+ self._meta.label,
40
+ self.get_breadcrumbs.__name__,
41
+ self.tn_parent.pk,
42
+ attr
43
+ )
44
+
45
+ # Try get value from cache
46
+ breadcrumbs = treenode_cache.get(cache_key)
47
+ if breadcrumbs is not None:
48
+ return breadcrumbs + [getattr(self, attr)]
49
+
50
+ queryset = self.get_ancestors_queryset(include_self=True).only(attr)
51
+ return [getattr(item, attr) for item in queryset]
52
+
53
+ @cached_method
54
+ def get_depth(self):
55
+ """Get the node depth (self, how many levels of descendants)."""
56
+ return self.closure_model.get_depth(self)
57
+
58
+ @cached_method
59
+ def get_index(self):
60
+ """Get the node index (self, index in node.parent.children list)."""
61
+ if self.tn_parent is None:
62
+ return self.tn_priority
63
+ source = list(self.tn_parent.tn_children.all())
64
+ return source.index(self) if self in source else self.tn_priority
65
+
66
+ @cached_method
67
+ def get_level(self):
68
+ """Get the node level (self, starting from 1)."""
69
+ return self.closure_model.get_level(self)
70
+
71
+ def get_order(self):
72
+ """Return the materialized path."""
73
+ path = self.get_breadcrumbs(attr='tn_priority')
74
+ segments = [to_base36(i).rjust(6, '0') for i in path]
75
+ return ''.join(segments)
76
+
77
+ def insert_at(self, target, position='first-child', save=False):
78
+ """
79
+ Insert a node into the tree relative to the target node.
80
+
81
+ Parameters:
82
+ target: еhe target node relative to which this node will be placed.
83
+
84
+ position – the position, relative to the target node, where the
85
+ current node object will be moved to, can be one of:
86
+
87
+ - first-root: the node will be the first root node;
88
+ - last-root: the node will be the last root node;
89
+ - sorted-root: the new node will be moved after sorting by
90
+ the treenode_sort_field field;
91
+
92
+ - first-sibling: the node will be the new leftmost sibling of the
93
+ target node;
94
+ - left-sibling: the node will take the target node’s place, which will
95
+ be moved to the target position with shifting follows nodes;
96
+ - right-sibling: the node will be moved to the position after the
97
+ target node;
98
+ - last-sibling: the node will be the new rightmost sibling of the
99
+ target node;
100
+ - sorted-sibling: the new node will be moved after sorting by
101
+ the treenode_sort_field field;
102
+
103
+ - first-child: the node will be the first child of the target node;
104
+ - last-child: the node will be the new rightmost child of the target
105
+ - sorted-child: the new node will be moved after sorting by
106
+ the treenode_sort_field field.
107
+
108
+ save : if `save=true`, the node will be saved in the tree. Otherwise,
109
+ the method will return a model instance with updated fields: parent
110
+ field and position in sibling list.
111
+
112
+ Before using this method, the model instance must be correctly created
113
+ with all required fields defined. If the model has required fields,
114
+ then simply creating an object and calling insert_at() will not work,
115
+ because Django will raise an exception.
116
+ """
117
+ # This method seems to have very dubious practical value.
118
+ parent, priority = self._meta.model._get_place(target, position)
119
+ self.tn_parent = parent
120
+ self.tn_priority = priority
121
+
122
+ if save:
123
+ self.save()
124
+
125
+ def move_to(self, target, position=0):
126
+ """
127
+ Move node relative to target node and position.
128
+
129
+ Moves the model instance relative to the target node and sets its
130
+ position (if necessary).
131
+
132
+ position – the position, relative to the target node, where the
133
+ current node object will be moved to, can be one of:
134
+
135
+ - first-root: the node will be the first root node;
136
+ - last-root: the node will be the last root node;
137
+ - sorted-root: the new node will be moved after sorting by
138
+ the treenode_sort_field field;
139
+ Note: if `position` contains `root`, then `target` parametr is ignored
140
+
141
+ - first-sibling: the node will be the new leftmost sibling of the
142
+ target node;
143
+ - left-sibling: the node will take the target node’s place, which will
144
+ be moved to the target position with shifting follows nodes;
145
+ - right-sibling: the node will be moved to the position after the
146
+ target node;
147
+ - last-sibling: the node will be the new rightmost sibling of the
148
+ target node;
149
+ - sorted-sibling: the new node will be moved after sorting by
150
+ the treenode_sort_field field;
151
+
152
+ - first-child: the node will be the first child of the target node;
153
+ - last-child: the node will be the new rightmost child of the target
154
+ - sorted-child: the new node will be moved after sorting by
155
+ the treenode_sort_field field;
156
+ """
157
+ parent, priority = self._meta.model._get_place(target, position)
158
+ self.tn_parent = parent
159
+ self.tn_priority = priority
160
+ self.save()
161
+
162
+ def get_path(self, prefix='', suffix='', delimiter='.', format_str=''):
163
+ """Return Materialized Path of node."""
164
+ priorities = self.get_breadcrumbs(attr='tn_priority')
165
+ if not priorities or all(p is None for p in priorities):
166
+ return prefix + suffix
167
+
168
+ str_ = "{%s}" % format_str
169
+ path = delimiter.join([
170
+ str_.format(p)
171
+ for p in priorities
172
+ if p is not None
173
+ ])
174
+ return prefix + path + suffix
175
+
176
+ @cached_method
177
+ def get_parent(self):
178
+ """Get the parent node."""
179
+ return self.tn_parent
180
+
181
+ def set_parent(self, parent_obj):
182
+ """Set the parent node."""
183
+ self.tn_parent = parent_obj
184
+ self.save()
185
+
186
+ def get_parent_pk(self):
187
+ """Get the parent node pk."""
188
+ return self.get_parent().pk if self.tn_parent else None
189
+
190
+ @cached_method
191
+ def get_priority(self):
192
+ """Get the node priority."""
193
+ return self.tn_priority
194
+
195
+ def set_priority(self, priority=0):
196
+ """Set the node priority."""
197
+ self.tn_priority = priority
198
+ self.save()
199
+
200
+ @cached_method
201
+ def get_root(self):
202
+ """Get the root node for the current node."""
203
+ return self.closure_model.get_root(self)
204
+
205
+ def get_root_pk(self):
206
+ """Get the root node pk for the current node."""
207
+ root = self.get_root()
208
+ return root.pk if root else None
209
+
210
+ # The End
@@ -0,0 +1,156 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ TreeNode Properties Mixin
4
+
5
+ Version: 2.1.0
6
+ Author: Timur Kady
7
+ Email: timurkady@yandex.com
8
+ """
9
+
10
+ from ..classproperty import classproperty
11
+
12
+
13
+ class TreeNodePropertiesMixin:
14
+ """
15
+ TreeNode Properties Mixin.
16
+
17
+ Public properties.
18
+ All properties map a get_{{property}}() method.
19
+ """
20
+
21
+ @property
22
+ def ancestors(self):
23
+ """Get a list with all ancestors; self included."""
24
+ return self.get_ancestors()
25
+
26
+ @property
27
+ def ancestors_count(self):
28
+ """Get the ancestors count."""
29
+ return self.get_ancestors_count()
30
+
31
+ @property
32
+ def ancestors_pks(self):
33
+ """Get the ancestors pks list; self included."""
34
+ return self.get_ancestors_pks()
35
+
36
+ @property
37
+ def breadcrumbs(self):
38
+ """Get the breadcrumbs to current node(self, included)."""
39
+ return self.get_breadcrumbs()
40
+
41
+ @property
42
+ def children(self):
43
+ """Get a list containing all children; self included."""
44
+ return self.get_children()
45
+
46
+ @property
47
+ def children_count(self):
48
+ """Get the children count."""
49
+ return self.get_children_count()
50
+
51
+ @property
52
+ def children_pks(self):
53
+ """Get the children pks list."""
54
+ return self.get_children_pks()
55
+
56
+ @property
57
+ def depth(self):
58
+ """Get the node depth."""
59
+ return self.get_depth()
60
+
61
+ @property
62
+ def descendants(self):
63
+ """Get a list containing all descendants; self not included."""
64
+ return self.get_descendants()
65
+
66
+ @property
67
+ def descendants_count(self):
68
+ """Get the descendants count; self not included."""
69
+ return self.get_descendants_count()
70
+
71
+ @property
72
+ def descendants_pks(self):
73
+ """Get the descendants pks list; self not included."""
74
+ return self.get_descendants_pks()
75
+
76
+ @property
77
+ def first_child(self):
78
+ """Get the first child node."""
79
+ return self.get_first_child()
80
+
81
+ @property
82
+ def index(self):
83
+ """Get the node index."""
84
+ return self.get_index()
85
+
86
+ @property
87
+ def last_child(self):
88
+ """Get the last child node."""
89
+ return self.get_last_child()
90
+
91
+ @property
92
+ def level(self):
93
+ """Get the node level."""
94
+ return self.get_level()
95
+
96
+ @property
97
+ def parent(self):
98
+ """Get node parent."""
99
+ return self.tn_parent
100
+
101
+ @property
102
+ def parent_pk(self):
103
+ """Get node parent pk."""
104
+ return self.get_parent_pk()
105
+
106
+ @property
107
+ def priority(self):
108
+ """Get node priority."""
109
+ return self.get_priority()
110
+
111
+ @classproperty
112
+ def roots(cls):
113
+ """Get a list with all root nodes."""
114
+ return cls.get_roots()
115
+
116
+ @property
117
+ def root(self):
118
+ """Get the root node for the current node."""
119
+ return self.get_root()
120
+
121
+ @property
122
+ def root_pk(self):
123
+ """Get the root node pk for the current node."""
124
+ return self.get_root_pk()
125
+
126
+ @property
127
+ def siblings(self):
128
+ """Get a list with all the siblings."""
129
+ return self.get_siblings()
130
+
131
+ @property
132
+ def siblings_count(self):
133
+ """Get the siblings count."""
134
+ return self.get_siblings_count()
135
+
136
+ @property
137
+ def siblings_pks(self):
138
+ """Get the siblings pks list."""
139
+ return self.get_siblings_pks()
140
+
141
+ @classproperty
142
+ def tree(cls):
143
+ """Get an n-dimensional dict representing the model tree."""
144
+ return cls.get_tree()
145
+
146
+ @classproperty
147
+ def tree_display(cls):
148
+ """Get a multiline string representing the model tree."""
149
+ return cls.get_tree_display()
150
+
151
+ @property
152
+ def tn_order(self):
153
+ """Return the materialized path."""
154
+ return self.get_order()
155
+
156
+ # The End
@@ -0,0 +1,96 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ TreeNode Roots Mixin
4
+
5
+ Version: 2.1.0
6
+ Author: Timur Kady
7
+ Email: timurkady@yandex.com
8
+ """
9
+
10
+ from django.db import models
11
+ from treenode.cache import cached_method
12
+
13
+
14
+ class TreeNodeRootsMixin(models.Model):
15
+ """TreeNode Roots Mixin."""
16
+
17
+ class Meta:
18
+ """Moxin Meta Class."""
19
+
20
+ abstract = True
21
+
22
+ @classmethod
23
+ def add_root(cls, position=None, **kwargs):
24
+ """
25
+ Add a root node to the tree.
26
+
27
+ Adds a new root node at the specified position. If no position is
28
+ specified, the new node will be the last element in the root.
29
+ Parameters:
30
+ position: can be 'first-root', 'last-root', 'sorted-root' or integer
31
+ value.
32
+ **kwargs – Object creation data that will be passed to the inherited
33
+ Node model
34
+ instance – Instead of passing object creation data, you can pass
35
+ an already-constructed (but not yet saved) model instance to be
36
+ inserted into the tree.
37
+
38
+ Returns:
39
+ The created node object. It will be save()d by this method.
40
+ """
41
+ if isinstance(position, int):
42
+ priority = position
43
+ else:
44
+ if position not in ['first-root', 'last-root', 'sorted-root']:
45
+ raise ValueError(f"Invalid position format: {position}")
46
+
47
+ parent, priority = cls._get_place(None, position)
48
+
49
+ instance = kwargs.get("instance")
50
+ if instance is None:
51
+ instance = cls(**kwargs)
52
+
53
+ parent, priority = cls._get_place(None, position)
54
+ instance.tn_parent = None
55
+ instance.tn_priority = priority
56
+ instance.save()
57
+ return instance
58
+
59
+ @classmethod
60
+ @cached_method
61
+ def get_roots_queryset(cls):
62
+ """Get root nodes queryset with preloaded children."""
63
+ qs = cls.objects.filter(tn_parent=None).prefetch_related('tn_children')
64
+ return qs
65
+
66
+ @classmethod
67
+ @cached_method
68
+ def get_roots_pks(cls):
69
+ """Get a list with all root nodes."""
70
+ pks = cls.objects.filter(tn_parent=None).values_list("id", flat=True)
71
+ return list(pks)
72
+
73
+ @classmethod
74
+ def get_roots(cls):
75
+ """Get a list with all root nodes."""
76
+ qs = cls.get_roots_queryset()
77
+ return list(qs)
78
+
79
+ @classmethod
80
+ def get_roots_count(cls):
81
+ """Get a list with all root nodes."""
82
+ return len(cls.get_roots_pks())
83
+
84
+ @classmethod
85
+ def get_first_root(cls):
86
+ """Return the first root node in the tree or None if it is empty."""
87
+ roots = cls.get_roots_queryset()
88
+ return roots.fiest() if roots.count() > 0 else None
89
+
90
+ @classmethod
91
+ def get_last_root(cls):
92
+ """Return the last root node in the tree or None if it is empty."""
93
+ roots = cls.get_roots_queryset()
94
+ return roots.last() if roots.count() > 0 else None
95
+
96
+ # The End
@@ -0,0 +1,99 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ TreeNode Siblings Mixin
4
+
5
+ Version: 2.1.0
6
+ Author: Timur Kady
7
+ Email: timurkady@yandex.com
8
+ """
9
+
10
+ from django.db import models
11
+ from treenode.cache import cached_method
12
+
13
+
14
+ class TreeNodeSiblingsMixin(models.Model):
15
+ """TreeNode Siblings Mixin."""
16
+
17
+ class Meta:
18
+ """Moxin Meta Class."""
19
+
20
+ abstract = True
21
+
22
+ def add_sibling(self, position=None, **kwargs):
23
+ """
24
+ Add a new node as a sibling to the current node object.
25
+
26
+ Returns the created node object or None if failed. It will be saved
27
+ by this method.
28
+ """
29
+ if isinstance(position, int):
30
+ priority = position
31
+ parent = self.tn_parent
32
+ else:
33
+ if position not in [
34
+ 'first-sibling', 'left-sibling', 'right-sibling',
35
+ 'last-sibling', 'sorted-sibling']:
36
+ raise ValueError(f"Invalid position format: {position}")
37
+ parent, priority = self._meta.model._get_place(self, position)
38
+
39
+ instance = kwargs.get("instance")
40
+ if instance is None:
41
+ instance = self._meta.model(**kwargs)
42
+ instance.tn_priority = priority
43
+ instance.tn_priority = parent
44
+ instance.save()
45
+ return instance
46
+
47
+ @cached_method
48
+ def get_siblings_queryset(self):
49
+ """Get the siblings queryset with prefetch."""
50
+ if self.tn_parent:
51
+ qs = self.tn_parent.tn_children.prefetch_related('tn_children')
52
+ else:
53
+ qs = self._meta.model.objects.filter(tn_parent__isnull=True)
54
+ return qs.exclude(pk=self.pk)
55
+
56
+ def get_siblings(self):
57
+ """Get a list with all the siblings."""
58
+ return list(self.get_siblings_queryset())
59
+
60
+ def get_siblings_count(self):
61
+ """Get the siblings count."""
62
+ return self.get_siblings_queryset().count()
63
+
64
+ def get_siblings_pks(self):
65
+ """Get the siblings pks list."""
66
+ return [item.pk for item in self.get_siblings_queryset()]
67
+
68
+ def get_first_sibling(self):
69
+ """
70
+ Return the fist node’s sibling.
71
+
72
+ Method can return the node itself if it was the leftmost sibling.
73
+ """
74
+ return self.get_siblings_queryset().fist()
75
+
76
+ def get_previous_sibling(self):
77
+ """Return the previous sibling in the tree, or None."""
78
+ priority = self.tn_priority - 1
79
+ if priority < 0:
80
+ return None
81
+ return self.get_siblings_queryset.filter(tn_priority=priority)
82
+
83
+ def get_next_sibling(self):
84
+ """Return the next sibling in the tree, or None."""
85
+ priority = self.tn_priority = 1
86
+ queryset = self.get_siblings_queryset()
87
+ if priority == queryset.count():
88
+ return None
89
+ return queryset.filter(tn_priority=priority)
90
+
91
+ def get_last_sibling(self):
92
+ """
93
+ Return the fist node’s sibling.
94
+
95
+ Method can return the node itself if it was the leftmost sibling.
96
+ """
97
+ return self.get_siblings_queryset().last()
98
+
99
+ # The End