django-fast-treenode 2.1.4__py3-none-any.whl → 3.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 (107) hide show
  1. django_fast_treenode-3.0.0.dist-info/METADATA +203 -0
  2. django_fast_treenode-3.0.0.dist-info/RECORD +90 -0
  3. {django_fast_treenode-2.1.4.dist-info → django_fast_treenode-3.0.0.dist-info}/WHEEL +1 -1
  4. treenode/admin/__init__.py +2 -7
  5. treenode/admin/admin.py +138 -209
  6. treenode/admin/changelist.py +21 -39
  7. treenode/admin/exporter.py +170 -0
  8. treenode/admin/importer.py +171 -0
  9. treenode/admin/mixin.py +291 -0
  10. treenode/apps.py +42 -20
  11. treenode/cache.py +192 -303
  12. treenode/forms.py +45 -65
  13. treenode/managers/__init__.py +4 -20
  14. treenode/managers/managers.py +216 -0
  15. treenode/managers/queries.py +233 -0
  16. treenode/managers/tasks.py +167 -0
  17. treenode/models/__init__.py +8 -5
  18. treenode/models/decorators.py +54 -0
  19. treenode/models/factory.py +44 -68
  20. treenode/models/mixins/__init__.py +2 -1
  21. treenode/models/mixins/ancestors.py +44 -20
  22. treenode/models/mixins/children.py +33 -26
  23. treenode/models/mixins/descendants.py +33 -22
  24. treenode/models/mixins/family.py +25 -15
  25. treenode/models/mixins/logical.py +23 -21
  26. treenode/models/mixins/node.py +162 -104
  27. treenode/models/mixins/properties.py +22 -16
  28. treenode/models/mixins/roots.py +59 -15
  29. treenode/models/mixins/siblings.py +46 -43
  30. treenode/models/mixins/tree.py +212 -153
  31. treenode/models/mixins/update.py +154 -0
  32. treenode/models/models.py +365 -0
  33. treenode/settings.py +28 -0
  34. treenode/static/{treenode/css → css}/tree_widget.css +1 -1
  35. treenode/static/{treenode/css → css}/treenode_admin.css +43 -2
  36. treenode/static/css/treenode_tabs.css +51 -0
  37. treenode/static/js/lz-string.min.js +1 -0
  38. treenode/static/{treenode/js → js}/tree_widget.js +9 -23
  39. treenode/static/js/treenode_admin.js +531 -0
  40. treenode/static/vendors/jquery-ui/AUTHORS.txt +384 -0
  41. treenode/static/vendors/jquery-ui/LICENSE.txt +43 -0
  42. treenode/static/vendors/jquery-ui/external/jquery/jquery.js +10716 -0
  43. treenode/static/vendors/jquery-ui/images/ui-icons_444444_256x240.png +0 -0
  44. treenode/static/vendors/jquery-ui/images/ui-icons_555555_256x240.png +0 -0
  45. treenode/static/vendors/jquery-ui/images/ui-icons_777620_256x240.png +0 -0
  46. treenode/static/vendors/jquery-ui/images/ui-icons_777777_256x240.png +0 -0
  47. treenode/static/vendors/jquery-ui/images/ui-icons_cc0000_256x240.png +0 -0
  48. treenode/static/vendors/jquery-ui/images/ui-icons_ffffff_256x240.png +0 -0
  49. treenode/static/vendors/jquery-ui/index.html +297 -0
  50. treenode/static/vendors/jquery-ui/jquery-ui.css +438 -0
  51. treenode/static/vendors/jquery-ui/jquery-ui.js +5223 -0
  52. treenode/static/vendors/jquery-ui/jquery-ui.min.css +7 -0
  53. treenode/static/vendors/jquery-ui/jquery-ui.min.js +6 -0
  54. treenode/static/vendors/jquery-ui/jquery-ui.structure.css +16 -0
  55. treenode/static/vendors/jquery-ui/jquery-ui.structure.min.css +5 -0
  56. treenode/static/vendors/jquery-ui/jquery-ui.theme.css +439 -0
  57. treenode/static/vendors/jquery-ui/jquery-ui.theme.min.css +5 -0
  58. treenode/static/vendors/jquery-ui/package.json +82 -0
  59. treenode/templates/admin/treenode_changelist.html +25 -0
  60. treenode/templates/admin/treenode_import_export.html +85 -0
  61. treenode/templates/admin/treenode_rows.html +57 -0
  62. treenode/tests.py +3 -0
  63. treenode/urls.py +6 -27
  64. treenode/utils/__init__.py +0 -15
  65. treenode/utils/db/__init__.py +7 -0
  66. treenode/utils/db/compiler.py +114 -0
  67. treenode/utils/db/db_vendor.py +50 -0
  68. treenode/utils/db/service.py +84 -0
  69. treenode/utils/db/sqlcompat.py +60 -0
  70. treenode/utils/db/sqlquery.py +70 -0
  71. treenode/version.py +2 -2
  72. treenode/views/__init__.py +5 -0
  73. treenode/views/autoapi.py +91 -0
  74. treenode/views/autocomplete.py +52 -0
  75. treenode/views/children.py +41 -0
  76. treenode/views/common.py +23 -0
  77. treenode/views/crud.py +209 -0
  78. treenode/views/search.py +48 -0
  79. treenode/widgets.py +27 -44
  80. django_fast_treenode-2.1.4.dist-info/METADATA +0 -166
  81. django_fast_treenode-2.1.4.dist-info/RECORD +0 -63
  82. treenode/admin/mixins.py +0 -302
  83. treenode/managers/adjacency.py +0 -205
  84. treenode/managers/closure.py +0 -278
  85. treenode/models/adjacency.py +0 -342
  86. treenode/models/classproperty.py +0 -27
  87. treenode/models/closure.py +0 -122
  88. treenode/static/treenode/js/.gitkeep +0 -1
  89. treenode/static/treenode/js/treenode_admin.js +0 -131
  90. treenode/templates/admin/export_success.html +0 -26
  91. treenode/templates/admin/tree_node_changelist.html +0 -19
  92. treenode/templates/admin/tree_node_export.html +0 -27
  93. treenode/templates/admin/tree_node_import.html +0 -45
  94. treenode/templates/admin/tree_node_import_report.html +0 -32
  95. treenode/templates/widgets/tree_widget.css +0 -23
  96. treenode/utils/aid.py +0 -46
  97. treenode/utils/base16.py +0 -38
  98. treenode/utils/base36.py +0 -37
  99. treenode/utils/db.py +0 -116
  100. treenode/utils/exporter.py +0 -196
  101. treenode/utils/importer.py +0 -328
  102. treenode/utils/radix.py +0 -61
  103. treenode/views.py +0 -184
  104. {django_fast_treenode-2.1.4.dist-info → django_fast_treenode-3.0.0.dist-info/licenses}/LICENSE +0 -0
  105. {django_fast_treenode-2.1.4.dist-info → django_fast_treenode-3.0.0.dist-info}/top_level.txt +0 -0
  106. /treenode/static/{treenode → css}/.gitkeep +0 -0
  107. /treenode/static/{treenode/css → js}/.gitkeep +0 -0
@@ -0,0 +1,167 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ TreeNode TaskQuery manager
4
+
5
+ Version: 3.0.0
6
+ Author: Timur Kady
7
+ Email: timurkady@yandex.com
8
+ """
9
+
10
+ from django.db import connection
11
+
12
+ from ..utils.db import TreePathCompiler
13
+
14
+ '''
15
+ try:
16
+ profile
17
+ except NameError:
18
+ def profile(func):
19
+ """Profile."""
20
+ return func
21
+ '''
22
+
23
+
24
+ class TreeTaskQueue:
25
+ """TreeTaskQueue Class."""
26
+
27
+ def __init__(self, model):
28
+ """Init the task query."""
29
+ self.model = model
30
+ self.queue = []
31
+
32
+ def add(self, mode, parent_id):
33
+ """Add task to the query."""
34
+ self.queue.append({"mode": mode, "parent_id": parent_id})
35
+
36
+ def run(self):
37
+ """Run task queue."""
38
+ if len(self.queue) == 0:
39
+ return
40
+ try:
41
+ optimized = self._optimize()
42
+ for task in optimized:
43
+ if task["mode"] == "update":
44
+ parent_id = task["parent_id"]
45
+ TreePathCompiler.update_path(
46
+ model=self.model,
47
+ parent_id=parent_id
48
+ )
49
+ finally:
50
+ self.queue.clear()
51
+ self._running = False
52
+
53
+ # @profile
54
+ def _optimize(self):
55
+ """Return optimized task queue (ID-only logic)."""
56
+ result_set = set()
57
+ id_set = set()
58
+
59
+ for task in self.queue:
60
+ if task["mode"] == "update":
61
+ parent_id = task["parent_id"]
62
+ if parent_id is None:
63
+ # If we are already updating the entire tree, then
64
+ # the remaining tasks are meaningless # noqa: D501
65
+ return [{"mode": "update", "parent_id": None}]
66
+ else:
67
+ id_set.add(parent_id)
68
+
69
+ id_list = list(id_set)
70
+
71
+ while id_list:
72
+ current = id_list.pop(0)
73
+ merged = False
74
+ for other in id_list[:]:
75
+ ancestor = self._get_common_ancestor(current, other)
76
+ if ancestor is not None:
77
+ # If the common ancestor is the root, then we update
78
+ # the entire tree
79
+ if ancestor in self._get_root_ids():
80
+ return [{"mode": "update", "parent_id": None}]
81
+ if ancestor not in id_set:
82
+ id_list.append(ancestor)
83
+ id_set.add(ancestor)
84
+ id_list.remove(other)
85
+ merged = True
86
+ break
87
+ if not merged:
88
+ result_set.add(current)
89
+
90
+ return [{"mode": "update", "parent_id": pk} for pk in sorted(result_set)] # noqa: D501
91
+
92
+ def _get_root_ids(self):
93
+ """Return root node IDs."""
94
+ with connection.cursor() as cursor:
95
+ cursor.execute(
96
+ f"SELECT id FROM {self.model._meta.db_table} WHERE parent_id IS NULL") # noqa: D501
97
+ return [row[0] for row in cursor.fetchall()]
98
+
99
+ def _get_parent_id(self, node_id):
100
+ """Return parent ID for a given node."""
101
+ with connection.cursor() as cursor:
102
+ cursor.execute(
103
+ f"SELECT parent_id FROM {self.model._meta.db_table} WHERE id = %s", [node_id]) # noqa: D501
104
+ row = cursor.fetchone()
105
+ return row[0] if row else None
106
+
107
+ '''
108
+ def _get_ancestor_path(self, node_id):
109
+ """Return list of ancestor IDs including the node itself."""
110
+ path = []
111
+ while node_id is not None:
112
+ path.append(node_id)
113
+ node_id = self._get_parent_id(node_id)
114
+ return path[::-1] # root to leaf
115
+ '''
116
+
117
+ # @profile
118
+ def _get_ancestor_path(self, node_id):
119
+ """Return list of ancestor IDs including the node itself, using recursive SQL.""" # noqa: D501
120
+ table = self.model._meta.db_table
121
+
122
+ sql = f"""
123
+ WITH RECURSIVE ancestor_cte AS (
124
+ SELECT id, parent_id, 0 AS depth
125
+ FROM {table}
126
+ WHERE id = %s
127
+
128
+ UNION ALL
129
+
130
+ SELECT t.id, t.parent_id, a.depth + 1
131
+ FROM {table} t
132
+ JOIN ancestor_cte a ON t.id = a.parent_id
133
+ )
134
+ SELECT id FROM ancestor_cte ORDER BY depth DESC
135
+ """
136
+
137
+ with connection.cursor() as cursor:
138
+ cursor.execute(sql, [node_id])
139
+ rows = cursor.fetchall()
140
+
141
+ return [row[0] for row in rows]
142
+
143
+ # @profile
144
+ def _get_common_ancestor(self, id1, id2):
145
+ """Return common ancestor ID between two nodes."""
146
+ path1 = self._get_ancestor_path(id1)
147
+ path2 = self._get_ancestor_path(id2)
148
+ common = None
149
+ for a, b in zip(path1, path2):
150
+ if a == b:
151
+ common = a
152
+ else:
153
+ break
154
+ return common
155
+
156
+
157
+ class TreeTaskManager:
158
+ """Handle to TreeTaskQueue."""
159
+
160
+ def __get__(self, instance, owner):
161
+ """Get query for instance."""
162
+ if not hasattr(owner, "_task_queue"):
163
+ owner._task_queue = TreeTaskQueue(owner)
164
+ return owner._task_queue
165
+
166
+
167
+ # The End
@@ -1,8 +1,11 @@
1
1
  # -*- coding: utf-8 -*-
2
- from .adjacency import TreeNodeModel
2
+ """
3
+ The TreeNode Model
3
4
 
5
+ Version: 3.0.0
6
+ Author: Timur Kady
7
+ Email: timurkady@yandex.com
8
+ """
9
+ from .models import TreeNodeModel
4
10
 
5
- __all__ = ["TreeNodeModel",]
6
-
7
-
8
- # The End
11
+ __all__ = ["TreeNodeModel"]
@@ -0,0 +1,54 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ TreeeNodeModel Class Decorators
4
+
5
+ - Decorator `@cached_method` for caching method results.
6
+
7
+ Version: 3.0.0
8
+ Author: Timur Kady
9
+ Email: timurkady@yandex.com
10
+ """
11
+
12
+
13
+ _TIMEOUT = 10
14
+ _INTERVAL = 0.2
15
+
16
+ _UNSET = object()
17
+
18
+
19
+ class classproperty(object):
20
+ """Classproperty class."""
21
+
22
+ def __init__(self, getter):
23
+ """Init."""
24
+ self.getter = getter
25
+
26
+ def __get__(self, instance, owner):
27
+ """Get."""
28
+ return self.getter(owner)
29
+
30
+
31
+ def lazy_property(source, default=_UNSET):
32
+ """
33
+ Декоратор ленивого свойства, которое берёт значение из другого поля.
34
+
35
+ source — имя поля (например, 'parent_id')
36
+ default — значение по умолчанию. Если не задан, берётся self.source
37
+ """
38
+ def decorator(func):
39
+ def getter(self):
40
+ attr_name = '_' + func.__name__
41
+ if not hasattr(self, attr_name):
42
+ if default is _UNSET:
43
+ value = getattr(self, source)
44
+ else:
45
+ value = default
46
+ setattr(self, attr_name, value)
47
+ return getattr(self, attr_name)
48
+
49
+ def setter(self, value):
50
+ attr_name = '_' + func.__name__
51
+ setattr(self, attr_name, value)
52
+
53
+ return property(getter, setter)
54
+ return decorator
@@ -1,81 +1,57 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """
3
- TreeNode Factory for Closure Table
3
+ TreeNode Factory
4
4
 
5
- This module provides a metaclass `TreeFactory` that automatically binds
6
- a model to a Closure Table for hierarchical data storage.
5
+ This module provides a metaclass that automatically associates a model with
6
+ a service table and creates a set of indexes for the database
7
7
 
8
8
  Features:
9
- - Ensures non-abstract, non-proxy models get a corresponding Closure Table.
10
- - Dynamically creates and assigns a Closure Model for each TreeNodeModel.
11
- - Facilitates the management of hierarchical relationships.
9
+ - Dynamically creates and assigns a service model.
10
+ - Facilitates the formation of indexes taking into account the DB vendor.
12
11
 
13
- Version: 2.1.0
12
+ Version: 3.0.0
14
13
  Author: Timur Kady
15
14
  Email: timurkady@yandex.com
16
15
  """
17
16
 
18
17
 
19
- import sys
20
- from django.db import models
21
- from .closure import ClosureModel
22
-
23
-
24
- class TreeFactory(models.base.ModelBase):
25
- """
26
- Metaclass for binding a model to a Closure Table.
27
-
28
- For each non-abstract, non-proxy, and "top" (without parents) model,
29
- assigns the `ClosureModel` as the closure table.
30
- """
31
-
32
- def __init__(cls, name, bases, dct):
33
- """Class initialization.
34
-
35
- We check that the model:
36
- - is not abstract
37
- - is not a proxy
38
- - is not a child
39
- and only then assign the ClosureModel.
40
- """
41
- super().__init__(name, bases, dct)
42
-
43
- if (cls._meta.abstract or cls._meta.proxy or
44
- cls._meta.get_parent_list()):
45
- return
46
-
47
- closure_name = f"{cls._meta.object_name}ClosureModel"
48
- if getattr(cls, "closure_model", None) is not None:
49
- return
50
-
51
- fields = {
52
- "parent": models.ForeignKey(
53
- cls._meta.model,
54
- related_name="children_set",
55
- on_delete=models.CASCADE
56
- ),
57
-
58
- "child": models.ForeignKey(
59
- cls._meta.model,
60
- related_name="parents_set",
61
- on_delete=models.CASCADE,
62
- ),
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
-
72
- "__module__": cls.__module__
73
- }
74
-
75
- closure_model = type(closure_name, (ClosureModel,), fields)
76
- setattr(sys.modules[cls.__module__], closure_name, closure_model)
77
-
78
- cls.closure_model = closure_model
79
-
18
+ from django.db import models, connection
19
+ from django.db.models import Func, F
20
+
21
+
22
+ class TreeNodeModelBase(models.base.ModelBase):
23
+ """Base Class for TreeNodeModel."""
24
+
25
+ def __new__(mcls, name, bases, attrs, **kwargs):
26
+ """Create a New Class."""
27
+ new_class = super().__new__(mcls, name, bases, attrs, **kwargs)
28
+ if not new_class._meta.abstract:
29
+ class_name = name.lower()
30
+ # Create an index with the desired name
31
+ """Set DB Indexes with unique names per model."""
32
+ vendor = connection.vendor
33
+ indexes = []
34
+
35
+ if vendor == 'postgresql':
36
+ indexes.append(models.Index(
37
+ fields=['_path'],
38
+ name=f'idx_{class_name}_path_ops',
39
+ opclasses=['text_pattern_ops']
40
+ ))
41
+ elif vendor in {'mysql'}:
42
+ indexes.append(models.Index(
43
+ Func(F('_path'), function='md5'),
44
+ name=f'idx_{class_name}_path_hash'
45
+ ))
46
+ else:
47
+ indexes.append(models.Index(
48
+ fields=['_path'],
49
+ name=f'idx_{class_name}_path'
50
+ ))
51
+
52
+ # Update the list of indexes
53
+ new_class._meta.indexes += indexes
54
+
55
+ return new_class
80
56
 
81
57
  # The End
@@ -10,13 +10,14 @@ from .properties import TreeNodePropertiesMixin
10
10
  from .roots import TreeNodeRootsMixin
11
11
  from .siblings import TreeNodeSiblingsMixin
12
12
  from .tree import TreeNodeTreeMixin
13
+ from .update import RawSQLMixin
13
14
 
14
15
 
15
16
  __all__ = [
16
17
  "TreeNodeAncestorsMixin", "TreeNodeChildrenMixin", "TreeNodeFamilyMixin",
17
18
  "TreeNodeDescendantsMixin", "TreeNodeLogicalMixin", "TreeNodeNodeMixin",
18
19
  "TreeNodePropertiesMixin", "TreeNodeRootsMixin", "TreeNodeSiblingsMixin",
19
- "TreeNodeTreeMixin"
20
+ "TreeNodeTreeMixin", "RawSQLMixin"
20
21
  ]
21
22
 
22
23
 
@@ -2,14 +2,13 @@
2
2
  """
3
3
  TreeNode Ancestors Mixin
4
4
 
5
- Version: 2.1.4
5
+ Version: 3.0.0
6
6
  Author: Timur Kady
7
7
  Email: timurkady@yandex.com
8
8
  """
9
9
 
10
10
  from django.db import models
11
- from django.db.models import OuterRef, Subquery, IntegerField, Case, When, Value
12
- from ...cache import treenode_cache, cached_method
11
+ from ...cache import cached_method
13
12
 
14
13
 
15
14
  class TreeNodeAncestorsMixin(models.Model):
@@ -20,29 +19,54 @@ class TreeNodeAncestorsMixin(models.Model):
20
19
 
21
20
  abstract = True
22
21
 
23
- @cached_method
24
- def get_ancestors_queryset(self, include_self=True, depth=None):
25
- """Get the ancestors queryset (ordered from root to parent)."""
26
- options = dict(child_id=self.pk, depth__gte=0 if include_self else 1)
27
- if depth:
28
- options.update({'depth__lte': depth})
29
-
30
- return self.closure_model.objects\
31
- .filter(**options)\
32
- .order_by('-depth')
22
+ def get_ancestors_queryset(self, include_self=True):
23
+ """Get all ancestors of a node."""
24
+ pks = self.query("ancestors", include_self)
25
+ return self._meta.model.objects.filter(pk__in=pks)
33
26
 
34
27
  @cached_method
35
28
  def get_ancestors_pks(self, include_self=True, depth=None):
36
29
  """Get the ancestors pks list."""
37
- return self.get_ancestors_queryset(include_self, depth)\
38
- .values_list('id', flat=True)
30
+ return self.query("ancestors", include_self)
39
31
 
40
- def get_ancestors(self, include_self=True, depth=None):
41
- """Get a list with all ancestors (ordered from root to self/parent)."""
42
- return list(self.get_ancestors_queryset(include_self, depth))
32
+ @cached_method
33
+ def get_ancestors(self, include_self=True):
34
+ """Get a list of all ancestors of a node."""
35
+ node = self if include_self else self.parent
36
+ ancestors = []
37
+ while node:
38
+ ancestors.append(node)
39
+ node = node.parent
40
+ return ancestors[::-1]
43
41
 
44
- def get_ancestors_count(self, include_self=True, depth=None):
42
+ @cached_method
43
+ def get_ancestors_count(self, include_self=True):
45
44
  """Get the ancestors count."""
46
- return len(self.get_ancestors_pks(include_self, depth))
45
+ return self.query(
46
+ objects="ancestors",
47
+ include_self=include_self,
48
+ mode='count'
49
+ )
50
+
51
+ def get_common_ancestor(self, target):
52
+ """Find lowest common ancestor between self and other node."""
53
+ if self._path == target._path:
54
+ return self
55
+
56
+ self_path_pks = self.query("ancestors")
57
+ target_path_pks = target.query("ancestors")
58
+ common = []
59
+
60
+ for a, b in zip(self_path_pks, target_path_pks):
61
+ if a == b:
62
+ common.append(a)
63
+ else:
64
+ break
65
+
66
+ if not common:
67
+ return None
68
+
69
+ ancestor_id = common[-1]
70
+ return self._meta.model.objects.get(pk=ancestor_id)
47
71
 
48
72
  # The End
@@ -2,13 +2,23 @@
2
2
  """
3
3
  TreeNode Children Mixin
4
4
 
5
- Version: 2.1.0
5
+ Version: 3.0.0
6
6
  Author: Timur Kady
7
7
  Email: timurkady@yandex.com
8
8
  """
9
9
 
10
10
  from django.db import models
11
- from treenode.cache import cached_method
11
+ from ...cache import cached_method
12
+
13
+
14
+ '''
15
+ try:
16
+ profile
17
+ except NameError:
18
+ def profile(func):
19
+ """Profile."""
20
+ return func
21
+ '''
12
22
 
13
23
 
14
24
  class TreeNodeChildrenMixin(models.Model):
@@ -36,47 +46,44 @@ class TreeNodeChildrenMixin(models.Model):
36
46
  Returns:
37
47
  The created node object. It will be save()d by this method.
38
48
  """
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
49
  instance = kwargs.get("instance")
48
50
  if instance is None:
49
51
  instance = self._meta.model(**kwargs)
50
- instance.tn_parent = parent
51
- instance.tn_priority = priority
52
- instance.save()
53
- return instance
54
52
 
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))
53
+ parent, priority = self._meta.model._get_place(self, position)
54
+
55
+ instance.parent = self
56
+ instance.priority = priority
57
+ instance.save()
59
58
 
60
- @cached_method
61
59
  def get_children_queryset(self):
62
- """Get the children queryset with prefetch."""
63
- # return self.tn_children.prefetch_related('tn_children')
64
- return self._meta.model.objects.filter(tn_parent__pk=self.id)
60
+ """Get the children queryset."""
61
+ return self._meta.model.objects.filter(parent_id=self.id)
65
62
 
63
+ @cached_method
66
64
  def get_children(self):
67
65
  """Get a list containing all children."""
68
- return list(self.get_children_queryset())
66
+ queryset = self._meta.model.objects.filter(parent_id=self.id)
67
+ return list(queryset)
69
68
 
69
+ @cached_method
70
+ def get_children_pks(self):
71
+ """Get the children pks list."""
72
+ return self.query("children")
73
+
74
+ @cached_method
70
75
  def get_children_count(self):
71
76
  """Get the children count."""
72
- return len(self.get_children_pks())
77
+ return self.query(objects="children", mode='count')
73
78
 
79
+ @cached_method
74
80
  def get_first_child(self):
75
81
  """Get the first child node or None if it has no children."""
76
- return self.get_children_queryset().first() if self.is_leaf else None
82
+ return self.get_children_queryset().first()
77
83
 
84
+ @cached_method
78
85
  def get_last_child(self):
79
86
  """Get the last child node or None if it has no children."""
80
- return self.get_children_queryset().last() if self.is_leaf else None
87
+ return self.get_children_queryset().last()
81
88
 
82
89
  # The End
@@ -2,15 +2,23 @@
2
2
  """
3
3
  TreeNode Descendants Mixin
4
4
 
5
- Version: 2.1.0
5
+ Version: 3.0.0
6
6
  Author: Timur Kady
7
7
  Email: timurkady@yandex.com
8
8
  """
9
9
 
10
10
  from django.db import models
11
- from django.db.models import OuterRef, Subquery, Min
11
+ from ...cache import cached_method
12
12
 
13
- from treenode.cache import treenode_cache, cached_method
13
+
14
+ '''
15
+ try:
16
+ profile
17
+ except NameError:
18
+ def profile(func):
19
+ """Profile."""
20
+ return func
21
+ '''
14
22
 
15
23
 
16
24
  class TreeNodeDescendantsMixin(models.Model):
@@ -21,40 +29,43 @@ class TreeNodeDescendantsMixin(models.Model):
21
29
 
22
30
  abstract = True
23
31
 
24
- @cached_method
25
32
  def get_descendants_queryset(self, include_self=False, depth=None):
26
33
  """Get the descendants queryset."""
27
- Closure = self.closure_model
28
- desc_qs = Closure.objects.filter(child=OuterRef('pk'), parent=self.pk)
29
- desc_qs = desc_qs.values('child').annotate(
30
- mdepth=Min('depth')).values('mdepth')[:1]
31
-
32
- queryset = self._meta.model.objects.annotate(
33
- min_depth=Subquery(desc_qs)
34
- ).filter(min_depth__isnull=False)
35
-
36
- if depth is not None:
37
- queryset = queryset.filter(min_depth__lte=depth)
34
+ path = self.get_order() # calls refresh and gets the current _path
35
+ from_path = path + '.'
36
+ to_path = path + '/'
37
+ options = {'_path__gte': from_path, '_path__lt': to_path}
38
+ if depth:
39
+ options["_depth__lt"] = depth
40
+ queryset = self._meta.model.objects.filter(**options)
38
41
 
39
- # add self if needed
40
42
  if include_self:
41
- queryset = queryset | self._meta.model.objects.filter(pk=self.pk)
42
-
43
- return queryset.order_by('min_depth', 'tn_priority')
43
+ return self._meta.model.objects.filter(pk=self.pk) | queryset
44
+ else:
45
+ return queryset
44
46
 
45
47
  @cached_method
46
48
  def get_descendants_pks(self, include_self=False, depth=None):
47
49
  """Get the descendants pks list."""
48
- return self.get_descendants_queryset(include_self, depth)\
49
- .values_list("id", flat=True)
50
+ return self.query("descendants", include_self)
50
51
 
52
+ # @profile
53
+ @cached_method
51
54
  def get_descendants(self, include_self=False, depth=None):
52
55
  """Get a list containing all descendants."""
56
+ # descendants_pks = self.query("descendants", include_self)
57
+ # queryset = self._meta.model.objects.filter(pk__in=descendants_pks)
53
58
  queryset = self.get_descendants_queryset(include_self, depth)
54
59
  return list(queryset)
55
60
 
61
+ @cached_method
56
62
  def get_descendants_count(self, include_self=False, depth=None):
57
63
  """Get the descendants count."""
58
- return len(self.get_descendants_pks(include_self, depth))
64
+ return self.query(
65
+ objects="descendants",
66
+ include_self=include_self,
67
+ mode='count'
68
+ )
69
+
59
70
 
60
71
  # The End