django-fast-treenode 3.0.0__tar.gz → 3.0.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.0.0 → django_fast_treenode-3.0.2}/LICENSE +1 -0
- {django_fast_treenode-3.0.0/django_fast_treenode.egg-info → django_fast_treenode-3.0.2}/PKG-INFO +2 -1
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2/django_fast_treenode.egg-info}/PKG-INFO +2 -1
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/docs/index.md +9 -9
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/pyproject.toml +2 -1
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/setup.py +2 -1
- django_fast_treenode-3.0.2/tests/test_suite.py +99 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/apps.py +1 -1
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/managers/queries.py +43 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/models/mixins/descendants.py +17 -11
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/utils/db/compiler.py +24 -25
- django_fast_treenode-3.0.2/treenode/utils/db/sqlcompat.py +145 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/version.py +2 -3
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/views/crud.py +31 -10
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/widgets.py +2 -0
- django_fast_treenode-3.0.0/tests/test_suite.py +0 -91
- django_fast_treenode-3.0.0/treenode/utils/db/sqlcompat.py +0 -60
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/MANIFEST.in +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/README.md +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/django_fast_treenode.egg-info/SOURCES.txt +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/django_fast_treenode.egg-info/dependency_links.txt +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/django_fast_treenode.egg-info/requires.txt +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/django_fast_treenode.egg-info/top_level.txt +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/docs/.gitignore +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/docs/.nojekyll +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/docs/about.md +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/docs/admin.md +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/docs/api.md +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/docs/apifirst.md +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/docs/cache.md +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/docs/customization.md +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/docs/dnd.md +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/docs/import_export.md +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/docs/insert-after.jpg +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/docs/insert-as-child.jpg +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/docs/installation.md +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/docs/migration.md +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/docs/models.md +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/docs/requirements.txt +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/docs/roadmap.md +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/docs/using.md +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/setup.cfg +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/__init__.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/admin/__init__.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/admin/admin.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/admin/changelist.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/admin/exporter.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/admin/importer.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/admin/mixin.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/cache.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/forms.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/managers/__init__.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/managers/managers.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/managers/tasks.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/models/__init__.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/models/decorators.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/models/factory.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/models/mixins/__init__.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/models/mixins/ancestors.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/models/mixins/children.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/models/mixins/family.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/models/mixins/logical.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/models/mixins/node.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/models/mixins/properties.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/models/mixins/roots.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/models/mixins/siblings.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/models/mixins/tree.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/models/mixins/update.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/models/models.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/settings.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/signals.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/.gitkeep +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/css/.gitkeep +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/css/tree_widget.css +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/css/treenode_admin.css +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/css/treenode_tabs.css +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/js/.gitkeep +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/js/lz-string.min.js +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/js/tree_widget.js +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/js/treenode_admin.js +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/vendors/jquery-ui/AUTHORS.txt +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/vendors/jquery-ui/LICENSE.txt +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/vendors/jquery-ui/external/jquery/jquery.js +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/vendors/jquery-ui/images/ui-icons_444444_256x240.png +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/vendors/jquery-ui/images/ui-icons_555555_256x240.png +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/vendors/jquery-ui/images/ui-icons_777620_256x240.png +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/vendors/jquery-ui/images/ui-icons_777777_256x240.png +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/vendors/jquery-ui/images/ui-icons_cc0000_256x240.png +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/vendors/jquery-ui/images/ui-icons_ffffff_256x240.png +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/vendors/jquery-ui/index.html +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/vendors/jquery-ui/jquery-ui.css +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/vendors/jquery-ui/jquery-ui.js +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/vendors/jquery-ui/jquery-ui.min.css +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/vendors/jquery-ui/jquery-ui.min.js +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/vendors/jquery-ui/jquery-ui.structure.css +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/vendors/jquery-ui/jquery-ui.structure.min.css +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/vendors/jquery-ui/jquery-ui.theme.css +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/vendors/jquery-ui/jquery-ui.theme.min.css +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/vendors/jquery-ui/package.json +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/templates/.gitkeep +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/templates/admin/.gitkeep +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/templates/admin/treenode_ajax_rows.html +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/templates/admin/treenode_changelist.html +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/templates/admin/treenode_import_export.html +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/templates/admin/treenode_rows.html +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/templates/widgets/tree_widget.html +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/tests.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/urls.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/utils/__init__.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/utils/db/__init__.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/utils/db/db_vendor.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/utils/db/service.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/utils/db/sqlquery.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/views/__init__.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/views/autoapi.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/views/autocomplete.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/views/children.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/views/common.py +0 -0
- {django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/views/search.py +0 -0
{django_fast_treenode-3.0.0/django_fast_treenode.egg-info → django_fast_treenode-3.0.2}/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.2
|
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
|
@@ -27,6 +27,7 @@ License: MIT License
|
|
27
27
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
28
28
|
SOFTWARE.
|
29
29
|
|
30
|
+
|
30
31
|
Project-URL: Homepage, https://github.com/TimurKady/django-fast-treenode
|
31
32
|
Project-URL: Documentation, https://django-fast-treenode.readthedocs.io/
|
32
33
|
Project-URL: Source, https://github.com/TimurKady/django-fast-treenode
|
{django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2/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.2
|
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
|
@@ -27,6 +27,7 @@ License: MIT License
|
|
27
27
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
28
28
|
SOFTWARE.
|
29
29
|
|
30
|
+
|
30
31
|
Project-URL: Homepage, https://github.com/TimurKady/django-fast-treenode
|
31
32
|
Project-URL: Documentation, https://django-fast-treenode.readthedocs.io/
|
32
33
|
Project-URL: Source, https://github.com/TimurKady/django-fast-treenode
|
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# Treenode Framework
|
2
2
|
**A hybrid open-source framework for working with trees in Django**
|
3
3
|
|
4
4
|
Welcome to the **django-fast-treenode** documentation!
|
@@ -147,14 +147,14 @@ The project is designed for long-term compatibility with future Django versions
|
|
147
147
|
|
148
148
|
All SQL queries are adapted through a universal compatibility layer, ensuring support for major databases without the need to rewrite SQL code.
|
149
149
|
|
150
|
-
| Database
|
151
|
-
|
152
|
-
| **PostgreSQL** | ≥
|
153
|
-
| **MySQL** | ≥ 8.0 | Partially tested |
|
154
|
-
| **MariaDB** | ≥ 10.
|
155
|
-
| **SQLite** | ≥ 3.
|
156
|
-
| **Oracle** | ≥ 12c
|
157
|
-
| **MS SQL Server** | ≥
|
150
|
+
| Database | Minimum Version | Status | Notes |
|
151
|
+
|:------------------|:------------------|:----------------|:-------------------|
|
152
|
+
| **PostgreSQL** | ≥ 10.0 | Full tested | Recommended ≥ 12 |
|
153
|
+
| **MySQL** | ≥ 8.0 | Partially tested | |
|
154
|
+
| **MariaDB** | ≥ 10.5 | Not tested | |
|
155
|
+
| **SQLite** | ≥ 3.35.0 | Partially tested | |
|
156
|
+
| **Oracle** | ≥ 12c R2 | Full tested | Recommended ≥ 19c |
|
157
|
+
| **MS SQL Server** | ≥ 2016 | Partially tested | |
|
158
158
|
|
159
159
|
|
160
160
|
The project is **ready for production use** across all modern versions of Django and major relational databases without manual SQL corrections.
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "django-fast-treenode"
|
7
|
-
version = "3.0.
|
7
|
+
version = "3.0.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" }]
|
@@ -43,3 +43,4 @@ Documentation = "https://django-fast-treenode.readthedocs.io/"
|
|
43
43
|
Source = "https://github.com/TimurKady/django-fast-treenode"
|
44
44
|
Issues = "https://github.com/TimurKady/django-fast-treenode/issues"
|
45
45
|
|
46
|
+
|
@@ -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.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',
|
@@ -41,3 +41,4 @@ setup(
|
|
41
41
|
],
|
42
42
|
python_requires='>=3.9',
|
43
43
|
)
|
44
|
+
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# tests/test_treenode.py
|
2
|
+
from django.test import TestCase
|
3
|
+
from django.db import transaction
|
4
|
+
from .models import TestModel
|
5
|
+
|
6
|
+
PATH_DIGITS = 3
|
7
|
+
|
8
|
+
|
9
|
+
def hex_path(parts):
|
10
|
+
"""Преобразуем массив индексов в нулепад-hex-строку."""
|
11
|
+
return ".".join(f"{n:0{PATH_DIGITS}X}" for n in parts)
|
12
|
+
|
13
|
+
|
14
|
+
class TreeNodeModelTests(TestCase):
|
15
|
+
"""Проверяем основные операции TreeNodeModel."""
|
16
|
+
|
17
|
+
@classmethod
|
18
|
+
def setUpTestData(cls):
|
19
|
+
"""
|
20
|
+
Создаём тестовое дерево один раз на весь класс.
|
21
|
+
Django сделает его снимок, и в каждом тесте база
|
22
|
+
будет возвращаться в это состояние автоматически.
|
23
|
+
"""
|
24
|
+
cls.root = TestModel.objects.create(name="root", priority=0)
|
25
|
+
cls.a = TestModel.objects.create(name="A", parent=cls.root, priority=1)
|
26
|
+
cls.b = TestModel.objects.create(name="B", parent=cls.root, priority=2)
|
27
|
+
cls.c = TestModel.objects.create(name="C", parent=cls.a, priority=1)
|
28
|
+
cls.d = TestModel.objects.create(name="D", parent=cls.a, priority=2)
|
29
|
+
|
30
|
+
_ = cls.a.get_order()
|
31
|
+
_ = cls.c.get_order()
|
32
|
+
|
33
|
+
# --- 1. Creating nodes -------------------------------------------------
|
34
|
+
|
35
|
+
def test_count_after_creation(self):
|
36
|
+
self.assertEqual(TestModel.objects.count(), 5)
|
37
|
+
|
38
|
+
# --- 2. _path and _depth --------------------------------------------------
|
39
|
+
|
40
|
+
def test_path_and_depth_saved(self):
|
41
|
+
with self.subTest("Depth values"):
|
42
|
+
self.assertEqual(self.a.get_depth(), 1)
|
43
|
+
self.assertEqual(self.c.get_depth(), 2)
|
44
|
+
|
45
|
+
with self.subTest("Path format"):
|
46
|
+
self.assertIn(".", self.a.get_order())
|
47
|
+
self.assertIn(".", self.c.get_order())
|
48
|
+
|
49
|
+
# --- 3. Ancestors and Descendants ------------------------------------------------
|
50
|
+
|
51
|
+
def test_ancestors_and_descendants(self):
|
52
|
+
TestModel.tasks.add("update", None)
|
53
|
+
TestModel.tasks.run()
|
54
|
+
|
55
|
+
ancestors = set(
|
56
|
+
self.c.get_ancestors_queryset().values_list("pk", flat=True)
|
57
|
+
)
|
58
|
+
expected_anc = {self.root.pk, self.a.pk, self.c.pk}
|
59
|
+
self.assertEqual(ancestors, expected_anc)
|
60
|
+
|
61
|
+
descendants = set(
|
62
|
+
self.root.get_descendants_queryset(include_self=True)
|
63
|
+
.values_list("pk", flat=True)
|
64
|
+
)
|
65
|
+
expected_desc = {
|
66
|
+
self.root.pk, self.a.pk, self.b.pk, self.c.pk, self.d.pk
|
67
|
+
}
|
68
|
+
|
69
|
+
test = descendants == expected_desc
|
70
|
+
|
71
|
+
self.assertEqual(test, True)
|
72
|
+
|
73
|
+
# --- 4. Moving a node ------------------------------------------------
|
74
|
+
|
75
|
+
def test_move_node(self):
|
76
|
+
# перемещаем c под b и проверяем
|
77
|
+
with transaction.atomic():
|
78
|
+
self.c.move_to(self.b)
|
79
|
+
|
80
|
+
self.c.refresh_from_db()
|
81
|
+
self.assertEqual(self.c.parent_id, self.b.pk)
|
82
|
+
self.assertEqual(self.c.get_depth(), self.b.get_depth() + 1)
|
83
|
+
self.assertTrue(
|
84
|
+
self.c.get_order().startswith(f"{self.b.get_order()}.")
|
85
|
+
)
|
86
|
+
self.assertEqual(self.c.get_order().count("."), self.c.get_depth())
|
87
|
+
|
88
|
+
# --- 5. Removing a node ---------------------------------------------------
|
89
|
+
|
90
|
+
def test_delete_subtree(self):
|
91
|
+
self.a.delete(cascade=False)
|
92
|
+
|
93
|
+
tree_data = TestModel.get_tree_json()
|
94
|
+
|
95
|
+
self.root.check_tree_integrity()
|
96
|
+
qs = TestModel.objects.filter(pk__in=[self.a.pk, self.c.pk]).all()
|
97
|
+
|
98
|
+
self.assertFalse(TestModel.objects.filter(pk=self.a.pk).exists())
|
99
|
+
self.assertTrue(TestModel.objects.filter(pk=self.c.pk).exists())
|
@@ -84,6 +84,48 @@ class TreeQuery:
|
|
84
84
|
sql = self.order_by(sql1, "priority")
|
85
85
|
return sql, params1
|
86
86
|
|
87
|
+
def get_descendants(self, include_self, depth):
|
88
|
+
"""
|
89
|
+
Build SQL for the 'descendants'.
|
90
|
+
|
91
|
+
Relationship using startswith-like logic.
|
92
|
+
Avoids locale-dependent string comparison issues by relying on
|
93
|
+
_path LIKE 'xxx.%'.
|
94
|
+
"""
|
95
|
+
like_pattern = self.node._path + '.%' # emulate startswith
|
96
|
+
|
97
|
+
base_sql = f"""
|
98
|
+
SELECT id, _depth, priority
|
99
|
+
FROM {self.db_table}
|
100
|
+
WHERE _path LIKE %s
|
101
|
+
"""
|
102
|
+
params = [like_pattern]
|
103
|
+
|
104
|
+
if depth is not None:
|
105
|
+
depth_val = getattr(self.node, "_depth", None)
|
106
|
+
if depth_val is None:
|
107
|
+
depth_val = type(self.node).objects.values_list(
|
108
|
+
"_depth", flat=True).get(pk=self.node.pk)
|
109
|
+
base_sql += " AND _depth <= %s"
|
110
|
+
params.append(depth_val + depth)
|
111
|
+
|
112
|
+
if include_self:
|
113
|
+
sql_self = f"""
|
114
|
+
SELECT id, _depth, priority
|
115
|
+
FROM {self.db_table}
|
116
|
+
WHERE id = %s
|
117
|
+
"""
|
118
|
+
union_sql, union_params = self.wrap_union_all([
|
119
|
+
(base_sql, params),
|
120
|
+
(sql_self, [self.node.pk])
|
121
|
+
])
|
122
|
+
else:
|
123
|
+
union_sql, union_params = base_sql, params
|
124
|
+
|
125
|
+
union_sql = self.order_by(union_sql, "_depth, priority")
|
126
|
+
return union_sql, union_params
|
127
|
+
|
128
|
+
'''
|
87
129
|
def get_descendants(self, include_self, depth):
|
88
130
|
"""
|
89
131
|
Build SQL for the 'descendants' relationship.
|
@@ -113,6 +155,7 @@ class TreeQuery:
|
|
113
155
|
|
114
156
|
union_sql = self.order_by(union_sql, "_depth, priority")
|
115
157
|
return union_sql, union_params
|
158
|
+
'''
|
116
159
|
|
117
160
|
def get_ancestors(self, include_self):
|
118
161
|
"""
|
{django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/models/mixins/descendants.py
RENAMED
@@ -32,17 +32,23 @@ class TreeNodeDescendantsMixin(models.Model):
|
|
32
32
|
def get_descendants_queryset(self, include_self=False, depth=None):
|
33
33
|
"""Get the descendants queryset."""
|
34
34
|
path = self.get_order() # calls refresh and gets the current _path
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
35
|
+
|
36
|
+
# from_path = path + '.'
|
37
|
+
# to_path = path + '/'
|
38
|
+
|
39
|
+
# options = {'_path__gte': from_path, '_path__lt': to_path}
|
40
|
+
# if depth:
|
41
|
+
# options["_depth__lt"] = depth
|
42
|
+
# queryset = self._meta.model.objects.filter(**options)
|
43
|
+
# if include_self:
|
44
|
+
# return self._meta.model.objects.filter(pk=self.pk) | queryset
|
45
|
+
# else:
|
46
|
+
# return queryset
|
47
|
+
|
48
|
+
suffix = "" if include_self else '.'
|
49
|
+
path += suffix
|
50
|
+
queryset = self._meta.model.objects.filter(_path__startswith=path)
|
51
|
+
return queryset
|
46
52
|
|
47
53
|
@cached_method
|
48
54
|
def get_descendants_pks(self, include_self=False, depth=None):
|
@@ -5,7 +5,7 @@ Tree update task compiler class.
|
|
5
5
|
Compiles tasks to low-level SQL to update the materialized path (_path), depth
|
6
6
|
(_depth), and node order (priority) when they are shifted or moved.
|
7
7
|
|
8
|
-
Version: 3.
|
8
|
+
Version: 3.1.0
|
9
9
|
Author: Timur Kady
|
10
10
|
Email: timurkady@yandex.com
|
11
11
|
"""
|
@@ -34,9 +34,11 @@ class TreePathCompiler:
|
|
34
34
|
_depth) are recalculated.
|
35
35
|
"""
|
36
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)
|
37
39
|
|
38
40
|
sorting_field = model.sorting_field
|
39
|
-
sorting_fields = ["priority", "id"] if sorting_field == "priority" else [sorting_field] # noqa:
|
41
|
+
sorting_fields = ["priority", "id"] if sorting_field == "priority" else [sorting_field] # noqa: D5017
|
40
42
|
sort_expr = ", ".join([
|
41
43
|
f"c.{field}" if "." not in field else field
|
42
44
|
for field in sorting_fields
|
@@ -44,7 +46,7 @@ class TreePathCompiler:
|
|
44
46
|
|
45
47
|
cte_header = "(id, parent_id, new_priority, new_path, new_depth)"
|
46
48
|
|
47
|
-
row_number_expr = "ROW_NUMBER() OVER (ORDER BY {sort_expr}) - 1"
|
49
|
+
row_number_expr = f"ROW_NUMBER() OVER (ORDER BY {sort_expr}) - 1"
|
48
50
|
hex_expr = SQLCompat.to_hex(row_number_expr)
|
49
51
|
lpad_expr = SQLCompat.lpad(hex_expr, SEGMENT_LENGTH, "'0'")
|
50
52
|
|
@@ -57,7 +59,7 @@ class TreePathCompiler:
|
|
57
59
|
{row_number_expr} AS new_priority,
|
58
60
|
{new_path_expr} AS new_path,
|
59
61
|
0 AS new_depth
|
60
|
-
FROM {
|
62
|
+
FROM {qname} AS c
|
61
63
|
WHERE c.parent_id IS NULL
|
62
64
|
"""
|
63
65
|
params = []
|
@@ -70,16 +72,18 @@ class TreePathCompiler:
|
|
70
72
|
{row_number_expr} AS new_priority,
|
71
73
|
{path_expr} AS new_path,
|
72
74
|
p._depth + 1 AS new_depth
|
73
|
-
FROM {
|
74
|
-
JOIN {
|
75
|
+
FROM {qname} c
|
76
|
+
JOIN {qname} p ON c.parent_id = p.id
|
75
77
|
WHERE p.id = %s
|
76
78
|
"""
|
77
79
|
params = [parent_id]
|
78
80
|
|
79
|
-
recursive_row_number_expr = "ROW_NUMBER() OVER (PARTITION BY c.parent_id ORDER BY {sort_expr}) - 1"
|
81
|
+
recursive_row_number_expr = f"ROW_NUMBER() OVER (PARTITION BY c.parent_id ORDER BY {sort_expr}) - 1"
|
80
82
|
recursive_hex_expr = SQLCompat.to_hex(recursive_row_number_expr)
|
81
|
-
recursive_lpad_expr = SQLCompat.lpad(
|
82
|
-
|
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)
|
83
87
|
|
84
88
|
recursive_sql = f"""
|
85
89
|
SELECT
|
@@ -88,27 +92,22 @@ class TreePathCompiler:
|
|
88
92
|
{recursive_row_number_expr} AS new_priority,
|
89
93
|
{recursive_path_expr} AS new_path,
|
90
94
|
t.new_depth + 1 AS new_depth
|
91
|
-
FROM {
|
95
|
+
FROM {qname} c
|
92
96
|
JOIN tree_cte t ON c.parent_id = t.id
|
93
97
|
"""
|
94
98
|
|
95
|
-
final_sql =
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
SET
|
103
|
-
priority = t.new_priority,
|
104
|
-
_path = t.new_path,
|
105
|
-
_depth = t.new_depth
|
106
|
-
FROM tree_cte t
|
107
|
-
WHERE orig.id = t.id;
|
108
|
-
"""
|
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
|
+
)
|
109
106
|
|
110
107
|
with connection.cursor() as cursor:
|
111
|
-
|
108
|
+
# Make params read-only
|
109
|
+
params = tuple(params)
|
110
|
+
cursor.execute(final_sql, params)
|
112
111
|
|
113
112
|
|
114
113
|
# The End
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
Database compatibility extension module.
|
4
|
+
|
5
|
+
Adapts SQL code to the specific features of SQL syntax of various
|
6
|
+
Database vendors.
|
7
|
+
|
8
|
+
Instead of direct concatenation:
|
9
|
+
old: p._path || '.' || LPAD(...)
|
10
|
+
new: SQLCompat.concat("p._path", "'.'", SQLCompat.lpad(...))
|
11
|
+
|
12
|
+
Instead of TO_HEX(...)
|
13
|
+
old: TO_HEX(...)
|
14
|
+
new: SQLCompat.to_hex(...)
|
15
|
+
|
16
|
+
Instead of LPAD(...)
|
17
|
+
old: LPAD(...)
|
18
|
+
new: SQLCompat.lpad(...)
|
19
|
+
|
20
|
+
Version: 3.1.0
|
21
|
+
Author: Timur Kady
|
22
|
+
Email: timurkady@yandex.com
|
23
|
+
"""
|
24
|
+
|
25
|
+
from django.db import connection
|
26
|
+
from .db_vendor import is_mysql, is_mariadb, is_sqlite, is_mssql
|
27
|
+
from ...settings import TREENODE_PAD_CHAR
|
28
|
+
|
29
|
+
|
30
|
+
class SQLCompat:
|
31
|
+
"""Vendor-Specific SQL Compatibility Layer."""
|
32
|
+
|
33
|
+
@staticmethod
|
34
|
+
def concat(*args):
|
35
|
+
"""Adapt string concatenation to the vendor-specific syntax."""
|
36
|
+
args_joined = ", ".join(args)
|
37
|
+
if is_mysql() or is_mariadb():
|
38
|
+
return f"CONCAT({args_joined})"
|
39
|
+
elif is_mssql():
|
40
|
+
return " + ".join(args)
|
41
|
+
else:
|
42
|
+
return " || ".join(args)
|
43
|
+
|
44
|
+
@staticmethod
|
45
|
+
def to_hex(value):
|
46
|
+
"""Convert integer to uppercase hexadecimal string."""
|
47
|
+
if is_sqlite():
|
48
|
+
return f"UPPER(printf('%x', {value}))"
|
49
|
+
elif is_mysql() or is_mariadb():
|
50
|
+
return f"UPPER(CONV({value}, 10, 16))"
|
51
|
+
else:
|
52
|
+
return f"UPPER(TO_HEX({value}))"
|
53
|
+
|
54
|
+
@staticmethod
|
55
|
+
def lpad(value, length, char=TREENODE_PAD_CHAR):
|
56
|
+
"""Pad string to the specified length."""
|
57
|
+
if is_sqlite():
|
58
|
+
return f"printf('%0{length}s', {value})"
|
59
|
+
else:
|
60
|
+
return f"LPAD({value}, {length}, {char})"
|
61
|
+
|
62
|
+
@staticmethod
|
63
|
+
def update_from(db_table, cte_header, base_sql, recursive_sql, update_fields):
|
64
|
+
"""
|
65
|
+
Generate final SQL for updating via recursive CTE.
|
66
|
+
|
67
|
+
PostgreSQL uses UPDATE ... FROM.
|
68
|
+
Other engines use vendor-specific strategies.
|
69
|
+
"""
|
70
|
+
qt = connection.ops.quote_name(db_table)
|
71
|
+
def qf(f): return connection.ops.quote_name(f)
|
72
|
+
|
73
|
+
cte_alias = {
|
74
|
+
"priority": "new_priority",
|
75
|
+
"_path": "new_path",
|
76
|
+
"_depth": "new_depth",
|
77
|
+
}
|
78
|
+
|
79
|
+
if connection.vendor == "postgresql":
|
80
|
+
set_clause = ", ".join(
|
81
|
+
f"{qf(f)} = t.{cte_alias.get(f, f)}" for f in update_fields
|
82
|
+
)
|
83
|
+
return f"""
|
84
|
+
WITH RECURSIVE tree_cte {cte_header} AS (
|
85
|
+
{base_sql}
|
86
|
+
UNION ALL
|
87
|
+
{recursive_sql}
|
88
|
+
)
|
89
|
+
UPDATE {qt} AS orig
|
90
|
+
SET {set_clause}
|
91
|
+
FROM tree_cte t
|
92
|
+
WHERE orig.id = t.id;
|
93
|
+
"""
|
94
|
+
|
95
|
+
elif connection.vendor in {"microsoft", "mssql"}:
|
96
|
+
set_clause = ", ".join(
|
97
|
+
f"{qt}.{f} = t.{f}" for f in update_fields
|
98
|
+
)
|
99
|
+
return f"""
|
100
|
+
WITH tree_cte {cte_header} AS (
|
101
|
+
{base_sql}
|
102
|
+
UNION ALL
|
103
|
+
{recursive_sql}
|
104
|
+
)
|
105
|
+
UPDATE orig
|
106
|
+
SET {set_clause}
|
107
|
+
FROM {qt} AS orig
|
108
|
+
JOIN tree_cte t ON orig.id = t.id;
|
109
|
+
"""
|
110
|
+
|
111
|
+
elif connection.vendor == "oracle":
|
112
|
+
set_clause = ", ".join(
|
113
|
+
f"orig.{f} = t.{f}" for f in update_fields
|
114
|
+
)
|
115
|
+
return f"""
|
116
|
+
WITH tree_cte {cte_header} AS (
|
117
|
+
{base_sql}
|
118
|
+
UNION ALL
|
119
|
+
{recursive_sql}
|
120
|
+
)
|
121
|
+
MERGE INTO {qt} orig
|
122
|
+
USING tree_cte t
|
123
|
+
ON (orig.id = t.id)
|
124
|
+
WHEN MATCHED THEN UPDATE SET
|
125
|
+
{set_clause};
|
126
|
+
"""
|
127
|
+
|
128
|
+
else:
|
129
|
+
set_clause = ", ".join(
|
130
|
+
f"{qf(f)} = (SELECT t.{f} FROM tree_cte t WHERE t.id = {qt}.id)"
|
131
|
+
for f in update_fields
|
132
|
+
)
|
133
|
+
where_clause = f"id IN (SELECT id FROM tree_cte)"
|
134
|
+
return f"""
|
135
|
+
WITH RECURSIVE tree_cte {cte_header} AS (
|
136
|
+
{base_sql}
|
137
|
+
UNION ALL
|
138
|
+
{recursive_sql}
|
139
|
+
)
|
140
|
+
UPDATE {qt}
|
141
|
+
SET {set_clause}
|
142
|
+
WHERE {where_clause};
|
143
|
+
"""
|
144
|
+
|
145
|
+
# The End
|
@@ -10,14 +10,17 @@ Email: timurkady@yandex.com
|
|
10
10
|
"""
|
11
11
|
|
12
12
|
import json
|
13
|
-
|
13
|
+
import logging
|
14
|
+
from django.core.exceptions import ValidationError
|
14
15
|
from django.forms.models import model_to_dict
|
15
16
|
from django.http import (
|
16
17
|
JsonResponse, HttpResponseBadRequest, HttpResponseNotFound,
|
17
18
|
)
|
18
|
-
|
19
|
+
from django.utils.translation import gettext_lazy as _
|
19
20
|
from django.views import View
|
20
21
|
|
22
|
+
logger = logging.getLogger("treenode.views.crud")
|
23
|
+
|
21
24
|
|
22
25
|
class TreeNodeBaseAPIView(View):
|
23
26
|
"""Simple API View for TreeNode-based models."""
|
@@ -128,8 +131,14 @@ class TreeNodeBaseAPIView(View):
|
|
128
131
|
obj.full_clean()
|
129
132
|
obj.save()
|
130
133
|
return JsonResponse(model_to_dict(obj), status=201)
|
131
|
-
except
|
132
|
-
|
134
|
+
except ValidationError as ve:
|
135
|
+
# give the client clear field errors
|
136
|
+
return JsonResponse({"errors": ve.message_dict}, status=400)
|
137
|
+
except Exception:
|
138
|
+
# log full information for development
|
139
|
+
logger.exception("Failed to create node")
|
140
|
+
return JsonResponse(
|
141
|
+
{"error": _("Failed to create node")}, status=400)
|
133
142
|
|
134
143
|
def put(self, request, pk):
|
135
144
|
"""
|
@@ -155,10 +164,16 @@ class TreeNodeBaseAPIView(View):
|
|
155
164
|
obj.full_clean()
|
156
165
|
obj.save()
|
157
166
|
return JsonResponse(model_to_dict(obj))
|
167
|
+
except ValidationError as ve:
|
168
|
+
return JsonResponse({"errors": ve.message_dict}, status=400)
|
158
169
|
except self.model.DoesNotExist:
|
159
|
-
|
160
|
-
|
161
|
-
|
170
|
+
# give the client clear field errors
|
171
|
+
return HttpResponseNotFound(_("Node not found (pk={pk})."))
|
172
|
+
except Exception:
|
173
|
+
# log full information for development
|
174
|
+
logger.exception("Error replacing node %s", pk)
|
175
|
+
return JsonResponse(
|
176
|
+
{"error": _("Failed to update node")}, status=400)
|
162
177
|
|
163
178
|
def patch(self, request, pk):
|
164
179
|
"""
|
@@ -200,10 +215,16 @@ class TreeNodeBaseAPIView(View):
|
|
200
215
|
"id": obj.pk,
|
201
216
|
"cascade": cascade
|
202
217
|
})
|
218
|
+
except ValidationError as ve:
|
219
|
+
# give the client clear field errors|
|
220
|
+
return JsonResponse({"errors": ve.message_dict}, status=400)
|
203
221
|
except self.model.DoesNotExist:
|
204
|
-
return HttpResponseNotFound("Node not found.")
|
205
|
-
except Exception
|
206
|
-
|
222
|
+
return HttpResponseNotFound(_("Node not found (pk={pk})."))
|
223
|
+
except Exception:
|
224
|
+
# log full information for development
|
225
|
+
logger.exception("Error deleting node %s", pk)
|
226
|
+
return JsonResponse(
|
227
|
+
{"error": _("Error deleting node")}, status=400)
|
207
228
|
|
208
229
|
|
209
230
|
# The End
|
@@ -21,6 +21,8 @@ from django.utils.translation import gettext_lazy as _
|
|
21
21
|
class TreeWidget(forms.Widget):
|
22
22
|
"""Custom widget for hierarchical tree selection."""
|
23
23
|
|
24
|
+
template_name = "django/forms/widgets/select.html"
|
25
|
+
|
24
26
|
class Media:
|
25
27
|
"""Meta class to define required CSS and JS files."""
|
26
28
|
|
@@ -1,91 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
"""
|
3
|
-
Automated tests for TreeNodeModel.
|
4
|
-
"""
|
5
|
-
|
6
|
-
import unittest
|
7
|
-
from django.db import transaction
|
8
|
-
from supertree.models import TestNode # путь поправь, если нужно
|
9
|
-
|
10
|
-
PATH_DIGITS = 3
|
11
|
-
|
12
|
-
|
13
|
-
def _set_path(array):
|
14
|
-
"""Store the path as a zero-padded hex string."""
|
15
|
-
return '.'.join(f"{n:0{PATH_DIGITS}X}" for n in array)
|
16
|
-
|
17
|
-
|
18
|
-
class TreeNodeModelTests(unittest.TestCase):
|
19
|
-
"""Test cases for the TreeNodeModel behavior."""
|
20
|
-
|
21
|
-
def setUp(self):
|
22
|
-
# Чистим таблицу перед каждым тестом
|
23
|
-
TestNode.objects.all().delete()
|
24
|
-
|
25
|
-
@transaction.atomic
|
26
|
-
def test_tree_operations(self):
|
27
|
-
"""Main test covering tree creation, move, and serialization."""
|
28
|
-
|
29
|
-
# 1️⃣ Create a test tree
|
30
|
-
root = TestNode.objects.create(name="root", priority=0)
|
31
|
-
node_a = TestNode.objects.create(name="A", parent=root, priority=1)
|
32
|
-
node_b = TestNode.objects.create(name="B", parent=root, priority=2)
|
33
|
-
node_c = TestNode.objects.create(name="C", parent=node_a, priority=1)
|
34
|
-
node_d = TestNode.objects.create(name="D", parent=node_a, priority=2)
|
35
|
-
|
36
|
-
self.assertEqual(TestNode.objects.count(), 5)
|
37
|
-
|
38
|
-
# 2️⃣ Check the saving of `_path`
|
39
|
-
path_a = node_a.get_order()
|
40
|
-
path_c = node_c.get_order()
|
41
|
-
|
42
|
-
self.assertEqual(node_a.get_depth(), 1)
|
43
|
-
self.assertEqual(node_c._depth, 2)
|
44
|
-
self.assertTrue(isinstance(path_a, str) and '.' in path_a)
|
45
|
-
self.assertTrue(isinstance(path_c, str) and '.' in path_c)
|
46
|
-
|
47
|
-
# 3️⃣ Check ancestors and descendants
|
48
|
-
ancestor_ids = set(
|
49
|
-
node_c.get_ancestors_queryset().values_list("pk", flat=True))
|
50
|
-
self.assertEqual(ancestor_ids, {root.pk, node_a.pk, node_c.pk})
|
51
|
-
|
52
|
-
descendant_ids = set(root.get_descendants_queryset(
|
53
|
-
include_self=True).values_list("pk", flat=True))
|
54
|
-
self.assertEqual(descendant_ids, {
|
55
|
-
root.pk, node_a.pk, node_b.pk, node_c.pk, node_d.pk})
|
56
|
-
|
57
|
-
# 4️⃣ Move node_c under node_b
|
58
|
-
node_c.move_to(node_b)
|
59
|
-
self.assertEqual(node_c.parent.pk, node_b.pk)
|
60
|
-
self.assertEqual(node_c.get_depth(), node_b.get_depth() + 1)
|
61
|
-
self.assertTrue(node_c.get_order().startswith(node_b.get_order() + '.'))
|
62
|
-
self.assertEqual(node_c.get_order().count('.'), node_c.get_depth())
|
63
|
-
|
64
|
-
# 5️⃣ Check deletion
|
65
|
-
node_a.delete()
|
66
|
-
self.assertFalse(TestNode.objects.filter(pk=node_a.pk).exists())
|
67
|
-
self.assertTrue(TestNode.objects.filter(pk=node_c.pk).exists())
|
68
|
-
|
69
|
-
# 6️⃣ Check save and load operations
|
70
|
-
TestNode.objects.all().delete()
|
71
|
-
self.assertFalse(TestNode.objects.exists())
|
72
|
-
|
73
|
-
# Recreate the tree
|
74
|
-
root = TestNode.objects.create(name="root", priority=0)
|
75
|
-
node_a = TestNode.objects.create(name="A", parent=root, priority=1)
|
76
|
-
node_b = TestNode.objects.create(name="B", parent=root, priority=2)
|
77
|
-
node_c = TestNode.objects.create(name="C", parent=node_a, priority=1)
|
78
|
-
node_d = TestNode.objects.create(name="D", parent=node_a, priority=2)
|
79
|
-
|
80
|
-
original_tree = TestNode.get_tree()
|
81
|
-
tree_json = TestNode.get_tree_json()
|
82
|
-
tree_data = TestNode.load_tree_json(tree_json)
|
83
|
-
|
84
|
-
TestNode.objects.all().delete()
|
85
|
-
self.assertFalse(TestNode.objects.exists())
|
86
|
-
|
87
|
-
TestNode.load_tree(tree_data)
|
88
|
-
new_tree = TestNode.get_tree_json()
|
89
|
-
|
90
|
-
# ✅ Checking the integrity of the tree after all operations
|
91
|
-
self.assertEqual(root.check_tree_integrity(verbose=False), [])
|
@@ -1,60 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
"""
|
3
|
-
Database compatibility extension module.
|
4
|
-
|
5
|
-
Adapts SQL code to the specific features of SQL syntax of various
|
6
|
-
Database vendors.
|
7
|
-
|
8
|
-
Instead of direct concatenation:
|
9
|
-
old: p._path || '.' || LPAD(...)
|
10
|
-
new: SQLCompat.concat("p._path", "'.'", SQLCompat.lpad(...))
|
11
|
-
|
12
|
-
Instead of TO_HEX(...)
|
13
|
-
old: TO_HEX(...)
|
14
|
-
new: SQLCompat.to_hex(...)
|
15
|
-
|
16
|
-
Instead of LPAD(...)
|
17
|
-
old: LPAD(...)
|
18
|
-
new: SQLCompat.lpad(...)
|
19
|
-
|
20
|
-
Version: 3.0.0
|
21
|
-
Author: Timur Kady
|
22
|
-
Email: timurkady@yandex.com
|
23
|
-
"""
|
24
|
-
|
25
|
-
from .db_vendor import is_mysql, is_mariadb, is_sqlite, is_mssql
|
26
|
-
from ...settings import TREENODE_PAD_CHAR
|
27
|
-
|
28
|
-
|
29
|
-
class SQLCompat:
|
30
|
-
"""Vendor-Specific SQL Compatibility Layer."""
|
31
|
-
|
32
|
-
@staticmethod
|
33
|
-
def concat(*args):
|
34
|
-
"""Adapt string concatenation to the vendor-specific syntax."""
|
35
|
-
args_joined = ", ".join(args)
|
36
|
-
if is_mysql() or is_mariadb():
|
37
|
-
return f"CONCAT({args_joined})"
|
38
|
-
elif is_mssql():
|
39
|
-
return " + ".join(args)
|
40
|
-
else:
|
41
|
-
return " || ".join(args)
|
42
|
-
|
43
|
-
@staticmethod
|
44
|
-
def to_hex(value):
|
45
|
-
"""Convert integer to hexadecimal string."""
|
46
|
-
if is_sqlite():
|
47
|
-
return f"printf('%x', {value})"
|
48
|
-
else:
|
49
|
-
return f"TO_HEX({value})"
|
50
|
-
|
51
|
-
@staticmethod
|
52
|
-
def lpad(value, length, char=TREENODE_PAD_CHAR):
|
53
|
-
"""Pad string to the specified length."""
|
54
|
-
if is_sqlite():
|
55
|
-
return (f"substr(replace(hex(zeroblob({length})), '00', {char}), "
|
56
|
-
f"1, {length} - length({value})) || {value}")
|
57
|
-
else:
|
58
|
-
return f"LPAD({value}, {length}, {char})"
|
59
|
-
|
60
|
-
# The End
|
File without changes
|
File without changes
|
{django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/django_fast_treenode.egg-info/SOURCES.txt
RENAMED
File without changes
|
File without changes
|
{django_fast_treenode-3.0.0 → django_fast_treenode-3.0.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.0.0 → django_fast_treenode-3.0.2}/treenode/models/mixins/__init__.py
RENAMED
File without changes
|
{django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/models/mixins/ancestors.py
RENAMED
File without changes
|
{django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/models/mixins/children.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/models/mixins/properties.py
RENAMED
File without changes
|
File without changes
|
{django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/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.0 → django_fast_treenode-3.0.2}/treenode/static/css/tree_widget.css
RENAMED
File without changes
|
{django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/css/treenode_admin.css
RENAMED
File without changes
|
{django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/css/treenode_tabs.css
RENAMED
File without changes
|
File without changes
|
{django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/treenode/static/js/lz-string.min.js
RENAMED
File without changes
|
File without changes
|
{django_fast_treenode-3.0.0 → django_fast_treenode-3.0.2}/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
|