django-fast-treenode 2.0.10__py3-none-any.whl → 2.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/LICENSE +2 -2
- django_fast_treenode-2.1.0.dist-info/METADATA +161 -0
- django_fast_treenode-2.1.0.dist-info/RECORD +75 -0
- {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/WHEEL +1 -1
- treenode/admin/__init__.py +9 -0
- treenode/admin/admin.py +295 -0
- treenode/admin/changelist.py +65 -0
- treenode/admin/mixins.py +302 -0
- treenode/apps.py +12 -1
- treenode/cache.py +2 -2
- treenode/docs/.gitignore +0 -0
- treenode/docs/about.md +36 -0
- treenode/docs/admin.md +104 -0
- treenode/docs/api.md +739 -0
- treenode/docs/cache.md +187 -0
- treenode/docs/import_export.md +35 -0
- treenode/docs/index.md +30 -0
- treenode/docs/installation.md +74 -0
- treenode/docs/migration.md +145 -0
- treenode/docs/models.md +128 -0
- treenode/docs/roadmap.md +45 -0
- treenode/forms.py +33 -22
- treenode/managers/__init__.py +21 -0
- treenode/managers/adjacency.py +203 -0
- treenode/managers/closure.py +278 -0
- treenode/models/__init__.py +2 -1
- treenode/models/adjacency.py +343 -0
- treenode/models/classproperty.py +3 -0
- treenode/models/closure.py +39 -65
- treenode/models/factory.py +12 -2
- treenode/models/mixins/__init__.py +23 -0
- treenode/models/mixins/ancestors.py +65 -0
- treenode/models/mixins/children.py +81 -0
- treenode/models/mixins/descendants.py +66 -0
- treenode/models/mixins/family.py +63 -0
- treenode/models/mixins/logical.py +68 -0
- treenode/models/mixins/node.py +210 -0
- treenode/models/mixins/properties.py +156 -0
- treenode/models/mixins/roots.py +96 -0
- treenode/models/mixins/siblings.py +99 -0
- treenode/models/mixins/tree.py +344 -0
- treenode/signals.py +26 -0
- treenode/static/treenode/css/tree_widget.css +201 -31
- treenode/static/treenode/css/treenode_admin.css +48 -41
- treenode/static/treenode/js/tree_widget.js +269 -131
- treenode/static/treenode/js/treenode_admin.js +131 -171
- treenode/templates/admin/tree_node_changelist.html +6 -0
- treenode/templates/admin/tree_node_import.html +27 -9
- treenode/templates/admin/tree_node_import_report.html +32 -0
- treenode/templates/admin/treenode_ajax_rows.html +7 -0
- treenode/tests/tests.py +488 -0
- treenode/urls.py +10 -6
- treenode/utils/__init__.py +2 -0
- treenode/utils/aid.py +46 -0
- treenode/utils/base16.py +38 -0
- treenode/utils/base36.py +3 -1
- treenode/utils/db.py +116 -0
- treenode/utils/exporter.py +63 -36
- treenode/utils/importer.py +168 -161
- treenode/utils/radix.py +61 -0
- treenode/version.py +2 -2
- treenode/views.py +119 -38
- treenode/widgets.py +104 -40
- django_fast_treenode-2.0.10.dist-info/METADATA +0 -698
- django_fast_treenode-2.0.10.dist-info/RECORD +0 -41
- treenode/admin.py +0 -396
- treenode/docs/Documentation +0 -664
- treenode/managers.py +0 -281
- treenode/models/proxy.py +0 -650
- {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
MIT License
|
2
2
|
|
3
|
-
Copyright (c) 2020-
|
3
|
+
Copyright (c) 2020-2025 Timur Kady
|
4
4
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
18
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
-
SOFTWARE.
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1,161 @@
|
|
1
|
+
Metadata-Version: 2.2
|
2
|
+
Name: django-fast-treenode
|
3
|
+
Version: 2.1.0
|
4
|
+
Summary: Application for supporting tree (hierarchical) data structure in Django projects
|
5
|
+
Home-page: https://github.com/TimurKady/django-fast-treenode
|
6
|
+
Author: Timur Kady
|
7
|
+
Author-email: Timur Kady <timurkady@yandex.com>
|
8
|
+
License: MIT License
|
9
|
+
|
10
|
+
Copyright (c) 2020-2025 Timur Kady
|
11
|
+
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
14
|
+
in the Software without restriction, including without limitation the rights
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
17
|
+
furnished to do so, subject to the following conditions:
|
18
|
+
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
20
|
+
copies or substantial portions of the Software.
|
21
|
+
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
28
|
+
SOFTWARE.
|
29
|
+
|
30
|
+
Project-URL: Homepage, https://github.com/TimurKady/django-fast-treenode
|
31
|
+
Project-URL: Documentation, https://github.com/TimurKady/django-fast-treenode#readme
|
32
|
+
Project-URL: Source, https://github.com/TimurKady/django-fast-treenode
|
33
|
+
Project-URL: Issues, https://github.com/TimurKady/django-fast-treenode/issues
|
34
|
+
Classifier: Development Status :: 5 - Production/Stable
|
35
|
+
Classifier: Intended Audience :: Developers
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
37
|
+
Classifier: Programming Language :: Python :: 3.9
|
38
|
+
Classifier: Programming Language :: Python :: 3.10
|
39
|
+
Classifier: Programming Language :: Python :: 3.11
|
40
|
+
Classifier: Programming Language :: Python :: 3.12
|
41
|
+
Classifier: Programming Language :: Python :: 3.13
|
42
|
+
Classifier: Programming Language :: Python :: 3.14
|
43
|
+
Classifier: Framework :: Django
|
44
|
+
Classifier: Framework :: Django :: 4.0
|
45
|
+
Classifier: Framework :: Django :: 4.1
|
46
|
+
Classifier: Framework :: Django :: 4.2
|
47
|
+
Classifier: Framework :: Django :: 5.0
|
48
|
+
Classifier: Framework :: Django :: 5.1
|
49
|
+
Classifier: Framework :: Django :: 5.2
|
50
|
+
Classifier: License :: OSI Approved :: MIT License
|
51
|
+
Classifier: Operating System :: OS Independent
|
52
|
+
Requires-Python: >=3.9
|
53
|
+
Description-Content-Type: text/markdown
|
54
|
+
License-File: LICENSE
|
55
|
+
Requires-Dist: Django>=4.0
|
56
|
+
Requires-Dist: pympler>=1.0
|
57
|
+
Requires-Dist: django-widget-tweaks>=1.5
|
58
|
+
Provides-Extra: import-export
|
59
|
+
Requires-Dist: openpyxl; extra == "import-export"
|
60
|
+
Requires-Dist: pyyaml; extra == "import-export"
|
61
|
+
Requires-Dist: xlsxwriter; extra == "import-export"
|
62
|
+
|
63
|
+
|
64
|
+
# ⚠️ Version 2.1.0 - betta.
|
65
|
+
|
66
|
+
# Django-fast-treenode
|
67
|
+
**Combining Adjacency List and Closure Table for Optimal Performance**
|
68
|
+
|
69
|
+
|
70
|
+
**Django Fast TreeNode** is a high-performance Django application for working with tree structures, combining **Adjacency List** and **Closure Table** models. Each **TreeNodeModel** instance maintains two synchronized tables, enabling most operations to be performed with a single database query.
|
71
|
+
|
72
|
+
## Features
|
73
|
+
- **Hybrid storage model**: Combines Adjacency List and Closure Table for optimal performance.
|
74
|
+
- **Custom caching system**: A built-in caching mechanism, specifically designed for this package, significantly boosts execution speed.
|
75
|
+
- **Efficient queries**: Retrieve ancestors, descendants, breadcrumbs, and tree depth with only one SQL queriy.
|
76
|
+
- **Bulk operations**: Supports fast insertion, movement, and deletion of nodes.
|
77
|
+
- **Flexibility**: Fully integrates with Django ORM and adapts to various business logic needs.
|
78
|
+
- **Admin panel integration**: Full compatibility with Django's admin panel, allowing intuitive management of tree structures.
|
79
|
+
- **Import & Export functionality**: Built-in support for importing and exporting tree structures in multiple formats (CSV, JSON, XLSX, YAML, TSV), including integration with the Django admin panel.
|
80
|
+
|
81
|
+
## Use Cases
|
82
|
+
Django Fast TreeNode is suitable for a wide range of applications, from simple directories to complex systems with deep hierarchical structures:
|
83
|
+
- **Categories and taxonomies**: Manage product categories, tags, and classification systems.
|
84
|
+
- **Menus and navigation**: Create tree-like menus and nested navigation structures.
|
85
|
+
- **Forums and comments**: Store threaded discussions and nested comment chains.
|
86
|
+
- **Geographical data**: Represent administrative divisions, regions, and areas of influence.
|
87
|
+
- **Organizational and Business Structures**: Model company hierarchies, business processes, employees and departments.
|
88
|
+
|
89
|
+
In all applications, `django-fast-treenode` models show excellent performance and stability.
|
90
|
+
|
91
|
+
## Quick start
|
92
|
+
1. Run `pip install django-fast-treenode`.
|
93
|
+
2. Add `treenode` to `settings.INSTALLED_APPS`.
|
94
|
+
|
95
|
+
```python
|
96
|
+
INSTALLED_APPS = [
|
97
|
+
...
|
98
|
+
'treenode',
|
99
|
+
]
|
100
|
+
```
|
101
|
+
|
102
|
+
3. Define your model inherit from `treenode.models.TreeNodeModel`.
|
103
|
+
|
104
|
+
```python
|
105
|
+
from treenode.models import TreeNodeModel
|
106
|
+
|
107
|
+
class Category(TreeNodeModel):
|
108
|
+
name = models.CharField(max_length=255)
|
109
|
+
treenode_display_field = "name"
|
110
|
+
```
|
111
|
+
|
112
|
+
4. Make your model-admin inherit from `treenode.admin.TreeNodeModelAdmin`.
|
113
|
+
|
114
|
+
```python
|
115
|
+
from treenode.admin import TreeNodeModelAdmin
|
116
|
+
from .models import Category
|
117
|
+
|
118
|
+
@admin.register(Category)
|
119
|
+
class CategoryAdmin(TreeNodeModelAdmin):
|
120
|
+
list_display = ("name",)
|
121
|
+
search_fields = ("name",)
|
122
|
+
```
|
123
|
+
5. Run migrations.
|
124
|
+
|
125
|
+
```bash
|
126
|
+
python manage.py makemigrations
|
127
|
+
python manage.py migrate
|
128
|
+
```
|
129
|
+
|
130
|
+
6. Run server and use!
|
131
|
+
|
132
|
+
```bash
|
133
|
+
>>> root = Category.objects.create(name="Root")
|
134
|
+
>>> child = Category.objects.create(name="Child")
|
135
|
+
>>> child.set_parent(root)
|
136
|
+
>>> root_descendants_list = root.get_descendants()
|
137
|
+
>>> root_children_queryset = root.get_children_queryset()
|
138
|
+
>>> ancestors_pks = child.get_ancestors_pks()
|
139
|
+
```
|
140
|
+
|
141
|
+
## Documentation
|
142
|
+
Full documentation is available at [this link](#).
|
143
|
+
|
144
|
+
Quick access links:
|
145
|
+
* [Installation, configuration and fine tuning](#)
|
146
|
+
* [Model Inheritance and Extensions](#)
|
147
|
+
* [Working with Admin Classes](#)
|
148
|
+
* [API Reference](#)
|
149
|
+
* [Import & Export](#)
|
150
|
+
* [Caching and working with cache](#)
|
151
|
+
* [Migration and upgrade guide](#)
|
152
|
+
|
153
|
+
Your wishes, objections, comments are welcome.
|
154
|
+
|
155
|
+
## License
|
156
|
+
Released under [MIT License](https://github.com/TimurKady/django-fast-treenode/blob/main/LICENSE).
|
157
|
+
|
158
|
+
## Credits
|
159
|
+
Thanks to everyone who contributed to the development and testing of this package, as well as the Django community for their inspiration and support.
|
160
|
+
|
161
|
+
Special thanks to [Fabio Caccamo](https://github.com/fabiocaccamo) for the idea behind creating a fast Django application for handling hierarchies and [Mathieu Leplatre](https://github.com/leplatrem) for the advice used in writing this application.
|
@@ -0,0 +1,75 @@
|
|
1
|
+
treenode/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
treenode/apps.py,sha256=a7UasXiZZudPccjmHEudP79TkhR_53Mvnb-dBXLHRRQ,862
|
3
|
+
treenode/cache.py,sha256=utTmMJ87fjbutaKbOcSp8bHqIDbI_Yr2nPXJoZLAqlQ,7213
|
4
|
+
treenode/forms.py,sha256=Mjrpuyd1CPsitcElDVagE3k-p2kU4xIlRuy1f5Zgt3c,3800
|
5
|
+
treenode/signals.py,sha256=ERrlKjGqhYaPYVKKRk1JBBlPFOmJKpJ6bXsJavcTlo0,518
|
6
|
+
treenode/urls.py,sha256=CsgX0hRyDVrMS8YnRlr_CxmDlgGIhDpqZ9ldoMYZCac,866
|
7
|
+
treenode/version.py,sha256=JWmJNiw3wLkpTwygbmlmO1XucR3NTB_QQbdATOp71ug,222
|
8
|
+
treenode/views.py,sha256=rEZEgdbEA3AJDHrvtrAm-t60QTJcJ4JEhNsNMR1Y_I4,5549
|
9
|
+
treenode/widgets.py,sha256=Mi0F-AK_UcmU6C50ENK9vv6xGQNuDtrtzXSnXSOXhLM,4760
|
10
|
+
treenode/admin/__init__.py,sha256=TdlPIyRW8i9qTVqGLmLWiBw4DyoGHUYZErE6rCyGOPE,119
|
11
|
+
treenode/admin/admin.py,sha256=6H3N2Dg6l-MrFwIcyKR5YENg0cEo-I4uKCP9MuhHkqo,10580
|
12
|
+
treenode/admin/changelist.py,sha256=YZm3zNniX75CgLjnbHpVr0OIP91halDEBHmrcS8m5Og,2128
|
13
|
+
treenode/admin/mixins.py,sha256=-dVZwEjKsfRzMkBe87dkI0SZ9MH45YE_o39SIhSJWy4,11194
|
14
|
+
treenode/docs/.gitignore,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
|
+
treenode/docs/about.md,sha256=x0RnV3UiTn-Ii0nBpTPgQmm6Al_DDwtl72TUC2oH_-0,3532
|
16
|
+
treenode/docs/admin.md,sha256=uA3esa2AF3epcXtfEN-JUoUXojw34PAGg-g3w9aqY8I,4653
|
17
|
+
treenode/docs/api.md,sha256=xDq9BDIhg7qOOZa88fF2WhHL8Wr77dYLmLnZ2fctF8I,23524
|
18
|
+
treenode/docs/cache.md,sha256=mX4P0a4ndyLZCEDKEXPBEcd8LSWqvEy_HdYIHgmnf8g,8323
|
19
|
+
treenode/docs/import_export.md,sha256=sbI1iR-eI54obTaIeR5xDhjn8FAy3dJ0aR4rfbCRcyU,2134
|
20
|
+
treenode/docs/index.md,sha256=wXq_8AA9RooIG_6V_Yq5KPgrxyOKkc84sq-lwa-FQDk,1140
|
21
|
+
treenode/docs/installation.md,sha256=w_ImbNgdt0Ot-z2ERApkRcBz1TdXloOFkNzU3EJBxeA,2512
|
22
|
+
treenode/docs/migration.md,sha256=eFZO_DWJA8-015hD0he7_HzxgMnbOv192M-9Zf1uLcQ,6581
|
23
|
+
treenode/docs/models.md,sha256=PxLHVvQDsH-IsD3pwilTtjUeB8hROYWNf3jdbHjhESY,5781
|
24
|
+
treenode/docs/roadmap.md,sha256=T3aB3LfqhfXdpc9phTA4oPlQriKTjfuoaMBvvMcHa90,3336
|
25
|
+
treenode/managers/__init__.py,sha256=EG_tj9P1Hama3kaqMfHck4lfzUWoPaJJVOXe3qaKMUo,585
|
26
|
+
treenode/managers/adjacency.py,sha256=NVN8dq5z7gIh90yqW2uxV7MokmUfXTOT7crqMDyMaH0,7889
|
27
|
+
treenode/managers/closure.py,sha256=PcScdJJUnLcKe8Y1wqROYPsRtAnBMUO4xn5sILk9AIM,10638
|
28
|
+
treenode/models/__init__.py,sha256=pBiMlEpC_Thh7asraNzA7W_7BKu2oAHtcn-K6_sdJe8,112
|
29
|
+
treenode/models/adjacency.py,sha256=ijStfIQDSd48L3nA8OnLD1nHGYo5YsnokqUVfzDt68w,12422
|
30
|
+
treenode/models/classproperty.py,sha256=J4W6snsfsEUSHKHkIlM9yOJYQ_FSrp3P3oEYMKJengg,571
|
31
|
+
treenode/models/closure.py,sha256=NEC8pi9QIxNtUnctEi0lPHBHEPX2K3V1oeSs9JrDGA0,4588
|
32
|
+
treenode/models/factory.py,sha256=10FEGGC5PGWaR58qErs0oOrCS0KeI8x9H-SknZAAWqw,2291
|
33
|
+
treenode/models/mixins/__init__.py,sha256=gTdMZFh1slNHMvxrnu-hGl46xqnWd4W7TOEFWTVJq40,757
|
34
|
+
treenode/models/mixins/ancestors.py,sha256=_nF8V99SBjT-G88BmcRtiTC0DgSEIgHrI0g02jqTnss,2075
|
35
|
+
treenode/models/mixins/children.py,sha256=OchaH6m6pOr6uuiZRRBHoZXoCSWM-ENTNWu1iLHXaBM,2564
|
36
|
+
treenode/models/mixins/descendants.py,sha256=2mhnIhC8VJomTqntzzAwFFW_CcMiwujzQoD5_mfMsK0,2208
|
37
|
+
treenode/models/mixins/family.py,sha256=h2IRRADkQxve97QqBHKv0evVz4cFQtcNR8CbPi9Ri_w,1645
|
38
|
+
treenode/models/mixins/logical.py,sha256=jlhBSq3AfCYNyNjqyKM9siyioS3SYcGD-aG2b4MV2RM,2169
|
39
|
+
treenode/models/mixins/node.py,sha256=E-eUgZoqKlyaCaDk0H7VQg6tDnfeufyGHn7hjTxIDqM,7694
|
40
|
+
treenode/models/mixins/properties.py,sha256=pfv80KLXcPeGx00IFCBcst1_cf0AmzhjshFjq1XQWMY,3876
|
41
|
+
treenode/models/mixins/roots.py,sha256=MoFQq1fph70awc26UMUbfeTpt0ToUOvMz1c7LlDyIP8,2956
|
42
|
+
treenode/models/mixins/siblings.py,sha256=fh0ZrlFXKxOQ4Qrp6sElTMvRhU5PyRRykLHDcbH-3Rk,3113
|
43
|
+
treenode/models/mixins/tree.py,sha256=CsO0ynwcwkrWgQbTzvF4yws-y7n1GGM2zImJH0hgV00,13042
|
44
|
+
treenode/static/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
45
|
+
treenode/static/treenode/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
46
|
+
treenode/static/treenode/css/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
47
|
+
treenode/static/treenode/css/tree_widget.css,sha256=SE74hZaOECHe1VKe-N6b-MxcZ6tQrA9d4ctfNHrVvvA,4864
|
48
|
+
treenode/static/treenode/css/treenode_admin.css,sha256=7Ye_bCgIgG-QUcih1jXIda1XxhAkTFLU-0CHcKNCZtw,2238
|
49
|
+
treenode/static/treenode/js/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
50
|
+
treenode/static/treenode/js/tree_widget.js,sha256=SGxm2Awu1Ysk53h1r8JIS5wo9XGQFUD0cz9WqsMQXMs,10331
|
51
|
+
treenode/static/treenode/js/treenode_admin.js,sha256=q_OvlDQPvmv9SfV2u69PztVY2Yrdl4qdlnoAzbbkovA,4747
|
52
|
+
treenode/templates/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
53
|
+
treenode/templates/admin/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
54
|
+
treenode/templates/admin/export_success.html,sha256=xN2D-BCH249CJB10fo_vHYUyFenQ9mFKqq7UTWcrXS4,747
|
55
|
+
treenode/templates/admin/tree_node_changelist.html,sha256=fGRVvWx2EnpiFYeJckyPKV-BCv9I13_ViiNN0LIUZKM,380
|
56
|
+
treenode/templates/admin/tree_node_export.html,sha256=vJxEoGI-US6VdFddxAFgL5r3MgGt6mgA43vltCsbA2k,1043
|
57
|
+
treenode/templates/admin/tree_node_import.html,sha256=unksxTAO2bJbxRkZfrCltHn61MgfqGt2sxIsUOW5dVk,1513
|
58
|
+
treenode/templates/admin/tree_node_import_report.html,sha256=azHJ8JFrSRu60lF1Uh22zs9JXQxZdvOjYdwCtlbaE3I,1133
|
59
|
+
treenode/templates/admin/treenode_ajax_rows.html,sha256=zFyPaTbSyxRjOqQ85SMv__qTIYDjEna6chYODBypDZA,224
|
60
|
+
treenode/templates/widgets/tree_widget.css,sha256=2bEaxu1x7QJZ7erbs2SLMaxeaiMkjQXadfcDEW8wfok,551
|
61
|
+
treenode/templates/widgets/tree_widget.html,sha256=GKcCU-B2FkkJ2BSOuXOw9e_PdYTtADcvyITEXqOlZ9Y,723
|
62
|
+
treenode/tests/tests.py,sha256=9Bd2BhvwtVhYBp5DEtkzKPpAP1iFo4asMsydzuIRASM,19316
|
63
|
+
treenode/utils/__init__.py,sha256=B4bv96ivtHELPv0_DllJa5z-k1QMo7z-MKuvj-3NdtI,356
|
64
|
+
treenode/utils/aid.py,sha256=o8Jgc1vDRtQpx4XYdv0qR5Lqvens55Jfbdca1nr-EOA,1013
|
65
|
+
treenode/utils/base16.py,sha256=U1PMit2aZOpYusG_u1c7eVpXO-cFrFPyVyk9zdHrehg,817
|
66
|
+
treenode/utils/base36.py,sha256=yICmyPE-yyPNO9T2oALOt-b6uYf37ahFfx0R4tXn3X0,847
|
67
|
+
treenode/utils/db.py,sha256=36q4OckKmEd6uHTbMTxdKpV9nOIZ55DAantRWR9bxWg,4297
|
68
|
+
treenode/utils/exporter.py,sha256=mV6Gch7XfW8f_1x3WqWgtV0qekMLdo-_n9gz6GJjXjw,7259
|
69
|
+
treenode/utils/importer.py,sha256=Hvirbd6NyZ2MHa56_jOrUF3kYFeby1DbSLR3mhHy-9s,12891
|
70
|
+
treenode/utils/radix.py,sha256=zHpOuDxsebiv9Gza6snNhAtBKiex6CDrAVRtB6esaWo,1642
|
71
|
+
django_fast_treenode-2.1.0.dist-info/LICENSE,sha256=T0evsb7y-63fg18ovdNSx3wwWWRwyluQvN9J4zFSvfE,1093
|
72
|
+
django_fast_treenode-2.1.0.dist-info/METADATA,sha256=gZTAxkM31OqmnQvHNN5fdj9yFHgB8E-vOlV4gV_Makw,7202
|
73
|
+
django_fast_treenode-2.1.0.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
|
74
|
+
django_fast_treenode-2.1.0.dist-info/top_level.txt,sha256=fmgxHbXyx1O2MPi_9kjx8aL9L-8TmV0gre4Go8XgqFk,9
|
75
|
+
django_fast_treenode-2.1.0.dist-info/RECORD,,
|
treenode/admin/admin.py
ADDED
@@ -0,0 +1,295 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Admin Module
|
4
|
+
|
5
|
+
This module provides Django admin integration for the TreeNode model.
|
6
|
+
It includes custom tree-based sorting, optimized queries, and
|
7
|
+
import/export functionality for hierarchical data structures.
|
8
|
+
|
9
|
+
Version: 2.1.0
|
10
|
+
Author: Timur Kady
|
11
|
+
Email: kaduevtr@gmail.com
|
12
|
+
"""
|
13
|
+
|
14
|
+
|
15
|
+
import importlib
|
16
|
+
from django.contrib import admin
|
17
|
+
from django.db import models
|
18
|
+
from django.http import HttpResponseRedirect
|
19
|
+
from django.utils.safestring import mark_safe
|
20
|
+
from django.urls import reverse, resolve
|
21
|
+
|
22
|
+
from .changelist import SortedChangeList
|
23
|
+
from .mixins import AdminMixin
|
24
|
+
from ..forms import TreeNodeForm
|
25
|
+
from ..widgets import TreeWidget
|
26
|
+
|
27
|
+
import logging
|
28
|
+
|
29
|
+
logger = logging.getLogger(__name__)
|
30
|
+
|
31
|
+
|
32
|
+
class TreeNodeAdminModel(AdminMixin, admin.ModelAdmin):
|
33
|
+
"""
|
34
|
+
TreeNodeAdmin class.
|
35
|
+
|
36
|
+
Admin configuration for TreeNodeModel with import/export functionality.
|
37
|
+
"""
|
38
|
+
|
39
|
+
TREENODE_DISPLAY_MODE_ACCORDION = 'accordion'
|
40
|
+
TREENODE_DISPLAY_MODE_BREADCRUMBS = 'breadcrumbs'
|
41
|
+
TREENODE_DISPLAY_MODE_INDENTATION = 'indentation'
|
42
|
+
|
43
|
+
treenode_display_mode = TREENODE_DISPLAY_MODE_ACCORDION
|
44
|
+
import_export = False # Track import/export availability
|
45
|
+
change_list_template = "admin/tree_node_changelist.html"
|
46
|
+
ordering = []
|
47
|
+
list_per_page = 1000
|
48
|
+
list_sorting_mode_session_key = "treenode_sorting_mode"
|
49
|
+
|
50
|
+
form = TreeNodeForm
|
51
|
+
formfield_overrides = {
|
52
|
+
models.ForeignKey: {"widget": TreeWidget()},
|
53
|
+
}
|
54
|
+
|
55
|
+
class Media:
|
56
|
+
"""Include custom CSS and JavaScript for admin interface."""
|
57
|
+
|
58
|
+
css = {"all": (
|
59
|
+
"treenode/css/treenode_admin.css",
|
60
|
+
)}
|
61
|
+
js = (
|
62
|
+
'admin/js/jquery.init.js',
|
63
|
+
'treenode/js/treenode_admin.js',
|
64
|
+
)
|
65
|
+
|
66
|
+
def drag(self, obj):
|
67
|
+
"""Display an empty column with an icon for future drag-and-drop."""
|
68
|
+
icon = "↕" # "
|
69
|
+
return mark_safe(f'<span class="treenode-drag-handle">{icon}</span>')
|
70
|
+
|
71
|
+
drag.short_description = ""
|
72
|
+
|
73
|
+
def toggle(self, obj):
|
74
|
+
"""Добавление кнопки для открытия/закрытия поддерева, если есть дети."""
|
75
|
+
icon = "►" # ➕➖
|
76
|
+
if obj.get_children_count() > 0:
|
77
|
+
return mark_safe(
|
78
|
+
f'<button class="treenode-toggle" '
|
79
|
+
f'data-node-id="{obj.pk}">'
|
80
|
+
f'{icon}'
|
81
|
+
f'</button>')
|
82
|
+
return mark_safe('<div class="treenode-space"> </div>')
|
83
|
+
|
84
|
+
toggle.short_description = ""
|
85
|
+
|
86
|
+
def __init__(self, model, admin_site):
|
87
|
+
"""Init method."""
|
88
|
+
super().__init__(model, admin_site)
|
89
|
+
|
90
|
+
# If `list_display` is empty, take all `fields`
|
91
|
+
if not self.list_display:
|
92
|
+
self.list_display = [field.name for field in model._meta.fields]
|
93
|
+
|
94
|
+
# Check for necessary dependencies
|
95
|
+
self.import_export = all([
|
96
|
+
importlib.util.find_spec(pkg) is not None
|
97
|
+
for pkg in ["openpyxl", "yaml", "xlsxwriter"]
|
98
|
+
])
|
99
|
+
if not self.import_export:
|
100
|
+
check_results = [
|
101
|
+
pkg for pkg in ["openpyxl", "pyyaml", "xlsxwriter"]
|
102
|
+
if importlib.util.find_spec(pkg) is not None
|
103
|
+
]
|
104
|
+
logger.info("Packages" + ", ".join(check_results) + " are \
|
105
|
+
not installed. Export and import functions are disabled.")
|
106
|
+
|
107
|
+
if self.import_export:
|
108
|
+
from ..utils import TreeNodeImporter, TreeNodeExporter
|
109
|
+
|
110
|
+
self.TreeNodeImporter = TreeNodeImporter
|
111
|
+
self.TreeNodeExporter = TreeNodeExporter
|
112
|
+
else:
|
113
|
+
self.TreeNodeImporter = None
|
114
|
+
self.TreeNodeExporter = None
|
115
|
+
|
116
|
+
def get_queryset(self, request):
|
117
|
+
"""
|
118
|
+
Get QuerySet.
|
119
|
+
|
120
|
+
Redefine the query so that by default only root nodes (nodes with
|
121
|
+
tn_parent=None) are loaded. If there is a search query (parameter "q"),
|
122
|
+
return the full list.
|
123
|
+
"""
|
124
|
+
qs = super().get_queryset(request)
|
125
|
+
|
126
|
+
resolved_match = resolve(request.path)
|
127
|
+
app_label = self.model._meta.app_label
|
128
|
+
model_name = self.model._meta.model_name
|
129
|
+
if resolved_match.url_name == f"{app_label}_{model_name}_change":
|
130
|
+
return qs
|
131
|
+
|
132
|
+
if not request.GET.get("q"):
|
133
|
+
qs = qs.filter(tn_parent__isnull=True)
|
134
|
+
|
135
|
+
field_name = getattr(self.model, 'treenode_display_field')
|
136
|
+
q = request.GET.get("q", "")
|
137
|
+
if not field_name:
|
138
|
+
return qs.none()
|
139
|
+
return qs.select_related('tn_parent')\
|
140
|
+
.filter(**{f"{field_name}__icontains": q})
|
141
|
+
|
142
|
+
def get_form(self, request, obj=None, **kwargs):
|
143
|
+
"""Get Form method."""
|
144
|
+
form = super().get_form(request, obj, **kwargs)
|
145
|
+
if "tn_parent" in form.base_fields:
|
146
|
+
form.base_fields["tn_parent"].widget = TreeWidget()
|
147
|
+
return form
|
148
|
+
|
149
|
+
def get_search_fields(self, request):
|
150
|
+
"""Return the correct search field."""
|
151
|
+
return [self.model.treenode_display_field]
|
152
|
+
|
153
|
+
def get_list_display(self, request):
|
154
|
+
"""Generate list_display dynamically with user-defined preferences."""
|
155
|
+
change_view_cols = (self.drag, self.toggle)
|
156
|
+
user_list_display = list(super().get_list_display(request))
|
157
|
+
|
158
|
+
treenode_display_field = getattr(
|
159
|
+
self.model,
|
160
|
+
'treenode_display_field',
|
161
|
+
'__str__'
|
162
|
+
)
|
163
|
+
|
164
|
+
def treenode_field(obj):
|
165
|
+
return self._get_treenode_field_display(request, obj)
|
166
|
+
|
167
|
+
treenode_field.short_description = self.model._meta.verbose_name
|
168
|
+
|
169
|
+
# If the custom list is empty or contains only '__str__'
|
170
|
+
if not user_list_display or user_list_display == ['__str__']:
|
171
|
+
result = (treenode_field,)
|
172
|
+
else:
|
173
|
+
# Remove `treenode_display_field` if it is the first one and
|
174
|
+
# insert `treenode_field`
|
175
|
+
if user_list_display[0] == treenode_display_field:
|
176
|
+
clean_list = user_list_display[1:]
|
177
|
+
else:
|
178
|
+
clean_list = user_list_display
|
179
|
+
|
180
|
+
# Гарантируем, что treenode_field есть в списке
|
181
|
+
if treenode_field not in clean_list:
|
182
|
+
clean_list.insert(0, treenode_field)
|
183
|
+
|
184
|
+
result = tuple(clean_list)
|
185
|
+
|
186
|
+
return change_view_cols + result
|
187
|
+
|
188
|
+
def get_list_display_links(self, request, list_display):
|
189
|
+
"""Specify that only `treenode_field` should be clickable."""
|
190
|
+
return ('treenode_field',)
|
191
|
+
|
192
|
+
def get_changelist(self, request):
|
193
|
+
"""Use SortedChangeList to sort the results at render time."""
|
194
|
+
return SortedChangeList
|
195
|
+
|
196
|
+
def changelist_view(self, request, extra_context=None):
|
197
|
+
"""Changelist View."""
|
198
|
+
extra_context = extra_context or {}
|
199
|
+
extra_context['import_export_enabled'] = self.import_export
|
200
|
+
|
201
|
+
response = super().changelist_view(request, extra_context=extra_context)
|
202
|
+
|
203
|
+
# If response is a redirect, then there is no point in updating
|
204
|
+
# ChangeList
|
205
|
+
if isinstance(response, HttpResponseRedirect):
|
206
|
+
return response
|
207
|
+
|
208
|
+
if request.GET.get("import_done"):
|
209
|
+
# Create a ChangeList instance manually
|
210
|
+
ChangeListClass = self.get_changelist(request)
|
211
|
+
|
212
|
+
cl = ChangeListClass(
|
213
|
+
request, self.model, self.list_display, self.list_display_links,
|
214
|
+
self.list_filter, self.date_hierarchy, self.search_fields,
|
215
|
+
self.list_select_related, self.list_per_page,
|
216
|
+
self.list_max_show_all, self.list_editable, self
|
217
|
+
)
|
218
|
+
|
219
|
+
# Force queryset update and apply sorting
|
220
|
+
cl.get_queryset(request)
|
221
|
+
cl.get_results(request)
|
222
|
+
|
223
|
+
# Add updated ChangeList to context
|
224
|
+
response.context_data["cl"] = cl
|
225
|
+
|
226
|
+
return response
|
227
|
+
|
228
|
+
def get_ordering(self, request):
|
229
|
+
"""Get Ordering."""
|
230
|
+
return None
|
231
|
+
|
232
|
+
# ------------------------------------------------------------------------
|
233
|
+
|
234
|
+
def _get_treenode_field_display(self, request, obj):
|
235
|
+
"""
|
236
|
+
Return the HTML display of the accordion node.
|
237
|
+
|
238
|
+
Modes:
|
239
|
+
- ACCORDION: ' ' * level + icon + str(node),
|
240
|
+
where icon = "📄" if obj.is_leaf() returns True, otherwise "📁".
|
241
|
+
- BREADCRUMBS: " / ".join(obj.get_breadcrumbs(attr=field)),
|
242
|
+
where field = getattr(self.model, 'treenode_display_field', None)
|
243
|
+
or "tn_priority" if None.
|
244
|
+
- INDENTATION: '—' * level + str(node)
|
245
|
+
"""
|
246
|
+
# Get a link to edit the object
|
247
|
+
meta = self.model._meta
|
248
|
+
edit_url = reverse(
|
249
|
+
f'admin:{meta.app_label}_{meta.model_name}_change', args=[obj.pk]
|
250
|
+
)
|
251
|
+
|
252
|
+
# Determine the node level
|
253
|
+
level = obj.get_depth()
|
254
|
+
|
255
|
+
mode = self.treenode_display_mode
|
256
|
+
if mode == self.TREENODE_DISPLAY_MODE_ACCORDION:
|
257
|
+
icon = "📄 " if obj.is_leaf() else "📁 "
|
258
|
+
obj_str = str(obj)
|
259
|
+
content = (
|
260
|
+
f'<span style="padding-left: {level * 1.5}em;">'
|
261
|
+
f'{icon}<a href="{edit_url}">{obj_str}</a>'
|
262
|
+
f'</span>'
|
263
|
+
)
|
264
|
+
elif mode == self.TREENODE_DISPLAY_MODE_BREADCRUMBS:
|
265
|
+
field = getattr(
|
266
|
+
self.model,
|
267
|
+
'treenode_display_field',
|
268
|
+
None) or "tn_priority"
|
269
|
+
content = " / ".join(obj.get_breadcrumbs(attr=field))
|
270
|
+
elif mode == self.TREENODE_DISPLAY_MODE_INDENTATION:
|
271
|
+
indent = "—" * level
|
272
|
+
obj_str = str(obj)
|
273
|
+
content = f'{indent}<a href="{edit_url}">{obj_str}</a>'
|
274
|
+
else:
|
275
|
+
# Just in case mode is not recognized, then use breadcrumbs
|
276
|
+
field = getattr(
|
277
|
+
self.model,
|
278
|
+
'treenode_display_field',
|
279
|
+
None) or "tn_priority"
|
280
|
+
content = " / ".join(obj.get_breadcrumbs(attr=field))
|
281
|
+
content = f'<a href="{edit_url}"">{content}</a>'
|
282
|
+
|
283
|
+
parent = str(getattr(obj, "tn_parent_id", "") or "")
|
284
|
+
html = (
|
285
|
+
f'<div class="treenode-wrapper" '
|
286
|
+
f'data-treenode-pk="{obj.pk}" '
|
287
|
+
f'data-treenode-depth="{level}" '
|
288
|
+
f'data-treenode-parent="{parent}">'
|
289
|
+
f'<span class="treenode-content">{content}</span>'
|
290
|
+
f'</div>'
|
291
|
+
)
|
292
|
+
return mark_safe(html)
|
293
|
+
|
294
|
+
|
295
|
+
# The End
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Sorted ChangeList Class for TreeNodeAdminModel.
|
4
|
+
|
5
|
+
Version: 2.1.0
|
6
|
+
Author: Timur Kady
|
7
|
+
Email: kaduevtr@gmail.com
|
8
|
+
"""
|
9
|
+
|
10
|
+
from django.contrib.admin.views.main import ChangeList
|
11
|
+
from django.core.serializers import serialize, deserialize
|
12
|
+
|
13
|
+
from ..cache import treenode_cache
|
14
|
+
|
15
|
+
|
16
|
+
class SortedChangeList(ChangeList):
|
17
|
+
"""Custom ChangeList that sorts results in Python (after DB query)."""
|
18
|
+
|
19
|
+
def get_ordering(self, request, queryset):
|
20
|
+
"""
|
21
|
+
Override ordering.
|
22
|
+
|
23
|
+
Overrides the sort order of objects in the list.
|
24
|
+
Django Admin sorts by `-pk` (descending) by default.
|
25
|
+
This method removes `-pk` so that objects are not sorted by ID.
|
26
|
+
"""
|
27
|
+
# Remove the default '-pk' ordering if present.
|
28
|
+
ordering = list(super().get_ordering(request, queryset))
|
29
|
+
if '-pk' in ordering:
|
30
|
+
ordering.remove('-pk')
|
31
|
+
return tuple(ordering)
|
32
|
+
|
33
|
+
def get_queryset(self, request):
|
34
|
+
"""Get QuerySet with select_related."""
|
35
|
+
return super().get_queryset(request).select_related('tn_parent')
|
36
|
+
|
37
|
+
def get_results(self, request):
|
38
|
+
"""Return sorted results for ChangeList rendering."""
|
39
|
+
# Populate self.result_list with objects from the DB.
|
40
|
+
super().get_results(request)
|
41
|
+
result_list = self.result_list
|
42
|
+
result_list_pks = ",".join(map(str, [obj.pk for obj in result_list]))
|
43
|
+
|
44
|
+
cache_key = treenode_cache.generate_cache_key(
|
45
|
+
self.model._meta.label,
|
46
|
+
self.get_results.__name__,
|
47
|
+
id(self.__class__),
|
48
|
+
result_list_pks
|
49
|
+
)
|
50
|
+
|
51
|
+
json_str = treenode_cache.get(cache_key)
|
52
|
+
if json_str:
|
53
|
+
sorted_results = [
|
54
|
+
obj.object for obj in deserialize("json", json_str)
|
55
|
+
]
|
56
|
+
self.result_list = sorted_results
|
57
|
+
return
|
58
|
+
|
59
|
+
sorted_result = self.model._sort_node_list(result_list)
|
60
|
+
json_str = serialize("json", sorted_result)
|
61
|
+
treenode_cache.set(cache_key, json_str)
|
62
|
+
|
63
|
+
self.result_list = sorted_result
|
64
|
+
|
65
|
+
# The End
|