django-fast-treenode 3.2.0__tar.gz → 3.2.2__tar.gz
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-3.2.0/django_fast_treenode.egg-info → django_fast_treenode-3.2.2}/PKG-INFO +1 -1
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2/django_fast_treenode.egg-info}/PKG-INFO +1 -1
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/django_fast_treenode.egg-info/SOURCES.txt +1 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/docs/api.md +23 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/docs/migration.md +1 -1
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/pyproject.toml +1 -1
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/setup.py +1 -1
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/tests/test_suite.py +11 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/managers/tasks.py +19 -1
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/models/mixins/__init__.py +3 -1
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/models/mixins/children.py +1 -1
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/models/mixins/descendants.py +1 -1
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/models/mixins/roots.py +1 -1
- django_fast_treenode-3.2.2/treenode/models/mixins/search.py +55 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/models/mixins/siblings.py +1 -1
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/models/models.py +17 -5
- django_fast_treenode-3.2.2/treenode/utils/db/compiler.py +72 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/version.py +2 -2
- django_fast_treenode-3.2.0/treenode/utils/db/compiler.py +0 -113
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/LICENSE +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/MANIFEST.in +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/README.md +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/django_fast_treenode.egg-info/dependency_links.txt +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/django_fast_treenode.egg-info/requires.txt +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/django_fast_treenode.egg-info/top_level.txt +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/docs/.gitignore +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/docs/.nojekyll +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/docs/about.md +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/docs/admin.md +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/docs/apifirst.md +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/docs/cache.md +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/docs/customization.md +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/docs/dnd.md +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/docs/import_export.md +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/docs/index.md +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/docs/insert-after.jpg +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/docs/insert-as-child.jpg +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/docs/installation.md +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/docs/models.md +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/docs/requirements.txt +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/docs/roadmap.md +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/docs/using.md +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/setup.cfg +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/__init__.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/admin/__init__.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/admin/admin.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/admin/changelist.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/admin/exporter.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/admin/importer.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/admin/mixin.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/apps.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/cache.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/forms.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/managers/__init__.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/managers/managers.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/managers/queries.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/models/__init__.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/models/decorators.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/models/factory.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/models/mixins/ancestors.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/models/mixins/family.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/models/mixins/logical.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/models/mixins/node.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/models/mixins/properties.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/models/mixins/tree.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/models/mixins/update.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/settings.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/signals.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/.gitkeep +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/.gitkeep +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/css/.gitkeep +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/css/tree_widget.css +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/css/treenode_admin.css +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/css/treenode_tabs.css +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/js/.gitkeep +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/js/lz-string.min.js +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/js/tree_widget.js +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/js/treenode_admin.js +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/vendors/.gitkeep +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/vendors/jquery-ui/.gitkeep +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/vendors/jquery-ui/AUTHORS.txt +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/vendors/jquery-ui/LICENSE.txt +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/vendors/jquery-ui/external/jquery/jquery.js +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/vendors/jquery-ui/images/ui-icons_444444_256x240.png +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/vendors/jquery-ui/images/ui-icons_555555_256x240.png +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/vendors/jquery-ui/images/ui-icons_777620_256x240.png +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/vendors/jquery-ui/images/ui-icons_777777_256x240.png +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/vendors/jquery-ui/images/ui-icons_cc0000_256x240.png +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/vendors/jquery-ui/images/ui-icons_ffffff_256x240.png +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/vendors/jquery-ui/index.html +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/vendors/jquery-ui/jquery-ui.css +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/vendors/jquery-ui/jquery-ui.js +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/vendors/jquery-ui/jquery-ui.min.css +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/vendors/jquery-ui/jquery-ui.min.js +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/vendors/jquery-ui/jquery-ui.structure.css +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/vendors/jquery-ui/jquery-ui.structure.min.css +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/vendors/jquery-ui/jquery-ui.theme.css +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/vendors/jquery-ui/jquery-ui.theme.min.css +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/vendors/jquery-ui/package.json +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/templates/.gitkeep +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/templates/treenode/.gitkeep +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/templates/treenode/admin/.gitkeep +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/templates/treenode/admin/treenode_ajax_rows.html +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/templates/treenode/admin/treenode_changelist.html +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/templates/treenode/admin/treenode_import_export.html +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/templates/treenode/admin/treenode_rows.html +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/templates/treenode/widgets/tree_widget.html +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/templatetags/__init__.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/templatetags/treenode_admin.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/tests.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/urls.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/utils/__init__.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/utils/db/__init__.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/utils/db/db_vendor.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/utils/db/service.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/utils/db/sqlcompat.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/utils/db/sqlquery.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/utils/jwt_auth.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/views/__init__.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/views/autoapi.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/views/autocomplete.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/views/children.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/views/common.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/views/crud.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/views/search.py +0 -0
- {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/widgets.py +0 -0
{django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/django_fast_treenode.egg-info/SOURCES.txt
RENAMED
@@ -61,6 +61,7 @@ treenode/models/mixins/logical.py
|
|
61
61
|
treenode/models/mixins/node.py
|
62
62
|
treenode/models/mixins/properties.py
|
63
63
|
treenode/models/mixins/roots.py
|
64
|
+
treenode/models/mixins/search.py
|
64
65
|
treenode/models/mixins/siblings.py
|
65
66
|
treenode/models/mixins/tree.py
|
66
67
|
treenode/models/mixins/update.py
|
@@ -11,6 +11,7 @@ The API is divided into several logical groups, each serving a specific purpose:
|
|
11
11
|
- **[Descendant Methods](#descendant-methods)** – Work with entire subtrees of nodes.
|
12
12
|
- **[Family Methods](#family-methods)** – Retrieve and analyze relationships within a node's family (ancestors, siblings, descendants).
|
13
13
|
- **[Node Utility Methods](#node-utility-methods)** – Additional methods for retrieving node order, paths, levels, and priorities.
|
14
|
+
- **[Search Methods](#search-methods)** – Quick node lookup utilities.
|
14
15
|
- **[Root Node Methods](#root-node-methods)** – Manage and retrieve root nodes of trees.
|
15
16
|
- **[Sibling Methods](#sibling-methods)** – Handle relationships between sibling nodes.
|
16
17
|
- **[Tree Methods](#tree-methods)** – Serialize and manipulate the entire tree structure, including JSON export/import.
|
@@ -351,6 +352,28 @@ obj.get_root_pk()
|
|
351
352
|
|
352
353
|
---
|
353
354
|
|
355
|
+
### Search Methods
|
356
|
+
These methods provide convenient helpers for finding nodes by
|
357
|
+
breadcrumb paths or within a subtree.
|
358
|
+
|
359
|
+
#### find_by_path
|
360
|
+
Find a node by a path previously generated via `get_breadcrumbs`.
|
361
|
+
```python
|
362
|
+
cls.find_by_path("root/A/D", attr="name", delimiter="/")
|
363
|
+
```
|
364
|
+
|
365
|
+
Returns the matching node or `None`.
|
366
|
+
|
367
|
+
#### find_in_subtree
|
368
|
+
Search for a node among the descendants of a parent node.
|
369
|
+
```python
|
370
|
+
cls.find_in_subtree(parent, "D", attr="name")
|
371
|
+
```
|
372
|
+
|
373
|
+
Returns the first found node or `None`.
|
374
|
+
|
375
|
+
---
|
376
|
+
|
354
377
|
### Root Node Methods
|
355
378
|
These methods allow managing **root nodes** efficiently. They provide retrieval, counting, and manipulation of the first and last root nodes in the tree.
|
356
379
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "django-fast-treenode"
|
7
|
-
version = "3.2.
|
7
|
+
version = "3.2.2"
|
8
8
|
description = "Treenode Framework for supporting tree (hierarchical) data structure in Django projects"
|
9
9
|
readme = "README.md"
|
10
10
|
authors = [{ name = "Timur Kady", email = "timurkady@yandex.com" }]
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
2
2
|
|
3
3
|
setup(
|
4
4
|
name='django-fast-treenode',
|
5
|
-
version='3.2.
|
5
|
+
version='3.2.2',
|
6
6
|
description='Treenode Framework for supporting tree (hierarchical) data structure in Django projects',
|
7
7
|
long_description=open('README.md', encoding='utf-8').read(),
|
8
8
|
long_description_content_type='text/markdown',
|
@@ -93,3 +93,14 @@ class TreeNodeModelTests(TestCase):
|
|
93
93
|
qs = TestModel.objects.filter(pk__in=[self.a.pk, self.c.pk]).all()
|
94
94
|
self.assertFalse(TestModel.objects.filter(pk=self.a.pk).exists())
|
95
95
|
self.assertTrue(TestModel.objects.filter(pk=self.c.pk).exists())
|
96
|
+
|
97
|
+
# --- 6. Search ----------------------------------------------------------
|
98
|
+
|
99
|
+
def test_find_by_path(self):
|
100
|
+
path = "/".join(self.d.get_breadcrumbs(attr="name"))
|
101
|
+
node = TestModel.find_by_path(path, attr="name", delimiter="/")
|
102
|
+
self.assertEqual(node, self.d)
|
103
|
+
|
104
|
+
def test_find_in_subtree(self):
|
105
|
+
node = TestModel.find_in_subtree(self.a, "D", attr="name")
|
106
|
+
self.assertEqual(node, self.d)
|
@@ -137,7 +137,25 @@ class TreeTaskQueue:
|
|
137
137
|
if not merged:
|
138
138
|
result_set.add(current)
|
139
139
|
|
140
|
-
|
140
|
+
if not result_set:
|
141
|
+
return []
|
142
|
+
|
143
|
+
depth_lookup = {}
|
144
|
+
if result_set:
|
145
|
+
with connection.cursor() as cursor:
|
146
|
+
format_ids = ', '.join(['%s'] * len(result_set))
|
147
|
+
sql = f"""
|
148
|
+
SELECT id, _depth
|
149
|
+
FROM {self.model._meta.db_table}
|
150
|
+
WHERE id IN ({format_ids})
|
151
|
+
"""
|
152
|
+
cursor.execute(sql, list(result_set))
|
153
|
+
for node_id, depth in cursor.fetchall():
|
154
|
+
depth_lookup[node_id] = depth
|
155
|
+
|
156
|
+
|
157
|
+
ordered_pks = sorted(result_set, key=lambda pk: (depth_lookup.get(pk, 0), pk))
|
158
|
+
return [{"mode": "update", "parent_id": pk} for pk in ordered_pks]
|
141
159
|
|
142
160
|
def _get_root_ids(self):
|
143
161
|
"""Return root node IDs."""
|
{django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/models/mixins/__init__.py
RENAMED
@@ -7,6 +7,7 @@ from .family import TreeNodeFamilyMixin
|
|
7
7
|
from .logical import TreeNodeLogicalMixin
|
8
8
|
from .node import TreeNodeNodeMixin
|
9
9
|
from .properties import TreeNodePropertiesMixin
|
10
|
+
from .search import TreeNodeSearchMixin
|
10
11
|
from .roots import TreeNodeRootsMixin
|
11
12
|
from .siblings import TreeNodeSiblingsMixin
|
12
13
|
from .tree import TreeNodeTreeMixin
|
@@ -16,7 +17,8 @@ from .update import RawSQLMixin
|
|
16
17
|
__all__ = [
|
17
18
|
"TreeNodeAncestorsMixin", "TreeNodeChildrenMixin", "TreeNodeFamilyMixin",
|
18
19
|
"TreeNodeDescendantsMixin", "TreeNodeLogicalMixin", "TreeNodeNodeMixin",
|
19
|
-
"
|
20
|
+
"TreeNodeSearchMixin", "TreeNodePropertiesMixin", "TreeNodeRootsMixin",
|
21
|
+
"TreeNodeSiblingsMixin", "TreeNodeTreeMixin", "RawSQLMixin"
|
20
22
|
"TreeNodeTreeMixin", "RawSQLMixin"
|
21
23
|
]
|
22
24
|
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Search Mixin
|
4
|
+
|
5
|
+
Version: 3.2.1
|
6
|
+
Author: Timur Kady
|
7
|
+
Email: timurkady@yandex.com
|
8
|
+
"""
|
9
|
+
|
10
|
+
from django.db import models
|
11
|
+
|
12
|
+
|
13
|
+
class TreeNodeSearchMixin(models.Model):
|
14
|
+
"""Mixin that provides helper methods for quick node lookup."""
|
15
|
+
|
16
|
+
class Meta:
|
17
|
+
"""Mixin Meta Class."""
|
18
|
+
abstract = True
|
19
|
+
|
20
|
+
@classmethod
|
21
|
+
def find_by_path(cls, path, attr="id", delimiter="/"):
|
22
|
+
"""Return a node referenced by breadcrumbs path.
|
23
|
+
|
24
|
+
The ``path`` argument may be a string or an iterable of breadcrumb
|
25
|
+
values previously produced by :py:meth:`get_breadcrumbs`.
|
26
|
+
If the path cannot be resolved, ``None`` is returned.
|
27
|
+
"""
|
28
|
+
if path is None:
|
29
|
+
return None
|
30
|
+
|
31
|
+
if not isinstance(path, (list, tuple)):
|
32
|
+
path = [p for p in str(path).strip(delimiter).split(delimiter) if p]
|
33
|
+
|
34
|
+
parent = None
|
35
|
+
for token in path:
|
36
|
+
lookup = {attr: token}
|
37
|
+
if parent is None:
|
38
|
+
qs = cls.objects.filter(parent_id__isnull=True, **lookup)
|
39
|
+
else:
|
40
|
+
qs = cls.objects.filter(parent=parent, **lookup)
|
41
|
+
parent = qs.first()
|
42
|
+
if parent is None:
|
43
|
+
return None
|
44
|
+
return parent
|
45
|
+
|
46
|
+
@classmethod
|
47
|
+
def find_in_subtree(cls, parent, value, attr="id"):
|
48
|
+
"""Search ``parent`` descendants for attribute ``attr`` equal to ``value``."""
|
49
|
+
if parent is None:
|
50
|
+
return None
|
51
|
+
prefix = parent.get_order() + "."
|
52
|
+
return cls.objects.filter(_path__startswith=prefix, **{attr: value}).first()
|
53
|
+
|
54
|
+
|
55
|
+
# The End
|
@@ -45,8 +45,9 @@ class TreeNodeModel(
|
|
45
45
|
mx.TreeNodeAncestorsMixin, mx.TreeNodeChildrenMixin,
|
46
46
|
mx.TreeNodeFamilyMixin, mx.TreeNodeDescendantsMixin,
|
47
47
|
mx.TreeNodeLogicalMixin, mx.TreeNodeNodeMixin,
|
48
|
-
mx.
|
49
|
-
mx.
|
48
|
+
mx.TreeNodeSearchMixin, mx.TreeNodePropertiesMixin,
|
49
|
+
mx.TreeNodeRootsMixin, mx.TreeNodeSiblingsMixin,
|
50
|
+
mx.TreeNodeTreeMixin, mx.RawSQLMixin,
|
50
51
|
models.Model, metaclass=TreeNodeModelBase):
|
51
52
|
"""
|
52
53
|
Abstract tree node model.
|
@@ -186,6 +187,7 @@ class TreeNodeModel(
|
|
186
187
|
is_new = False
|
187
188
|
is_shift = False
|
188
189
|
is_move = False
|
190
|
+
sorting_changed = False
|
189
191
|
|
190
192
|
if self.pk:
|
191
193
|
state = self.get_db_state()
|
@@ -194,6 +196,14 @@ class TreeNodeModel(
|
|
194
196
|
is_move = self.parent_id != state["parent_id"]
|
195
197
|
if is_move:
|
196
198
|
self._meta.model.tasks.add("update", state["parent_id"])
|
199
|
+
|
200
|
+
if self.sorting_field != "priority":
|
201
|
+
old_value = self.__class__.objects.filter(
|
202
|
+
pk=self.pk
|
203
|
+
).values_list(self.sorting_field, flat=True).first()
|
204
|
+
sorting_changed = old_value != getattr(
|
205
|
+
self, self.sorting_field
|
206
|
+
)
|
197
207
|
else:
|
198
208
|
logger.error(
|
199
209
|
"TreeNodeModel save error: object with pk %s not found in DB",
|
@@ -231,12 +241,14 @@ class TreeNodeModel(
|
|
231
241
|
pass
|
232
242
|
"""
|
233
243
|
|
234
|
-
if is_new or is_move or is_shift:
|
244
|
+
if is_new or is_move or is_shift or sorting_changed:
|
235
245
|
# Step 1: Shift siblings
|
236
246
|
if (is_new or is_move) and (self.priority is not None):
|
237
247
|
self._shift_siblings_forward()
|
238
248
|
# Step 2: Update paths for the new parent -> sqlq
|
239
|
-
self.
|
249
|
+
path_ids = self.query.get_relative_pks(objects="ancestors", include_self=True)
|
250
|
+
update_root_id = path_ids[0] if path_ids else self.parent_id
|
251
|
+
self._meta.model.tasks.add("update", update_root_id)
|
240
252
|
# Step 3: Clear model cache
|
241
253
|
self.clear_cache()
|
242
254
|
|
@@ -245,7 +257,7 @@ class TreeNodeModel(
|
|
245
257
|
disable_signals(post_save, model)):
|
246
258
|
super().save(*args, **kwargs)
|
247
259
|
|
248
|
-
if is_new or is_move or is_shift:
|
260
|
+
if is_new or is_move or is_shift or sorting_changed:
|
249
261
|
# Run sql
|
250
262
|
# self.sqlq.flush()
|
251
263
|
setattr(model, 'is_dry', True)
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
Tree update task compiler class.
|
4
|
+
|
5
|
+
Compiles tasks to low-level SQL to update the materialized path (_path), depth
|
6
|
+
(_depth), and node order (priority) when they are shifted or moved.
|
7
|
+
|
8
|
+
Version: 3.1.0
|
9
|
+
Author: Timur Kady
|
10
|
+
Email: timurkady@yandex.com
|
11
|
+
"""
|
12
|
+
|
13
|
+
from django.db import connection
|
14
|
+
|
15
|
+
from ...settings import SEGMENT_LENGTH
|
16
|
+
from .sqlcompat import SQLCompat
|
17
|
+
|
18
|
+
|
19
|
+
class TreePathCompiler:
|
20
|
+
"""
|
21
|
+
Tree Task compiler class.
|
22
|
+
|
23
|
+
Efficient, ORM-free computation of _path, _depth and priority
|
24
|
+
for tree structures based on Materialized Path.
|
25
|
+
"""
|
26
|
+
|
27
|
+
@classmethod
|
28
|
+
def update_path(cls, model, parent_id=None):
|
29
|
+
"""Rebuild subtree using BFS so parents update before children."""
|
30
|
+
table = model._meta.db_table
|
31
|
+
sort_field = model.sorting_field
|
32
|
+
|
33
|
+
def fetch_children(pid):
|
34
|
+
if pid is None:
|
35
|
+
where = "parent_id IS NULL"
|
36
|
+
params = []
|
37
|
+
else:
|
38
|
+
where = "parent_id = %s"
|
39
|
+
params = [pid]
|
40
|
+
with connection.cursor() as cursor:
|
41
|
+
cursor.execute(
|
42
|
+
f"SELECT id FROM {table} WHERE {where} ORDER BY {sort_field}, id",
|
43
|
+
params,
|
44
|
+
)
|
45
|
+
return [row[0] for row in cursor.fetchall()]
|
46
|
+
|
47
|
+
queue = []
|
48
|
+
|
49
|
+
if parent_id is None:
|
50
|
+
for idx, node_id in enumerate(fetch_children(None)):
|
51
|
+
queue.append((node_id, "", 0, idx))
|
52
|
+
else:
|
53
|
+
parent_data = model.objects.filter(pk=parent_id).values("_path", "_depth").first()
|
54
|
+
parent_path = parent_data["_path"] if parent_data else ""
|
55
|
+
depth = (parent_data["_depth"] + 1) if parent_data else 0
|
56
|
+
for idx, node_id in enumerate(fetch_children(parent_id)):
|
57
|
+
queue.append((node_id, parent_path, depth, idx))
|
58
|
+
|
59
|
+
while queue:
|
60
|
+
node_id, base_path, depth, index = queue.pop(0)
|
61
|
+
segment = f"{index:0{SEGMENT_LENGTH}X}"
|
62
|
+
path = segment if base_path == "" else f"{base_path}.{segment}"
|
63
|
+
with connection.cursor() as cursor:
|
64
|
+
cursor.execute(
|
65
|
+
f"UPDATE {table} SET priority=%s, _path=%s, _depth=%s WHERE id=%s",
|
66
|
+
[index, path, depth, node_id],
|
67
|
+
)
|
68
|
+
for child_idx, child_id in enumerate(fetch_children(node_id)):
|
69
|
+
queue.append((child_id, path, depth + 1, child_idx))
|
70
|
+
|
71
|
+
|
72
|
+
# The End
|
@@ -1,113 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
"""
|
3
|
-
Tree update task compiler class.
|
4
|
-
|
5
|
-
Compiles tasks to low-level SQL to update the materialized path (_path), depth
|
6
|
-
(_depth), and node order (priority) when they are shifted or moved.
|
7
|
-
|
8
|
-
Version: 3.1.0
|
9
|
-
Author: Timur Kady
|
10
|
-
Email: timurkady@yandex.com
|
11
|
-
"""
|
12
|
-
|
13
|
-
from django.db import connection
|
14
|
-
|
15
|
-
from ...settings import SEGMENT_LENGTH
|
16
|
-
from .sqlcompat import SQLCompat
|
17
|
-
|
18
|
-
|
19
|
-
class TreePathCompiler:
|
20
|
-
"""
|
21
|
-
Tree Task compiler class.
|
22
|
-
|
23
|
-
Efficient, ORM-free computation of _path, _depth and priority
|
24
|
-
for tree structures based on Materialized Path.
|
25
|
-
"""
|
26
|
-
|
27
|
-
@classmethod
|
28
|
-
def update_path(cls, model, parent_id=None):
|
29
|
-
"""
|
30
|
-
Rebuild subtree starting from parent_id.
|
31
|
-
|
32
|
-
If parent_id=None, then the whole tree is rebuilt.
|
33
|
-
Uses only fields: parent_id and id. All others (priority, _path,
|
34
|
-
_depth) are recalculated.
|
35
|
-
"""
|
36
|
-
db_table = model._meta.db_table
|
37
|
-
# Will eliminate the risk if the user names the model order or user.
|
38
|
-
qname = connection.ops.quote_name(db_table)
|
39
|
-
|
40
|
-
sorting_field = model.sorting_field
|
41
|
-
sorting_fields = ["priority", "id"] if sorting_field == "priority" else [sorting_field] # noqa: D5017
|
42
|
-
sort_expr = ", ".join([
|
43
|
-
f"c.{field}" if "." not in field else field
|
44
|
-
for field in sorting_fields
|
45
|
-
])
|
46
|
-
|
47
|
-
cte_header = "(id, parent_id, new_priority, new_path, new_depth)"
|
48
|
-
|
49
|
-
row_number_expr = f"ROW_NUMBER() OVER (ORDER BY {sort_expr}) - 1"
|
50
|
-
hex_expr = SQLCompat.to_hex(row_number_expr)
|
51
|
-
lpad_expr = SQLCompat.lpad(hex_expr, SEGMENT_LENGTH, "'0'")
|
52
|
-
|
53
|
-
if parent_id is None:
|
54
|
-
new_path_expr = lpad_expr
|
55
|
-
base_sql = f"""
|
56
|
-
SELECT
|
57
|
-
c.id,
|
58
|
-
c.parent_id,
|
59
|
-
{row_number_expr} AS new_priority,
|
60
|
-
{new_path_expr} AS new_path,
|
61
|
-
0 AS new_depth
|
62
|
-
FROM {qname} AS c
|
63
|
-
WHERE c.parent_id IS NULL
|
64
|
-
"""
|
65
|
-
params = []
|
66
|
-
else:
|
67
|
-
path_expr = SQLCompat.concat("p._path", "'.'", lpad_expr)
|
68
|
-
base_sql = f"""
|
69
|
-
SELECT
|
70
|
-
c.id,
|
71
|
-
c.parent_id,
|
72
|
-
{row_number_expr} AS new_priority,
|
73
|
-
{path_expr} AS new_path,
|
74
|
-
p._depth + 1 AS new_depth
|
75
|
-
FROM {qname} c
|
76
|
-
JOIN {qname} p ON c.parent_id = p.id
|
77
|
-
WHERE p.id = %s
|
78
|
-
"""
|
79
|
-
params = [parent_id]
|
80
|
-
|
81
|
-
recursive_row_number_expr = f"ROW_NUMBER() OVER (PARTITION BY c.parent_id ORDER BY {sort_expr}) - 1"
|
82
|
-
recursive_hex_expr = SQLCompat.to_hex(recursive_row_number_expr)
|
83
|
-
recursive_lpad_expr = SQLCompat.lpad(
|
84
|
-
recursive_hex_expr, SEGMENT_LENGTH, "'0'")
|
85
|
-
recursive_path_expr = SQLCompat.concat(
|
86
|
-
"t.new_path", "'.'", recursive_lpad_expr)
|
87
|
-
|
88
|
-
recursive_sql = f"""
|
89
|
-
SELECT
|
90
|
-
c.id,
|
91
|
-
c.parent_id,
|
92
|
-
{recursive_row_number_expr} AS new_priority,
|
93
|
-
{recursive_path_expr} AS new_path,
|
94
|
-
t.new_depth + 1 AS new_depth
|
95
|
-
FROM {qname} c
|
96
|
-
JOIN tree_cte t ON c.parent_id = t.id
|
97
|
-
"""
|
98
|
-
|
99
|
-
final_sql = SQLCompat.update_from(
|
100
|
-
db_table=db_table,
|
101
|
-
cte_header=cte_header,
|
102
|
-
base_sql=base_sql,
|
103
|
-
recursive_sql=recursive_sql,
|
104
|
-
update_fields=["priority", "_path", "_depth"]
|
105
|
-
)
|
106
|
-
|
107
|
-
with connection.cursor() as cursor:
|
108
|
-
# Make params read-only
|
109
|
-
params = tuple(params)
|
110
|
-
cursor.execute(final_sql, params)
|
111
|
-
|
112
|
-
|
113
|
-
# The End
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/django_fast_treenode.egg-info/requires.txt
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/models/mixins/ancestors.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/models/mixins/properties.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/css/.gitkeep
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/js/.gitkeep
RENAMED
File without changes
|
File without changes
|
{django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/js/tree_widget.js
RENAMED
File without changes
|
File without changes
|
{django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/static/treenode/vendors/.gitkeep
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/templates/treenode/.gitkeep
RENAMED
File without changes
|
{django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/templates/treenode/admin/.gitkeep
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{django_fast_treenode-3.2.0 → django_fast_treenode-3.2.2}/treenode/templatetags/treenode_admin.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|