django-fast-treenode 3.0.3__tar.gz → 3.0.4__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.0.3/django_fast_treenode.egg-info → django_fast_treenode-3.0.4}/PKG-INFO +2 -2
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4/django_fast_treenode.egg-info}/PKG-INFO +2 -2
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/django_fast_treenode.egg-info/requires.txt +1 -1
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/roadmap.md +17 -14
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/pyproject.toml +2 -2
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/setup.py +2 -2
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/admin/mixin.py +1 -1
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/managers/queries.py +6 -18
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/managers/tasks.py +80 -42
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/utils/db/sqlcompat.py +33 -1
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/utils/db/sqlquery.py +24 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/version.py +1 -1
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/LICENSE +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/MANIFEST.in +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/README.md +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/django_fast_treenode.egg-info/SOURCES.txt +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/django_fast_treenode.egg-info/dependency_links.txt +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/django_fast_treenode.egg-info/top_level.txt +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/.gitignore +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/.nojekyll +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/about.md +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/admin.md +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/api.md +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/apifirst.md +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/cache.md +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/customization.md +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/dnd.md +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/import_export.md +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/index.md +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/insert-after.jpg +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/insert-as-child.jpg +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/installation.md +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/migration.md +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/models.md +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/requirements.txt +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/docs/using.md +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/setup.cfg +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/tests/test_suite.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/__init__.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/admin/__init__.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/admin/admin.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/admin/changelist.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/admin/exporter.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/admin/importer.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/apps.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/cache.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/forms.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/managers/__init__.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/managers/managers.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/__init__.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/decorators.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/factory.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/__init__.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/ancestors.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/children.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/descendants.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/family.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/logical.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/node.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/properties.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/roots.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/siblings.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/tree.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/update.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/models.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/settings.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/signals.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/.gitkeep +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/css/.gitkeep +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/css/tree_widget.css +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/css/treenode_admin.css +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/css/treenode_tabs.css +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/js/.gitkeep +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/js/lz-string.min.js +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/js/tree_widget.js +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/js/treenode_admin.js +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/AUTHORS.txt +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/LICENSE.txt +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/external/jquery/jquery.js +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/images/ui-icons_444444_256x240.png +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/images/ui-icons_555555_256x240.png +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/images/ui-icons_777620_256x240.png +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/images/ui-icons_777777_256x240.png +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/images/ui-icons_cc0000_256x240.png +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/images/ui-icons_ffffff_256x240.png +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/index.html +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/jquery-ui.css +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/jquery-ui.js +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/jquery-ui.min.css +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/jquery-ui.min.js +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/jquery-ui.structure.css +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/jquery-ui.structure.min.css +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/jquery-ui.theme.css +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/jquery-ui.theme.min.css +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/vendors/jquery-ui/package.json +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/templates/.gitkeep +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/templates/admin/.gitkeep +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/templates/admin/treenode_ajax_rows.html +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/templates/admin/treenode_changelist.html +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/templates/admin/treenode_import_export.html +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/templates/admin/treenode_rows.html +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/templates/widgets/tree_widget.html +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/tests.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/urls.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/utils/__init__.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/utils/db/__init__.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/utils/db/compiler.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/utils/db/db_vendor.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/utils/db/service.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/views/__init__.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/views/autoapi.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/views/autocomplete.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/views/children.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/views/common.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/views/crud.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/views/search.py +0 -0
- {django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/widgets.py +0 -0
{django_fast_treenode-3.0.3/django_fast_treenode.egg-info → django_fast_treenode-3.0.4}/PKG-INFO
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: django-fast-treenode
|
3
|
-
Version: 3.0.
|
3
|
+
Version: 3.0.4
|
4
4
|
Summary: Treenode Framework for supporting tree (hierarchical) data structure in Django projects
|
5
5
|
Home-page: https://django-fast-treenode.readthedocs.io/
|
6
6
|
Author: Timur Kady
|
@@ -53,7 +53,7 @@ Classifier: Operating System :: OS Independent
|
|
53
53
|
Requires-Python: >=3.9
|
54
54
|
Description-Content-Type: text/markdown
|
55
55
|
License-File: LICENSE
|
56
|
-
Requires-Dist: Django>=
|
56
|
+
Requires-Dist: Django>=5.0
|
57
57
|
Requires-Dist: msgpack>=1.0.0
|
58
58
|
Requires-Dist: openpyxl>=3.0.0
|
59
59
|
Requires-Dist: pyyaml>=5.1
|
{django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4/django_fast_treenode.egg-info}/PKG-INFO
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: django-fast-treenode
|
3
|
-
Version: 3.0.
|
3
|
+
Version: 3.0.4
|
4
4
|
Summary: Treenode Framework for supporting tree (hierarchical) data structure in Django projects
|
5
5
|
Home-page: https://django-fast-treenode.readthedocs.io/
|
6
6
|
Author: Timur Kady
|
@@ -53,7 +53,7 @@ Classifier: Operating System :: OS Independent
|
|
53
53
|
Requires-Python: >=3.9
|
54
54
|
Description-Content-Type: text/markdown
|
55
55
|
License-File: LICENSE
|
56
|
-
Requires-Dist: Django>=
|
56
|
+
Requires-Dist: Django>=5.0
|
57
57
|
Requires-Dist: msgpack>=1.0.0
|
58
58
|
Requires-Dist: openpyxl>=3.0.0
|
59
59
|
Requires-Dist: pyyaml>=5.1
|
@@ -1,6 +1,6 @@
|
|
1
1
|
## Roadmap
|
2
2
|
|
3
|
-
The **`django-fast-treenode`** package will continue to evolve from its original concept—combining the benefits of
|
3
|
+
The **`django-fast-treenode`** package will continue to evolve from its original concept—combining the benefits of hybrid models into a high-performance solution for managing and visualizing hierarchical data in Django projects.
|
4
4
|
|
5
5
|
The focus is on **speed, usability, and flexibility**.
|
6
6
|
|
@@ -8,32 +8,36 @@ The focus is on **speed, usability, and flexibility**.
|
|
8
8
|
|
9
9
|
The 3.x release series will focus on strengthening TreeNode Framework in terms of security, usability, and performance scalability, while maintaining backward compatibility and architectural cleanliness.
|
10
10
|
|
11
|
+
* **Version 3.1 — Background Task Worker (Production Mode)**
|
11
12
|
|
12
|
-
|
13
|
+
- Introduce a centralized queue manager with a multiprocessing or Redis-based backend.
|
14
|
+
- Add a built-in worker process for safe and efficient task execution in production environments.
|
15
|
+
- Provide a fallback auto-run mode for DEBUG environments (using `atexit` or thread-based handler).
|
16
|
+
- Ensure task queue consistency across multiple WSGI workers or scripts.
|
17
|
+
|
18
|
+
* **Version 3.2 — JWT Authentication for API**
|
13
19
|
|
14
20
|
- Introduce optional JWT-based token authentication for the auto-generated API.
|
15
21
|
- Allow easy activation through a single setting (`TREENODE_API_USE_JWT = True`).
|
16
22
|
- Preserve backward compatibility: API remains open unless explicitly protected.
|
17
23
|
- Foundation for future security features (e.g., user-specific trees, audit trails).
|
18
24
|
|
19
|
-
* **Version 3.
|
25
|
+
* **Version 3.3 — Admin Usability Improvements**
|
20
26
|
|
21
27
|
Focus: enhance operational safety and optimize workflows for large-scale trees.
|
22
28
|
|
23
29
|
- **Safe Import Preview**: Implement a staging layer for imports, allowing users to review and confirm imported data before committing changes.
|
24
|
-
|
25
30
|
- **Incremental Export**: Support selective export of nodes modified after a specified date or revision marker.
|
26
31
|
|
27
|
-
|
28
|
-
* **Version 3.3 — Soft Deletion Support**
|
32
|
+
* **Version 3.4 — Soft Deletion Support**
|
29
33
|
|
30
34
|
Focus: improve real-world resilience without sacrificing performance.
|
31
35
|
|
32
36
|
- Add optional support for "soft delete" behavior (`is_deleted` field).
|
33
37
|
- Modify core queries and cache invalidation logic to respect soft-deleted nodes.
|
34
38
|
- Add a new task type to the internal task queue system for bulk logical deletions.
|
35
|
-
|
36
|
-
* **Version 3.
|
39
|
+
|
40
|
+
* **Version 3.5 — Cache System Enhancements**
|
37
41
|
|
38
42
|
Focus: lay the foundation for scaling Treenode Framework to extreme node counts (>100,000 nodes).
|
39
43
|
|
@@ -45,13 +49,12 @@ Each step in the 3.x roadmap is intended to strengthen the framework's key princ
|
|
45
49
|
|
46
50
|
#### Long-Term Vision
|
47
51
|
|
52
|
+
* **Version 4.0 – Improved Architecture**
|
48
53
|
|
49
|
-
|
50
|
-
|
51
|
-
The main debut idea of version 4.0 is to further develop the hybrid approach. This version will implement a new new architectural solution that is designed to increase the speed of selecting descendants, and therefore moving subtrees, and remove the existing restrictions on the maximum nesting depth of the tree (currently the recommended value when using up to 1000 levels).
|
54
|
+
The main debut idea of version 4.0 is to further develop the hybrid approach. This version will implement a new architectural solution that is designed to increase the speed of selecting descendants, and therefore moving subtrees, and remove the existing restrictions on the maximum nesting depth of the tree (currently the recommended value when using up to 1000 levels).
|
52
55
|
|
53
|
-
- Speed
|
54
|
-
-
|
56
|
+
- Speed up the operation of extracting descendants.
|
57
|
+
- Speed up operations for moving large subtrees.
|
55
58
|
- Performance optimization when working with trees that have extreme depth (more than 2000 levels).
|
56
59
|
|
57
60
|
* **Version 5.0 – Beyond Django ORM**
|
@@ -67,4 +70,4 @@ So, each milestone is designed to improve performance, scalability, and flexibil
|
|
67
70
|
|
68
71
|
Stay tuned for updates!
|
69
72
|
|
70
|
-
Your wishes, objections, and comments are welcome.
|
73
|
+
Your wishes, objections, and comments are welcome.
|
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "django-fast-treenode"
|
7
|
-
version = "3.0.
|
7
|
+
version = "3.0.4"
|
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" }]
|
11
11
|
license = { file = "LICENSE" }
|
12
12
|
requires-python = ">=3.9"
|
13
13
|
dependencies = [
|
14
|
-
'Django>=
|
14
|
+
'Django>=5.0',
|
15
15
|
'msgpack>=1.0.0',
|
16
16
|
'openpyxl>=3.0.0',
|
17
17
|
'pyyaml>=5.1',
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
2
2
|
|
3
3
|
setup(
|
4
4
|
name='django-fast-treenode',
|
5
|
-
version='3.0.
|
5
|
+
version='3.0.4',
|
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',
|
@@ -14,7 +14,7 @@ setup(
|
|
14
14
|
license='MIT',
|
15
15
|
license_files=['LICENSE'],
|
16
16
|
install_requires=[
|
17
|
-
'Django>=
|
17
|
+
'Django>=5.0',
|
18
18
|
'msgpack>=1.0.0',
|
19
19
|
'openpyxl>=3.0.0',
|
20
20
|
'pyyaml>=5.1',
|
@@ -84,7 +84,7 @@ class AdminMixin(admin.ModelAdmin):
|
|
84
84
|
value = field(obj)
|
85
85
|
field_name = getattr(field, "__name__", "field")
|
86
86
|
else:
|
87
|
-
attr, value = lookup_field(field, obj, self)
|
87
|
+
r, attr, value = lookup_field(field, obj, self)
|
88
88
|
field_name = field
|
89
89
|
|
90
90
|
row_data.append(value)
|
@@ -5,13 +5,14 @@ Low-level SQL Query Manager.
|
|
5
5
|
Encapsulates all logic to retrieve related primary keys based on relationships
|
6
6
|
(e.g., ancestors, children, descendants, siblings, family, root) using raw SQL.
|
7
7
|
|
8
|
-
Version: 3.0.
|
8
|
+
Version: 3.0.4
|
9
9
|
Author: Timur Kady
|
10
10
|
Email: timurkady@yandex.com
|
11
11
|
"""
|
12
12
|
|
13
13
|
|
14
14
|
from django.db import connection
|
15
|
+
from ..utils.db.sqlcompat import SQLCompat
|
15
16
|
|
16
17
|
|
17
18
|
class TreeQuery:
|
@@ -32,19 +33,6 @@ class TreeQuery:
|
|
32
33
|
cursor.execute(sql, params)
|
33
34
|
return cursor.fetchall()
|
34
35
|
|
35
|
-
def wrap_union_all(self, queries):
|
36
|
-
"""
|
37
|
-
Combine multiple SQL queries using UNION ALL.
|
38
|
-
|
39
|
-
Each query is a tuple: (sql, params).
|
40
|
-
Returns a tuple: (combined_sql, combined_params).
|
41
|
-
"""
|
42
|
-
union_query = " UNION ALL ".join(f"({q[0]})" for q in queries)
|
43
|
-
combined_params = []
|
44
|
-
for q in queries:
|
45
|
-
combined_params.extend(q[1])
|
46
|
-
return union_query, combined_params
|
47
|
-
|
48
36
|
def order_by(self, sql, order_by_clause):
|
49
37
|
"""Wrap the SQL in an outer query to enforce ordering."""
|
50
38
|
return f"SELECT * FROM ({sql}) AS combined ORDER BY {order_by_clause}"
|
@@ -76,7 +64,7 @@ class TreeQuery:
|
|
76
64
|
if include_self:
|
77
65
|
sql2 = f"SELECT id, priority FROM {self.db_table} WHERE id = %s"
|
78
66
|
params2 = [self.node.pk]
|
79
|
-
combined_sql, combined_params =
|
67
|
+
combined_sql, combined_params = SQLCompat.wrap_union_all(
|
80
68
|
[(sql1, params1), (sql2, params2)])
|
81
69
|
sql = self.order_by(combined_sql, "priority")
|
82
70
|
return sql, combined_params
|
@@ -115,7 +103,7 @@ class TreeQuery:
|
|
115
103
|
FROM {self.db_table}
|
116
104
|
WHERE id = %s
|
117
105
|
"""
|
118
|
-
union_sql, union_params =
|
106
|
+
union_sql, union_params = SQLCompat.wrap_union_all([
|
119
107
|
(base_sql, params),
|
120
108
|
(sql_self, [self.node.pk])
|
121
109
|
])
|
@@ -148,7 +136,7 @@ class TreeQuery:
|
|
148
136
|
|
149
137
|
if include_self:
|
150
138
|
sql_self = f"SELECT id, _depth, priority FROM {self.db_table} WHERE id = %s" # noqa: D501
|
151
|
-
union_sql, union_params =
|
139
|
+
union_sql, union_params = SQLCompat.wrap_union_all(
|
152
140
|
[(base_sql, params), (sql_self, [self.node.pk])])
|
153
141
|
else:
|
154
142
|
union_sql, union_params = base_sql, params
|
@@ -198,7 +186,7 @@ class TreeQuery:
|
|
198
186
|
if include_self:
|
199
187
|
sql_self = f"SELECT id, _depth, priority FROM {self.db_table} WHERE id = %s" # noqa: D501
|
200
188
|
queries.append((sql_self, [self.node.pk]))
|
201
|
-
combined_sql, combined_params =
|
189
|
+
combined_sql, combined_params = SQLCompat.wrap_union_all(queries)
|
202
190
|
combined_sql = self.order_by(combined_sql, "_depth, priority")
|
203
191
|
return combined_sql, combined_params
|
204
192
|
|
@@ -2,24 +2,16 @@
|
|
2
2
|
"""
|
3
3
|
TreeNode TaskQuery manager
|
4
4
|
|
5
|
-
Version: 3.0.
|
5
|
+
Version: 3.0.4
|
6
6
|
Author: Timur Kady
|
7
7
|
Email: timurkady@yandex.com
|
8
8
|
"""
|
9
9
|
|
10
|
-
|
10
|
+
import atexit
|
11
|
+
from django.db import connection, transaction
|
11
12
|
|
12
13
|
from ..utils.db import TreePathCompiler
|
13
14
|
|
14
|
-
'''
|
15
|
-
try:
|
16
|
-
profile
|
17
|
-
except NameError:
|
18
|
-
def profile(func):
|
19
|
-
"""Profile."""
|
20
|
-
return func
|
21
|
-
'''
|
22
|
-
|
23
15
|
|
24
16
|
class TreeTaskQueue:
|
25
17
|
"""TreeTaskQueue Class."""
|
@@ -28,31 +20,93 @@ class TreeTaskQueue:
|
|
28
20
|
"""Init the task query."""
|
29
21
|
self.model = model
|
30
22
|
self.queue = []
|
23
|
+
self._running = False
|
24
|
+
|
25
|
+
# Register the execution queue when the interpreter exits
|
26
|
+
atexit.register(self._atexit_run)
|
27
|
+
|
28
|
+
def _atexit_run(self):
|
29
|
+
"""Run queue on interpreter exit if pending tasks exist."""
|
30
|
+
if self.queue and not self._running:
|
31
|
+
try:
|
32
|
+
self.run()
|
33
|
+
except Exception as e:
|
34
|
+
# Don't crash on completion, just log
|
35
|
+
print(f"[TreeTaskQueue] Error during atexit: {e}")
|
31
36
|
|
32
37
|
def add(self, mode, parent_id):
|
33
|
-
"""Add task to the
|
38
|
+
"""Add task to the queue.
|
39
|
+
|
40
|
+
Parameters:
|
41
|
+
mode (str): Task type (currently only "update").
|
42
|
+
parent_id (int|None): ID of parent node to update from (None = full tree).
|
43
|
+
"""
|
34
44
|
self.queue.append({"mode": mode, "parent_id": parent_id})
|
35
45
|
|
36
46
|
def run(self):
|
37
|
-
"""Run task queue.
|
47
|
+
"""Run task queue.
|
48
|
+
|
49
|
+
This method collects all queued tasks, optimizes them, and performs
|
50
|
+
a recursive rebuild of tree paths and depths using SQL. Locks the
|
51
|
+
required rows before running.
|
52
|
+
|
53
|
+
Uses Django's `transaction.atomic()` to ensure that any recursive CTE
|
54
|
+
execution or SAVEPOINT creation works properly under PostgreSQL.
|
55
|
+
"""
|
38
56
|
if len(self.queue) == 0:
|
39
57
|
return
|
58
|
+
|
59
|
+
self._running = True
|
40
60
|
try:
|
41
61
|
optimized = self._optimize()
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
62
|
+
if not optimized:
|
63
|
+
return
|
64
|
+
|
65
|
+
parent_ids = [t["parent_id"] for t in optimized if t["parent_id"] is not None]
|
66
|
+
|
67
|
+
with transaction.atomic():
|
68
|
+
if any(t["parent_id"] is None for t in optimized):
|
69
|
+
try:
|
70
|
+
with connection.cursor() as cursor:
|
71
|
+
cursor.execute(
|
72
|
+
f"SELECT id FROM {self.model._meta.db_table} WHERE parent_id IS NULL FOR UPDATE NOWAIT"
|
73
|
+
)
|
74
|
+
except Exception as e:
|
75
|
+
print(f"[TreeTaskQueue] Skipped (root locked): {e}")
|
76
|
+
return
|
77
|
+
else:
|
78
|
+
try:
|
79
|
+
with connection.cursor() as cursor:
|
80
|
+
for parent_id in parent_ids:
|
81
|
+
cursor.execute(
|
82
|
+
f"SELECT id FROM {self.model._meta.db_table} WHERE id = %s FOR UPDATE NOWAIT",
|
83
|
+
[parent_id],
|
84
|
+
)
|
85
|
+
except Exception as e:
|
86
|
+
print(f"[TreeTaskQueue] Skipped (parent locked): {e}")
|
87
|
+
return
|
88
|
+
|
89
|
+
for task in optimized:
|
90
|
+
if task["mode"] == "update":
|
91
|
+
TreePathCompiler.update_path(
|
92
|
+
model=self.model,
|
93
|
+
parent_id=task["parent_id"]
|
94
|
+
)
|
95
|
+
|
96
|
+
except Exception as e:
|
97
|
+
print(f"[TreeTaskQueue] Error in run: {e}")
|
98
|
+
connection.rollback()
|
49
99
|
finally:
|
50
100
|
self.queue.clear()
|
51
101
|
self._running = False
|
52
102
|
|
53
|
-
# @profile
|
54
103
|
def _optimize(self):
|
55
|
-
"""Return optimized task queue (ID-only logic).
|
104
|
+
"""Return optimized task queue (ID-only logic).
|
105
|
+
|
106
|
+
Attempts to merge redundant or overlapping subtree updates into
|
107
|
+
the minimal set of unique parent IDs that need to be rebuilt.
|
108
|
+
If it finds a common root, it returns a single task for full rebuild.
|
109
|
+
"""
|
56
110
|
result_set = set()
|
57
111
|
id_set = set()
|
58
112
|
|
@@ -60,8 +114,6 @@ class TreeTaskQueue:
|
|
60
114
|
if task["mode"] == "update":
|
61
115
|
parent_id = task["parent_id"]
|
62
116
|
if parent_id is None:
|
63
|
-
# If we are already updating the entire tree, then
|
64
|
-
# the remaining tasks are meaningless # noqa: D501
|
65
117
|
return [{"mode": "update", "parent_id": None}]
|
66
118
|
else:
|
67
119
|
id_set.add(parent_id)
|
@@ -74,8 +126,6 @@ class TreeTaskQueue:
|
|
74
126
|
for other in id_list[:]:
|
75
127
|
ancestor = self._get_common_ancestor(current, other)
|
76
128
|
if ancestor is not None:
|
77
|
-
# If the common ancestor is the root, then we update
|
78
|
-
# the entire tree
|
79
129
|
if ancestor in self._get_root_ids():
|
80
130
|
return [{"mode": "update", "parent_id": None}]
|
81
131
|
if ancestor not in id_set:
|
@@ -87,36 +137,25 @@ class TreeTaskQueue:
|
|
87
137
|
if not merged:
|
88
138
|
result_set.add(current)
|
89
139
|
|
90
|
-
return [{"mode": "update", "parent_id": pk} for pk in sorted(result_set)]
|
140
|
+
return [{"mode": "update", "parent_id": pk} for pk in sorted(result_set)]
|
91
141
|
|
92
142
|
def _get_root_ids(self):
|
93
143
|
"""Return root node IDs."""
|
94
144
|
with connection.cursor() as cursor:
|
95
145
|
cursor.execute(
|
96
|
-
f"SELECT id FROM {self.model._meta.db_table} WHERE parent_id IS NULL")
|
146
|
+
f"SELECT id FROM {self.model._meta.db_table} WHERE parent_id IS NULL")
|
97
147
|
return [row[0] for row in cursor.fetchall()]
|
98
148
|
|
99
149
|
def _get_parent_id(self, node_id):
|
100
150
|
"""Return parent ID for a given node."""
|
101
151
|
with connection.cursor() as cursor:
|
102
152
|
cursor.execute(
|
103
|
-
f"SELECT parent_id FROM {self.model._meta.db_table} WHERE id = %s", [node_id])
|
153
|
+
f"SELECT parent_id FROM {self.model._meta.db_table} WHERE id = %s", [node_id])
|
104
154
|
row = cursor.fetchone()
|
105
155
|
return row[0] if row else None
|
106
156
|
|
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
157
|
def _get_ancestor_path(self, node_id):
|
119
|
-
"""Return list of ancestor IDs including the node itself, using recursive SQL."""
|
158
|
+
"""Return list of ancestor IDs including the node itself, using recursive SQL."""
|
120
159
|
table = self.model._meta.db_table
|
121
160
|
|
122
161
|
sql = f"""
|
@@ -140,7 +179,6 @@ class TreeTaskQueue:
|
|
140
179
|
|
141
180
|
return [row[0] for row in rows]
|
142
181
|
|
143
|
-
# @profile
|
144
182
|
def _get_common_ancestor(self, id1, id2):
|
145
183
|
"""Return common ancestor ID between two nodes."""
|
146
184
|
path1 = self._get_ancestor_path(id1)
|
@@ -125,12 +125,44 @@ class SQLCompat:
|
|
125
125
|
{set_clause};
|
126
126
|
"""
|
127
127
|
|
128
|
+
elif connection.vendor == "sqlite":
|
129
|
+
# SQLite workaround via temporary table
|
130
|
+
temp_table = "temp_tree_update"
|
131
|
+
cols = ["id"] + [cte_alias.get(f, f) for f in update_fields]
|
132
|
+
col_defs = ", ".join(f"{c} TEXT" for c in cols)
|
133
|
+
insert_cols = ", ".join(cols)
|
134
|
+
select_cols = ", ".join(cols)
|
135
|
+
|
136
|
+
set_clause = ", ".join(
|
137
|
+
f"{qf(f)} = (SELECT t.{cte_alias.get(f, f)} FROM {temp_table} t WHERE t.id = {qt}.id)" # noqa
|
138
|
+
for f in update_fields
|
139
|
+
)
|
140
|
+
|
141
|
+
return f"""
|
142
|
+
DROP TABLE IF EXISTS {temp_table};
|
143
|
+
CREATE TEMP TABLE {temp_table} ({col_defs});
|
144
|
+
|
145
|
+
WITH RECURSIVE tree_cte {cte_header} AS (
|
146
|
+
{base_sql}
|
147
|
+
UNION ALL
|
148
|
+
{recursive_sql}
|
149
|
+
)
|
150
|
+
INSERT INTO {temp_table} ({insert_cols})
|
151
|
+
SELECT {select_cols} FROM tree_cte;
|
152
|
+
|
153
|
+
UPDATE {qt}
|
154
|
+
SET {set_clause}
|
155
|
+
WHERE id IN (SELECT id FROM {temp_table});
|
156
|
+
"""
|
157
|
+
|
128
158
|
else:
|
159
|
+
# Fallback: subqueries
|
160
|
+
# (still buggy in SQLite, hence above workaround)
|
129
161
|
set_clause = ", ".join(
|
130
162
|
f"{qf(f)} = (SELECT t.{f} FROM tree_cte t WHERE t.id = {qt}.id)"
|
131
163
|
for f in update_fields
|
132
164
|
)
|
133
|
-
where_clause =
|
165
|
+
where_clause = "id IN (SELECT id FROM tree_cte)"
|
134
166
|
return f"""
|
135
167
|
WITH RECURSIVE tree_cte {cte_header} AS (
|
136
168
|
{base_sql}
|
@@ -66,5 +66,29 @@ class SQLQueue:
|
|
66
66
|
raise
|
67
67
|
self._items.clear()
|
68
68
|
|
69
|
+
@staticmethod
|
70
|
+
def wrap_union_all(queries):
|
71
|
+
"""
|
72
|
+
Combine multiple SQL queries using UNION ALL with vendor-specific handling.
|
73
|
+
Each query is a tuple: (sql, params).
|
74
|
+
Returns a tuple: (combined_sql, combined_params).
|
75
|
+
"""
|
76
|
+
if is_sqlite():
|
77
|
+
# SQLite требует одинаковое число и порядок столбцов. Добавим к каждому SELECT псевдонимы.
|
78
|
+
def alias_select(sql, alias_prefix, idx):
|
79
|
+
return f"SELECT * FROM ({sql}) AS {alias_prefix}_{idx}"
|
80
|
+
|
81
|
+
wrapped_queries = [
|
82
|
+
alias_select(q[0], "q", i) for i, q in enumerate(queries)
|
83
|
+
]
|
84
|
+
combined_sql = " UNION ALL ".join(wrapped_queries)
|
85
|
+
else:
|
86
|
+
combined_sql = " UNION ALL ".join(f"({q[0]})" for q in queries)
|
87
|
+
|
88
|
+
combined_params = []
|
89
|
+
for q in queries:
|
90
|
+
combined_params.extend(q[1])
|
91
|
+
|
92
|
+
return combined_sql, combined_params
|
69
93
|
|
70
94
|
# The End
|
File without changes
|
File without changes
|
File without changes
|
{django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/django_fast_treenode.egg-info/SOURCES.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
|
File without changes
|
{django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/__init__.py
RENAMED
File without changes
|
{django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/ancestors.py
RENAMED
File without changes
|
{django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/children.py
RENAMED
File without changes
|
{django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/descendants.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/properties.py
RENAMED
File without changes
|
File without changes
|
{django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/models/mixins/siblings.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
|
{django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/css/tree_widget.css
RENAMED
File without changes
|
{django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/css/treenode_admin.css
RENAMED
File without changes
|
{django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/css/treenode_tabs.css
RENAMED
File without changes
|
File without changes
|
{django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/js/lz-string.min.js
RENAMED
File without changes
|
File without changes
|
{django_fast_treenode-3.0.3 → django_fast_treenode-3.0.4}/treenode/static/js/treenode_admin.js
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|