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.
- {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.dist-info}/LICENSE +2 -2
- django_fast_treenode-2.1.1.dist-info/METADATA +158 -0
- django_fast_treenode-2.1.1.dist-info/RECORD +64 -0
- {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.dist-info}/WHEEL +1 -1
- treenode/admin/__init__.py +9 -0
- treenode/admin/admin.py +295 -0
- treenode/admin/changelist.py +65 -0
- treenode/admin/mixins.py +302 -0
- treenode/apps.py +12 -1
- treenode/cache.py +2 -2
- treenode/forms.py +8 -10
- treenode/managers/__init__.py +21 -0
- treenode/managers/adjacency.py +203 -0
- treenode/managers/closure.py +278 -0
- treenode/models/__init__.py +2 -1
- treenode/models/adjacency.py +343 -0
- treenode/models/classproperty.py +3 -0
- treenode/models/closure.py +23 -24
- treenode/models/factory.py +12 -2
- treenode/models/mixins/__init__.py +23 -0
- treenode/models/mixins/ancestors.py +65 -0
- treenode/models/mixins/children.py +81 -0
- treenode/models/mixins/descendants.py +66 -0
- treenode/models/mixins/family.py +63 -0
- treenode/models/mixins/logical.py +68 -0
- treenode/models/mixins/node.py +210 -0
- treenode/models/mixins/properties.py +156 -0
- treenode/models/mixins/roots.py +96 -0
- treenode/models/mixins/siblings.py +99 -0
- treenode/models/mixins/tree.py +344 -0
- treenode/signals.py +26 -0
- treenode/static/treenode/css/tree_widget.css +201 -31
- treenode/static/treenode/css/treenode_admin.css +48 -41
- treenode/static/treenode/js/tree_widget.js +269 -131
- treenode/static/treenode/js/treenode_admin.js +131 -171
- treenode/templates/admin/tree_node_changelist.html +6 -0
- treenode/templates/admin/treenode_ajax_rows.html +7 -0
- treenode/tests/tests.py +488 -0
- treenode/urls.py +10 -6
- treenode/utils/__init__.py +2 -0
- treenode/utils/aid.py +46 -0
- treenode/utils/base16.py +38 -0
- treenode/utils/base36.py +3 -1
- treenode/utils/db.py +116 -0
- treenode/utils/exporter.py +2 -0
- treenode/utils/importer.py +0 -1
- treenode/utils/radix.py +61 -0
- treenode/version.py +2 -2
- treenode/views.py +118 -43
- treenode/widgets.py +91 -43
- django_fast_treenode-2.0.11.dist-info/METADATA +0 -698
- django_fast_treenode-2.0.11.dist-info/RECORD +0 -42
- treenode/admin.py +0 -439
- treenode/docs/Documentation +0 -636
- treenode/managers.py +0 -419
- treenode/models/proxy.py +0 -669
- {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.dist-info}/top_level.txt +0 -0
treenode/models/closure.py
CHANGED
@@ -11,16 +11,17 @@ Features:
|
|
11
11
|
- Implements cached queries for improved performance.
|
12
12
|
- Provides bulk operations for inserting, moving, and deleting nodes.
|
13
13
|
|
14
|
-
Version: 2.0
|
14
|
+
Version: 2.1.0
|
15
15
|
Author: Timur Kady
|
16
16
|
Email: timurkady@yandex.com
|
17
17
|
"""
|
18
18
|
|
19
19
|
|
20
20
|
from django.db import models, transaction
|
21
|
+
from django.db.models.signals import pre_save, post_save
|
21
22
|
|
22
23
|
from ..managers import ClosureModelManager
|
23
|
-
from ..
|
24
|
+
from ..signals import disable_signals
|
24
25
|
|
25
26
|
|
26
27
|
class ClosureModel(models.Model):
|
@@ -44,6 +45,14 @@ class ClosureModel(models.Model):
|
|
44
45
|
|
45
46
|
depth = models.PositiveIntegerField()
|
46
47
|
|
48
|
+
node = models.OneToOneField(
|
49
|
+
'TreeNodeModel',
|
50
|
+
related_name="tn_closure",
|
51
|
+
on_delete=models.CASCADE,
|
52
|
+
null=True,
|
53
|
+
blank=True,
|
54
|
+
)
|
55
|
+
|
47
56
|
objects = ClosureModelManager()
|
48
57
|
|
49
58
|
class Meta:
|
@@ -53,6 +62,7 @@ class ClosureModel(models.Model):
|
|
53
62
|
unique_together = (("parent", "child"),)
|
54
63
|
indexes = [
|
55
64
|
models.Index(fields=["parent", "child"]),
|
65
|
+
models.Index(fields=["child", "parent"]),
|
56
66
|
models.Index(fields=["parent", "child", "depth"]),
|
57
67
|
]
|
58
68
|
|
@@ -63,39 +73,34 @@ class ClosureModel(models.Model):
|
|
63
73
|
# ----------- Methods of working with tree structure ----------- #
|
64
74
|
|
65
75
|
@classmethod
|
66
|
-
def clear_cache(cls):
|
67
|
-
"""Clear cache for this model only."""
|
68
|
-
treenode_cache.invalidate(cls._meta.label)
|
69
|
-
|
70
|
-
@classmethod
|
71
|
-
@cached_method
|
72
76
|
def get_ancestors_pks(cls, node, include_self=True, depth=None):
|
73
77
|
"""Get the ancestors pks list."""
|
74
78
|
options = dict(child_id=node.pk, depth__gte=0 if include_self else 1)
|
75
79
|
if depth:
|
76
80
|
options["depth__lte"] = depth
|
77
|
-
queryset = cls.objects.filter(**options)
|
81
|
+
queryset = cls.objects.filter(**options)\
|
82
|
+
.order_by('depth')\
|
83
|
+
.values_list('parent_id', flat=True)
|
78
84
|
return list(queryset.values_list("parent_id", flat=True))
|
79
85
|
|
80
86
|
@classmethod
|
81
|
-
@cached_method
|
82
87
|
def get_descendants_pks(cls, node, include_self=False, depth=None):
|
83
88
|
"""Get a list containing all descendants."""
|
84
89
|
options = dict(parent_id=node.pk, depth__gte=0 if include_self else 1)
|
85
90
|
if depth:
|
86
91
|
options.update({'depth__lte': depth})
|
87
|
-
queryset = cls.objects.filter(**options)
|
88
|
-
|
92
|
+
queryset = cls.objects.filter(**options)\
|
93
|
+
.order_by('depth')\
|
94
|
+
.values_list('child_id', flat=True)
|
95
|
+
return queryset
|
89
96
|
|
90
97
|
@classmethod
|
91
|
-
@cached_method
|
92
98
|
def get_root(cls, node):
|
93
99
|
"""Get the root node pk for the current node."""
|
94
100
|
queryset = cls.objects.filter(child=node).order_by('-depth')
|
95
|
-
return queryset.
|
101
|
+
return queryset.first().parent if queryset.count() > 0 else None
|
96
102
|
|
97
103
|
@classmethod
|
98
|
-
@cached_method
|
99
104
|
def get_depth(cls, node):
|
100
105
|
"""Get the node depth (how deep the node is in the tree)."""
|
101
106
|
result = cls.objects.filter(child__pk=node.pk).aggregate(
|
@@ -104,7 +109,6 @@ class ClosureModel(models.Model):
|
|
104
109
|
return result if result is not None else 0
|
105
110
|
|
106
111
|
@classmethod
|
107
|
-
@cached_method
|
108
112
|
def get_level(cls, node):
|
109
113
|
"""Get the node level (starting from 1)."""
|
110
114
|
return cls.objects.filter(child__pk=node.pk).aggregate(
|
@@ -116,8 +120,6 @@ class ClosureModel(models.Model):
|
|
116
120
|
"""Add a node to a Closure table."""
|
117
121
|
# Call bulk_create passing a single object
|
118
122
|
cls.objects.bulk_create([node], batch_size=1000)
|
119
|
-
# Clear cache
|
120
|
-
cls.clear_cache()
|
121
123
|
|
122
124
|
@classmethod
|
123
125
|
@transaction.atomic
|
@@ -125,20 +127,17 @@ class ClosureModel(models.Model):
|
|
125
127
|
"""Move a nodes (node and its subtree) to a new parent."""
|
126
128
|
# Call bulk_update passing a single object
|
127
129
|
cls.objects.bulk_update(nodes, batch_size=1000)
|
128
|
-
# Clear cache
|
129
|
-
cls.clear_cache()
|
130
130
|
|
131
131
|
@classmethod
|
132
132
|
@transaction.atomic
|
133
133
|
def delete_all(cls):
|
134
134
|
"""Clear the Closure Table."""
|
135
|
-
# Clear cache
|
136
|
-
cls.clear_cache()
|
137
135
|
cls.objects.all().delete()
|
138
136
|
|
139
137
|
def save(self, force_insert=False, *args, **kwargs):
|
140
138
|
"""Save method."""
|
141
|
-
|
142
|
-
|
139
|
+
with (disable_signals(pre_save, self._meta.model),
|
140
|
+
disable_signals(post_save, self._meta.model)):
|
141
|
+
super().save(force_insert, *args, **kwargs)
|
143
142
|
|
144
143
|
# The End
|
treenode/models/factory.py
CHANGED
@@ -10,7 +10,7 @@ Features:
|
|
10
10
|
- Dynamically creates and assigns a Closure Model for each TreeNodeModel.
|
11
11
|
- Facilitates the management of hierarchical relationships.
|
12
12
|
|
13
|
-
Version: 2.
|
13
|
+
Version: 2.1.0
|
14
14
|
Author: Timur Kady
|
15
15
|
Email: timurkady@yandex.com
|
16
16
|
"""
|
@@ -18,7 +18,7 @@ Email: timurkady@yandex.com
|
|
18
18
|
|
19
19
|
import sys
|
20
20
|
from django.db import models
|
21
|
-
from .closure import ClosureModel
|
21
|
+
from .closure import ClosureModel
|
22
22
|
|
23
23
|
|
24
24
|
class TreeFactory(models.base.ModelBase):
|
@@ -61,11 +61,21 @@ class TreeFactory(models.base.ModelBase):
|
|
61
61
|
on_delete=models.CASCADE,
|
62
62
|
),
|
63
63
|
|
64
|
+
"node": models.OneToOneField(
|
65
|
+
cls._meta.model,
|
66
|
+
related_name="tn_closure",
|
67
|
+
on_delete=models.CASCADE,
|
68
|
+
null=True,
|
69
|
+
blank=True,
|
70
|
+
),
|
71
|
+
|
64
72
|
"__module__": cls.__module__
|
65
73
|
}
|
74
|
+
|
66
75
|
closure_model = type(closure_name, (ClosureModel,), fields)
|
67
76
|
setattr(sys.modules[cls.__module__], closure_name, closure_model)
|
68
77
|
|
69
78
|
cls.closure_model = closure_model
|
70
79
|
|
80
|
+
|
71
81
|
# The End
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
from .ancestors import TreeNodeAncestorsMixin
|
4
|
+
from .children import TreeNodeChildrenMixin
|
5
|
+
from .descendants import TreeNodeDescendantsMixin
|
6
|
+
from .family import TreeNodeFamilyMixin
|
7
|
+
from .logical import TreeNodeLogicalMixin
|
8
|
+
from .node import TreeNodeNodeMixin
|
9
|
+
from .properties import TreeNodePropertiesMixin
|
10
|
+
from .roots import TreeNodeRootsMixin
|
11
|
+
from .siblings import TreeNodeSiblingsMixin
|
12
|
+
from .tree import TreeNodeTreeMixin
|
13
|
+
|
14
|
+
|
15
|
+
__all__ = [
|
16
|
+
"TreeNodeAncestorsMixin", "TreeNodeChildrenMixin", "TreeNodeFamilyMixin",
|
17
|
+
"TreeNodeDescendantsMixin", "TreeNodeLogicalMixin", "TreeNodeNodeMixin",
|
18
|
+
"TreeNodePropertiesMixin", "TreeNodeRootsMixin", "TreeNodeSiblingsMixin",
|
19
|
+
"TreeNodeTreeMixin"
|
20
|
+
]
|
21
|
+
|
22
|
+
|
23
|
+
# The End
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Ancestors 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 ...cache import treenode_cache, cached_method
|
12
|
+
|
13
|
+
|
14
|
+
class TreeNodeAncestorsMixin(models.Model):
|
15
|
+
"""TreeNode Ancestors Mixin."""
|
16
|
+
|
17
|
+
class Meta:
|
18
|
+
"""Moxin Meta Class."""
|
19
|
+
|
20
|
+
abstract = True
|
21
|
+
|
22
|
+
@cached_method
|
23
|
+
def get_ancestors_queryset(self, include_self=True, depth=None):
|
24
|
+
"""Get the ancestors queryset (ordered from root to parent)."""
|
25
|
+
qs = self._meta.model.objects.filter(tn_closure__child=self.pk)
|
26
|
+
|
27
|
+
if depth is not None:
|
28
|
+
qs = qs.filter(tn_closure__depth__lte=depth)
|
29
|
+
|
30
|
+
if include_self:
|
31
|
+
qs = qs | self._meta.model.objects.filter(pk=self.pk)
|
32
|
+
|
33
|
+
return qs.distinct().order_by("tn_closure__depth")
|
34
|
+
|
35
|
+
@cached_method
|
36
|
+
def get_ancestors_pks(self, include_self=True, depth=None):
|
37
|
+
"""Get the ancestors pks list."""
|
38
|
+
cache_key = treenode_cache.generate_cache_key(
|
39
|
+
label=self._meta.label,
|
40
|
+
func_name=getattr(self, "get_ancestors_queryset").__name__,
|
41
|
+
unique_id=self.pk,
|
42
|
+
arg={
|
43
|
+
"include_self": include_self,
|
44
|
+
"depth": depth
|
45
|
+
}
|
46
|
+
)
|
47
|
+
queryset = treenode_cache.get(cache_key)
|
48
|
+
if queryset is not None:
|
49
|
+
return list(queryset.values_list("id", flat=True))
|
50
|
+
elif hasattr(self, "closure_model"):
|
51
|
+
return self.closure_model.get_ancestors_pks(
|
52
|
+
self, include_self, depth
|
53
|
+
)
|
54
|
+
return []
|
55
|
+
|
56
|
+
def get_ancestors(self, include_self=True, depth=None):
|
57
|
+
"""Get a list with all ancestors (ordered from root to self/parent)."""
|
58
|
+
queryset = self.get_ancestors_queryset(include_self, depth)
|
59
|
+
return list(queryset)
|
60
|
+
|
61
|
+
def get_ancestors_count(self, include_self=True, depth=None):
|
62
|
+
"""Get the ancestors count."""
|
63
|
+
return len(self.get_ancestors_pks(include_self, depth))
|
64
|
+
|
65
|
+
# The End
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Children 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 TreeNodeChildrenMixin(models.Model):
|
15
|
+
"""TreeNode Ancestors Mixin."""
|
16
|
+
|
17
|
+
class Meta:
|
18
|
+
"""Moxin Meta Class."""
|
19
|
+
|
20
|
+
abstract = True
|
21
|
+
|
22
|
+
def add_child(self, position=None, **kwargs):
|
23
|
+
"""
|
24
|
+
Add a child to the node.
|
25
|
+
|
26
|
+
position:
|
27
|
+
Can be 'first-child', 'last-child', 'sorted-child' or integer value.
|
28
|
+
|
29
|
+
Parameters:
|
30
|
+
**kwargs – Object creation data that will be passed to the inherited
|
31
|
+
Node model
|
32
|
+
instance – Instead of passing object creation data, you can pass
|
33
|
+
an already-constructed (but not yet saved) model instance to be
|
34
|
+
inserted into the tree.
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
The created node object. It will be save()d by this method.
|
38
|
+
"""
|
39
|
+
if isinstance(position, int):
|
40
|
+
priority = position
|
41
|
+
parent = self
|
42
|
+
else:
|
43
|
+
if position not in ['first-child', 'last-child', 'sorted-child']:
|
44
|
+
raise ValueError(f"Invalid position format: {position}")
|
45
|
+
parent, priority = self._meta.model._get_place(self, position)
|
46
|
+
|
47
|
+
instance = kwargs.get("instance")
|
48
|
+
if instance is None:
|
49
|
+
instance = self._meta.model(**kwargs)
|
50
|
+
instance.tn_parent = parent
|
51
|
+
instance.tn_priority = priority
|
52
|
+
instance.save()
|
53
|
+
return instance
|
54
|
+
|
55
|
+
@cached_method
|
56
|
+
def get_children_pks(self):
|
57
|
+
"""Get the children pks list."""
|
58
|
+
return list(self.get_children_queryset().values_list("id", flat=True))
|
59
|
+
|
60
|
+
@cached_method
|
61
|
+
def get_children_queryset(self):
|
62
|
+
"""Get the children queryset with prefetch."""
|
63
|
+
return self.tn_children.prefetch_related('tn_children')
|
64
|
+
|
65
|
+
def get_children(self):
|
66
|
+
"""Get a list containing all children."""
|
67
|
+
return list(self.get_children_queryset())
|
68
|
+
|
69
|
+
def get_children_count(self):
|
70
|
+
"""Get the children count."""
|
71
|
+
return len(self.get_children_pks())
|
72
|
+
|
73
|
+
def get_first_child(self):
|
74
|
+
"""Get the first child node or None if it has no children."""
|
75
|
+
return self.get_children_queryset().first() if self.is_leaf else None
|
76
|
+
|
77
|
+
def get_last_child(self):
|
78
|
+
"""Get the last child node or None if it has no children."""
|
79
|
+
return self.get_children_queryset().last() if self.is_leaf else None
|
80
|
+
|
81
|
+
# The End
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Descendants 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 treenode_cache, cached_method
|
12
|
+
|
13
|
+
|
14
|
+
class TreeNodeDescendantsMixin(models.Model):
|
15
|
+
"""TreeNode Descendants Mixin."""
|
16
|
+
|
17
|
+
class Meta:
|
18
|
+
"""Moxin Meta Class."""
|
19
|
+
|
20
|
+
abstract = True
|
21
|
+
|
22
|
+
@cached_method
|
23
|
+
def get_descendants_queryset(self, include_self=False, depth=None):
|
24
|
+
"""Get the descendants queryset."""
|
25
|
+
queryset = self._meta.model.objects\
|
26
|
+
.annotate(min_depth=models.Min("parents_set__depth"))\
|
27
|
+
.filter(parents_set__parent=self.pk)
|
28
|
+
|
29
|
+
if depth is not None:
|
30
|
+
queryset = queryset.filter(min_depth__lte=depth)
|
31
|
+
if include_self and not queryset.filter(pk=self.pk).exists():
|
32
|
+
queryset = queryset | self._meta.model.objects.filter(pk=self.pk)
|
33
|
+
|
34
|
+
return queryset.order_by("min_depth", "tn_priority")
|
35
|
+
|
36
|
+
@cached_method
|
37
|
+
def get_descendants_pks(self, include_self=False, depth=None):
|
38
|
+
"""Get the descendants pks list."""
|
39
|
+
cache_key = treenode_cache.generate_cache_key(
|
40
|
+
label=self._meta.label,
|
41
|
+
func_name=getattr(self, "get_descendants_queryset").__name__,
|
42
|
+
unique_id=self.pk,
|
43
|
+
arg={
|
44
|
+
"include_self": include_self,
|
45
|
+
"depth": depth
|
46
|
+
}
|
47
|
+
)
|
48
|
+
queryset = treenode_cache.get(cache_key)
|
49
|
+
if queryset is not None:
|
50
|
+
return list(queryset.values_list("id", flat=True))
|
51
|
+
elif hasattr(self, "closure_model"):
|
52
|
+
return self.closure_model.get_descendants_pks(
|
53
|
+
self, include_self, depth
|
54
|
+
)
|
55
|
+
return []
|
56
|
+
|
57
|
+
def get_descendants(self, include_self=False, depth=None):
|
58
|
+
"""Get a list containing all descendants."""
|
59
|
+
queryset = self.get_descendants_queryset(include_self, depth)
|
60
|
+
return list(queryset)
|
61
|
+
|
62
|
+
def get_descendants_count(self, include_self=False, depth=None):
|
63
|
+
"""Get the descendants count."""
|
64
|
+
return len(self.get_descendants_pks(include_self, depth))
|
65
|
+
|
66
|
+
# The End
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Descendants 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 TreeNodeFamilyMixin(models.Model):
|
15
|
+
"""TreeNode Family Mixin."""
|
16
|
+
|
17
|
+
class Meta:
|
18
|
+
"""Moxin Meta Class."""
|
19
|
+
|
20
|
+
abstract = True
|
21
|
+
|
22
|
+
@cached_method
|
23
|
+
def get_family_queryset(self):
|
24
|
+
"""
|
25
|
+
Return node family.
|
26
|
+
|
27
|
+
Return a QuerySet containing the ancestors, itself and the descendants,
|
28
|
+
in tree order.
|
29
|
+
"""
|
30
|
+
model = self._meta.model
|
31
|
+
queryset = model.objects.filter(
|
32
|
+
models.Q(tn_closure__child=self.pk) |
|
33
|
+
models.Q(tn_closure__parent=self.pk) |
|
34
|
+
models.Q(pk=self.pk)
|
35
|
+
).distinct().order_by("tn_closure__depth", "tn_parent", "tn_priority")
|
36
|
+
return queryset
|
37
|
+
|
38
|
+
@cached_method
|
39
|
+
def get_family_pks(self):
|
40
|
+
"""
|
41
|
+
Return node family.
|
42
|
+
|
43
|
+
Return a pk-list containing the ancestors, the model itself and
|
44
|
+
the descendants, in tree order.
|
45
|
+
"""
|
46
|
+
pks = self.get_family_queryset().values_list("id", flat=True)
|
47
|
+
return list(pks)
|
48
|
+
|
49
|
+
def get_family(self):
|
50
|
+
"""
|
51
|
+
Return node family.
|
52
|
+
|
53
|
+
Return a list containing the ancestors, the model itself and
|
54
|
+
the descendants, in tree order.
|
55
|
+
"""
|
56
|
+
queryset = self.get_family_queryset()
|
57
|
+
return list(queryset)
|
58
|
+
|
59
|
+
def get_family_count(self):
|
60
|
+
"""Return number of nodes in family."""
|
61
|
+
return self.get_family_queryset().count()
|
62
|
+
|
63
|
+
# The End
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Logical methods 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
|
+
|
12
|
+
|
13
|
+
class TreeNodeLogicalMixin(models.Model):
|
14
|
+
"""TreeNode Logical Mixin."""
|
15
|
+
|
16
|
+
class Meta:
|
17
|
+
"""Moxin Meta Class."""
|
18
|
+
|
19
|
+
abstract = True
|
20
|
+
|
21
|
+
def is_ancestor_of(self, target_obj):
|
22
|
+
"""Return True if the current node is ancestor of target_obj."""
|
23
|
+
return self in target_obj.get_ancestors(include_self=False)
|
24
|
+
|
25
|
+
def is_child_of(self, target_obj):
|
26
|
+
"""Return True if the current node is child of target_obj."""
|
27
|
+
return self in target_obj.get_children()
|
28
|
+
|
29
|
+
def is_descendant_of(self, target_obj):
|
30
|
+
"""Return True if the current node is descendant of target_obj."""
|
31
|
+
return self in target_obj.get_descendants()
|
32
|
+
|
33
|
+
def is_first_child(self):
|
34
|
+
"""Return True if the current node is the first child."""
|
35
|
+
return self.tn_priority == 0
|
36
|
+
|
37
|
+
def has_children(self):
|
38
|
+
"""Return True if the node has children."""
|
39
|
+
return self.tn_children.exists()
|
40
|
+
|
41
|
+
def is_last_child(self):
|
42
|
+
"""Return True if the current node is the last child."""
|
43
|
+
return self.tn_priority == self.get_siblings_count() - 1
|
44
|
+
|
45
|
+
def is_leaf(self):
|
46
|
+
"""Return True if the current node is a leaf."""
|
47
|
+
return self.tn_children.count() == 0
|
48
|
+
|
49
|
+
def is_parent_of(self, target_obj):
|
50
|
+
"""Return True if the current node is parent of target_obj."""
|
51
|
+
return self == target_obj.tn_parent
|
52
|
+
|
53
|
+
def is_root(self):
|
54
|
+
"""Return True if the current node is root."""
|
55
|
+
return self.tn_parent is None
|
56
|
+
|
57
|
+
def is_root_of(self, target_obj):
|
58
|
+
"""Return True if the current node is root of target_obj."""
|
59
|
+
return self == target_obj.get_root()
|
60
|
+
|
61
|
+
def is_sibling_of(self, target_obj):
|
62
|
+
"""Return True if the current node is sibling of target_obj."""
|
63
|
+
if target_obj.tn_parent is None and self.tn_parent is None:
|
64
|
+
# Both objects are roots
|
65
|
+
return True
|
66
|
+
return (self.tn_parent == target_obj.tn_parent)
|
67
|
+
|
68
|
+
# The End
|