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
@@ -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.11
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 ..cache import cached_method, treenode_cache
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).order_by('depth')
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
- return list(queryset.values_list("child_id", flat=True))
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.firts().parent if queryset.count() > 0 else None
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
- super().save(force_insert, *args, **kwargs)
142
- self._meta.model.clear_cache()
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
@@ -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.0.0
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 # Используем готовый 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