django-fast-treenode 2.0.11__py3-none-any.whl → 2.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.dist-info}/LICENSE +2 -2
  2. django_fast_treenode-2.1.1.dist-info/METADATA +158 -0
  3. django_fast_treenode-2.1.1.dist-info/RECORD +64 -0
  4. {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.dist-info}/WHEEL +1 -1
  5. treenode/admin/__init__.py +9 -0
  6. treenode/admin/admin.py +295 -0
  7. treenode/admin/changelist.py +65 -0
  8. treenode/admin/mixins.py +302 -0
  9. treenode/apps.py +12 -1
  10. treenode/cache.py +2 -2
  11. treenode/forms.py +8 -10
  12. treenode/managers/__init__.py +21 -0
  13. treenode/managers/adjacency.py +203 -0
  14. treenode/managers/closure.py +278 -0
  15. treenode/models/__init__.py +2 -1
  16. treenode/models/adjacency.py +343 -0
  17. treenode/models/classproperty.py +3 -0
  18. treenode/models/closure.py +23 -24
  19. treenode/models/factory.py +12 -2
  20. treenode/models/mixins/__init__.py +23 -0
  21. treenode/models/mixins/ancestors.py +65 -0
  22. treenode/models/mixins/children.py +81 -0
  23. treenode/models/mixins/descendants.py +66 -0
  24. treenode/models/mixins/family.py +63 -0
  25. treenode/models/mixins/logical.py +68 -0
  26. treenode/models/mixins/node.py +210 -0
  27. treenode/models/mixins/properties.py +156 -0
  28. treenode/models/mixins/roots.py +96 -0
  29. treenode/models/mixins/siblings.py +99 -0
  30. treenode/models/mixins/tree.py +344 -0
  31. treenode/signals.py +26 -0
  32. treenode/static/treenode/css/tree_widget.css +201 -31
  33. treenode/static/treenode/css/treenode_admin.css +48 -41
  34. treenode/static/treenode/js/tree_widget.js +269 -131
  35. treenode/static/treenode/js/treenode_admin.js +131 -171
  36. treenode/templates/admin/tree_node_changelist.html +6 -0
  37. treenode/templates/admin/treenode_ajax_rows.html +7 -0
  38. treenode/tests/tests.py +488 -0
  39. treenode/urls.py +10 -6
  40. treenode/utils/__init__.py +2 -0
  41. treenode/utils/aid.py +46 -0
  42. treenode/utils/base16.py +38 -0
  43. treenode/utils/base36.py +3 -1
  44. treenode/utils/db.py +116 -0
  45. treenode/utils/exporter.py +2 -0
  46. treenode/utils/importer.py +0 -1
  47. treenode/utils/radix.py +61 -0
  48. treenode/version.py +2 -2
  49. treenode/views.py +118 -43
  50. treenode/widgets.py +91 -43
  51. django_fast_treenode-2.0.11.dist-info/METADATA +0 -698
  52. django_fast_treenode-2.0.11.dist-info/RECORD +0 -42
  53. treenode/admin.py +0 -439
  54. treenode/docs/Documentation +0 -636
  55. treenode/managers.py +0 -419
  56. treenode/models/proxy.py +0 -669
  57. {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2020-2023 Timur Kady
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,158 @@
1
+ Metadata-Version: 2.2
2
+ Name: django-fast-treenode
3
+ Version: 2.1.1
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://django-fast-treenode.readthedocs.io/latest/
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
+ # Django-fast-treenode
64
+ **Combining Adjacency List and Closure Table for Optimal Performance**
65
+
66
+
67
+ **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.
68
+
69
+ ## Features
70
+ - **Hybrid storage model**: Combines Adjacency List and Closure Table for optimal performance.
71
+ - **Custom caching system**: A built-in caching mechanism, specifically designed for this package, significantly boosts execution speed.
72
+ - **Efficient queries**: Retrieve ancestors, descendants, breadcrumbs, and tree depth with only one SQL queriy.
73
+ - **Bulk operations**: Supports fast insertion, movement, and deletion of nodes.
74
+ - **Flexibility**: Fully integrates with Django ORM and adapts to various business logic needs.
75
+ - **Admin panel integration**: Full compatibility with Django's admin panel, allowing intuitive management of tree structures.
76
+ - **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.
77
+
78
+ ## Use Cases
79
+ Django Fast TreeNode is suitable for a wide range of applications, from simple directories to complex systems with deep hierarchical structures:
80
+ - **Categories and taxonomies**: Manage product categories, tags, and classification systems.
81
+ - **Menus and navigation**: Create tree-like menus and nested navigation structures.
82
+ - **Forums and comments**: Store threaded discussions and nested comment chains.
83
+ - **Geographical data**: Represent administrative divisions, regions, and areas of influence.
84
+ - **Organizational and Business Structures**: Model company hierarchies, business processes, employees and departments.
85
+
86
+ In all applications, `django-fast-treenode` models show excellent performance and stability.
87
+
88
+ ## Quick start
89
+ 1. Run `pip install django-fast-treenode`.
90
+ 2. Add `treenode` to `settings.INSTALLED_APPS`.
91
+
92
+ ```python
93
+ INSTALLED_APPS = [
94
+ ...
95
+ 'treenode',
96
+ ]
97
+ ```
98
+
99
+ 3. Define your model inherit from `treenode.models.TreeNodeModel`.
100
+
101
+ ```python
102
+ from treenode.models import TreeNodeModel
103
+
104
+ class Category(TreeNodeModel):
105
+ name = models.CharField(max_length=255)
106
+ treenode_display_field = "name"
107
+ ```
108
+
109
+ 4. Make your model-admin inherit from `treenode.admin.TreeNodeModelAdmin`.
110
+
111
+ ```python
112
+ from treenode.admin import TreeNodeModelAdmin
113
+ from .models import Category
114
+
115
+ @admin.register(Category)
116
+ class CategoryAdmin(TreeNodeModelAdmin):
117
+ list_display = ("name",)
118
+ search_fields = ("name",)
119
+ ```
120
+ 5. Run migrations.
121
+
122
+ ```bash
123
+ python manage.py makemigrations
124
+ python manage.py migrate
125
+ ```
126
+
127
+ 6. Run server and use!
128
+
129
+ ```bash
130
+ >>> root = Category.objects.create(name="Root")
131
+ >>> child = Category.objects.create(name="Child")
132
+ >>> child.set_parent(root)
133
+ >>> root_descendants_list = root.get_descendants()
134
+ >>> root_children_queryset = root.get_children_queryset()
135
+ >>> ancestors_pks = child.get_ancestors_pks()
136
+ ```
137
+
138
+ ## Documentation
139
+ Full documentation is available at [this link](https://django-fast-treenode.readthedocs.io/latest/).
140
+
141
+ Quick access links:
142
+ * [Installation, configuration and fine tuning](https://django-fast-treenode.readthedocs.io/latest/installation/)
143
+ * [Model Inheritance and Extensions](https://django-fast-treenode.readthedocs.io/latest/models/)
144
+ * [Working with Admin Classes](https://django-fast-treenode.readthedocs.io/latest/admin/)
145
+ * [API Reference](https://django-fast-treenode.readthedocs.io/latest/api/)
146
+ * [Import & Export](https://django-fast-treenode.readthedocs.io/latest/import_export/)
147
+ * [Caching and working with cache](https://django-fast-treenode.readthedocs.io/latest/cache/)
148
+ * [Migration and upgrade guide](https://django-fast-treenode.readthedocs.io/latest/migration/)
149
+
150
+ Your wishes, objections, comments are welcome.
151
+
152
+ ## License
153
+ Released under [MIT License](https://github.com/TimurKady/django-fast-treenode/blob/main/LICENSE).
154
+
155
+ ## Credits
156
+ Thanks to everyone who contributed to the development and testing of this package, as well as the Django community for their inspiration and support.
157
+
158
+ 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,64 @@
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/managers/__init__.py,sha256=EG_tj9P1Hama3kaqMfHck4lfzUWoPaJJVOXe3qaKMUo,585
15
+ treenode/managers/adjacency.py,sha256=NVN8dq5z7gIh90yqW2uxV7MokmUfXTOT7crqMDyMaH0,7889
16
+ treenode/managers/closure.py,sha256=PcScdJJUnLcKe8Y1wqROYPsRtAnBMUO4xn5sILk9AIM,10638
17
+ treenode/models/__init__.py,sha256=pBiMlEpC_Thh7asraNzA7W_7BKu2oAHtcn-K6_sdJe8,112
18
+ treenode/models/adjacency.py,sha256=ijStfIQDSd48L3nA8OnLD1nHGYo5YsnokqUVfzDt68w,12422
19
+ treenode/models/classproperty.py,sha256=J4W6snsfsEUSHKHkIlM9yOJYQ_FSrp3P3oEYMKJengg,571
20
+ treenode/models/closure.py,sha256=NEC8pi9QIxNtUnctEi0lPHBHEPX2K3V1oeSs9JrDGA0,4588
21
+ treenode/models/factory.py,sha256=10FEGGC5PGWaR58qErs0oOrCS0KeI8x9H-SknZAAWqw,2291
22
+ treenode/models/mixins/__init__.py,sha256=gTdMZFh1slNHMvxrnu-hGl46xqnWd4W7TOEFWTVJq40,757
23
+ treenode/models/mixins/ancestors.py,sha256=_nF8V99SBjT-G88BmcRtiTC0DgSEIgHrI0g02jqTnss,2075
24
+ treenode/models/mixins/children.py,sha256=OchaH6m6pOr6uuiZRRBHoZXoCSWM-ENTNWu1iLHXaBM,2564
25
+ treenode/models/mixins/descendants.py,sha256=2mhnIhC8VJomTqntzzAwFFW_CcMiwujzQoD5_mfMsK0,2208
26
+ treenode/models/mixins/family.py,sha256=h2IRRADkQxve97QqBHKv0evVz4cFQtcNR8CbPi9Ri_w,1645
27
+ treenode/models/mixins/logical.py,sha256=jlhBSq3AfCYNyNjqyKM9siyioS3SYcGD-aG2b4MV2RM,2169
28
+ treenode/models/mixins/node.py,sha256=E-eUgZoqKlyaCaDk0H7VQg6tDnfeufyGHn7hjTxIDqM,7694
29
+ treenode/models/mixins/properties.py,sha256=pfv80KLXcPeGx00IFCBcst1_cf0AmzhjshFjq1XQWMY,3876
30
+ treenode/models/mixins/roots.py,sha256=MoFQq1fph70awc26UMUbfeTpt0ToUOvMz1c7LlDyIP8,2956
31
+ treenode/models/mixins/siblings.py,sha256=fh0ZrlFXKxOQ4Qrp6sElTMvRhU5PyRRykLHDcbH-3Rk,3113
32
+ treenode/models/mixins/tree.py,sha256=CsO0ynwcwkrWgQbTzvF4yws-y7n1GGM2zImJH0hgV00,13042
33
+ treenode/static/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
+ treenode/static/treenode/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
35
+ treenode/static/treenode/css/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
36
+ treenode/static/treenode/css/tree_widget.css,sha256=SE74hZaOECHe1VKe-N6b-MxcZ6tQrA9d4ctfNHrVvvA,4864
37
+ treenode/static/treenode/css/treenode_admin.css,sha256=7Ye_bCgIgG-QUcih1jXIda1XxhAkTFLU-0CHcKNCZtw,2238
38
+ treenode/static/treenode/js/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
39
+ treenode/static/treenode/js/tree_widget.js,sha256=SGxm2Awu1Ysk53h1r8JIS5wo9XGQFUD0cz9WqsMQXMs,10331
40
+ treenode/static/treenode/js/treenode_admin.js,sha256=q_OvlDQPvmv9SfV2u69PztVY2Yrdl4qdlnoAzbbkovA,4747
41
+ treenode/templates/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
42
+ treenode/templates/admin/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
43
+ treenode/templates/admin/export_success.html,sha256=xN2D-BCH249CJB10fo_vHYUyFenQ9mFKqq7UTWcrXS4,747
44
+ treenode/templates/admin/tree_node_changelist.html,sha256=fGRVvWx2EnpiFYeJckyPKV-BCv9I13_ViiNN0LIUZKM,380
45
+ treenode/templates/admin/tree_node_export.html,sha256=vJxEoGI-US6VdFddxAFgL5r3MgGt6mgA43vltCsbA2k,1043
46
+ treenode/templates/admin/tree_node_import.html,sha256=unksxTAO2bJbxRkZfrCltHn61MgfqGt2sxIsUOW5dVk,1513
47
+ treenode/templates/admin/tree_node_import_report.html,sha256=azHJ8JFrSRu60lF1Uh22zs9JXQxZdvOjYdwCtlbaE3I,1133
48
+ treenode/templates/admin/treenode_ajax_rows.html,sha256=zFyPaTbSyxRjOqQ85SMv__qTIYDjEna6chYODBypDZA,224
49
+ treenode/templates/widgets/tree_widget.css,sha256=2bEaxu1x7QJZ7erbs2SLMaxeaiMkjQXadfcDEW8wfok,551
50
+ treenode/templates/widgets/tree_widget.html,sha256=GKcCU-B2FkkJ2BSOuXOw9e_PdYTtADcvyITEXqOlZ9Y,723
51
+ treenode/tests/tests.py,sha256=9Bd2BhvwtVhYBp5DEtkzKPpAP1iFo4asMsydzuIRASM,19316
52
+ treenode/utils/__init__.py,sha256=B4bv96ivtHELPv0_DllJa5z-k1QMo7z-MKuvj-3NdtI,356
53
+ treenode/utils/aid.py,sha256=o8Jgc1vDRtQpx4XYdv0qR5Lqvens55Jfbdca1nr-EOA,1013
54
+ treenode/utils/base16.py,sha256=U1PMit2aZOpYusG_u1c7eVpXO-cFrFPyVyk9zdHrehg,817
55
+ treenode/utils/base36.py,sha256=yICmyPE-yyPNO9T2oALOt-b6uYf37ahFfx0R4tXn3X0,847
56
+ treenode/utils/db.py,sha256=36q4OckKmEd6uHTbMTxdKpV9nOIZ55DAantRWR9bxWg,4297
57
+ treenode/utils/exporter.py,sha256=mV6Gch7XfW8f_1x3WqWgtV0qekMLdo-_n9gz6GJjXjw,7259
58
+ treenode/utils/importer.py,sha256=Hvirbd6NyZ2MHa56_jOrUF3kYFeby1DbSLR3mhHy-9s,12891
59
+ treenode/utils/radix.py,sha256=zHpOuDxsebiv9Gza6snNhAtBKiex6CDrAVRtB6esaWo,1642
60
+ django_fast_treenode-2.1.1.dist-info/LICENSE,sha256=T0evsb7y-63fg18ovdNSx3wwWWRwyluQvN9J4zFSvfE,1093
61
+ django_fast_treenode-2.1.1.dist-info/METADATA,sha256=E1ThWhY4yy_h8_28imc4CYkd4kR7e9aCvtvpa6Pyncs,7620
62
+ django_fast_treenode-2.1.1.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
63
+ django_fast_treenode-2.1.1.dist-info/top_level.txt,sha256=fmgxHbXyx1O2MPi_9kjx8aL9L-8TmV0gre4Go8XgqFk,9
64
+ django_fast_treenode-2.1.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (75.8.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1,9 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from .admin import TreeNodeAdminModel
4
+
5
+
6
+ __all__ = ["TreeNodeAdminModel"]
7
+
8
+
9
+ # The end
@@ -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 = "↕" # &nbsp;"
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">&nbsp;</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: '&nbsp;' * 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: '&mdash;' * 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 = "&mdash;" * 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