django-fast-treenode 2.0.11__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.
Files changed (68) hide show
  1. {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.0.dist-info}/LICENSE +2 -2
  2. django_fast_treenode-2.1.0.dist-info/METADATA +161 -0
  3. django_fast_treenode-2.1.0.dist-info/RECORD +75 -0
  4. {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.0.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/docs/.gitignore +0 -0
  12. treenode/docs/about.md +36 -0
  13. treenode/docs/admin.md +104 -0
  14. treenode/docs/api.md +739 -0
  15. treenode/docs/cache.md +187 -0
  16. treenode/docs/import_export.md +35 -0
  17. treenode/docs/index.md +30 -0
  18. treenode/docs/installation.md +74 -0
  19. treenode/docs/migration.md +145 -0
  20. treenode/docs/models.md +128 -0
  21. treenode/docs/roadmap.md +45 -0
  22. treenode/forms.py +8 -10
  23. treenode/managers/__init__.py +21 -0
  24. treenode/managers/adjacency.py +203 -0
  25. treenode/managers/closure.py +278 -0
  26. treenode/models/__init__.py +2 -1
  27. treenode/models/adjacency.py +343 -0
  28. treenode/models/classproperty.py +3 -0
  29. treenode/models/closure.py +23 -24
  30. treenode/models/factory.py +12 -2
  31. treenode/models/mixins/__init__.py +23 -0
  32. treenode/models/mixins/ancestors.py +65 -0
  33. treenode/models/mixins/children.py +81 -0
  34. treenode/models/mixins/descendants.py +66 -0
  35. treenode/models/mixins/family.py +63 -0
  36. treenode/models/mixins/logical.py +68 -0
  37. treenode/models/mixins/node.py +210 -0
  38. treenode/models/mixins/properties.py +156 -0
  39. treenode/models/mixins/roots.py +96 -0
  40. treenode/models/mixins/siblings.py +99 -0
  41. treenode/models/mixins/tree.py +344 -0
  42. treenode/signals.py +26 -0
  43. treenode/static/treenode/css/tree_widget.css +201 -31
  44. treenode/static/treenode/css/treenode_admin.css +48 -41
  45. treenode/static/treenode/js/tree_widget.js +269 -131
  46. treenode/static/treenode/js/treenode_admin.js +131 -171
  47. treenode/templates/admin/tree_node_changelist.html +6 -0
  48. treenode/templates/admin/treenode_ajax_rows.html +7 -0
  49. treenode/tests/tests.py +488 -0
  50. treenode/urls.py +10 -6
  51. treenode/utils/__init__.py +2 -0
  52. treenode/utils/aid.py +46 -0
  53. treenode/utils/base16.py +38 -0
  54. treenode/utils/base36.py +3 -1
  55. treenode/utils/db.py +116 -0
  56. treenode/utils/exporter.py +2 -0
  57. treenode/utils/importer.py +0 -1
  58. treenode/utils/radix.py +61 -0
  59. treenode/version.py +2 -2
  60. treenode/views.py +118 -43
  61. treenode/widgets.py +91 -43
  62. django_fast_treenode-2.0.11.dist-info/METADATA +0 -698
  63. django_fast_treenode-2.0.11.dist-info/RECORD +0 -42
  64. treenode/admin.py +0 -439
  65. treenode/docs/Documentation +0 -636
  66. treenode/managers.py +0 -419
  67. treenode/models/proxy.py +0 -669
  68. {django_fast_treenode-2.0.11.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-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,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,,
@@ -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