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.
Files changed (70) hide show
  1. {django_fast_treenode-2.0.10.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.10.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 +33 -22
  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 +39 -65
  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/tree_node_import.html +27 -9
  49. treenode/templates/admin/tree_node_import_report.html +32 -0
  50. treenode/templates/admin/treenode_ajax_rows.html +7 -0
  51. treenode/tests/tests.py +488 -0
  52. treenode/urls.py +10 -6
  53. treenode/utils/__init__.py +2 -0
  54. treenode/utils/aid.py +46 -0
  55. treenode/utils/base16.py +38 -0
  56. treenode/utils/base36.py +3 -1
  57. treenode/utils/db.py +116 -0
  58. treenode/utils/exporter.py +63 -36
  59. treenode/utils/importer.py +168 -161
  60. treenode/utils/radix.py +61 -0
  61. treenode/version.py +2 -2
  62. treenode/views.py +119 -38
  63. treenode/widgets.py +104 -40
  64. django_fast_treenode-2.0.10.dist-info/METADATA +0 -698
  65. django_fast_treenode-2.0.10.dist-info/RECORD +0 -41
  66. treenode/admin.py +0 -396
  67. treenode/docs/Documentation +0 -664
  68. treenode/managers.py +0 -281
  69. treenode/models/proxy.py +0 -650
  70. {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/top_level.txt +0 -0
@@ -1,41 +0,0 @@
1
- treenode/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- treenode/admin.py,sha256=VkGuqc8KI1_SPgvM6yynyfYLEmZRfgMm2M5d9OoDxZE,14720
3
- treenode/apps.py,sha256=M0O9IKEnJZFfhfz12v4wksYJ-0ECyj1Cy3qXrfywos8,472
4
- treenode/cache.py,sha256=Z_FpaS0vTKXqAI4n1QkZ7A_ILsLU3Q8rLgerA6pYyAA,7210
5
- treenode/forms.py,sha256=KJOVqIhMt8Z3J38LTl1exO2JyS46mZKzkQJZI-Uem9Q,3573
6
- treenode/managers.py,sha256=7z8GU64A2_jEonJyQDTyIpdOocaBbM352DkwZTHjdQk,10828
7
- treenode/urls.py,sha256=7N0d4XiI6880sc8P89eWGr-ZjmOqPorA-fWfcnviqAM,876
8
- treenode/version.py,sha256=-zaHoXRvTvJ0QzwA9ocYp7O38iBtIarACZbCNzwyc4s,222
9
- treenode/views.py,sha256=vBDIxmYXBwmJxRnInvUVGy75FQmt-XN_HnEaKIu-fVs,3365
10
- treenode/widgets.py,sha256=4Q6WlPPT5fggEuTXiZ_Z40pjb46CylSp28pa0xBT_Ps,2079
11
- treenode/docs/Documentation,sha256=6USAESU8MuY1vlj95yY8S6T0o_0RsktGv6S_0SdSkwk,21030
12
- treenode/models/__init__.py,sha256=gjDwVai0jf-l0hMaeeEBTYLR-DXkxUZMLUMGGs_tnuo,83
13
- treenode/models/classproperty.py,sha256=IrwBWpmyjsAXpkpfDSOIMsnX6EMcbXql3mZjurHgRcw,556
14
- treenode/models/closure.py,sha256=N026nzqY62FfP7DsCH-I4j_HLGbbsrJQrQ_-rAUUboA,5361
15
- treenode/models/factory.py,sha256=Wt1szWhbeICPwm0-RUy9p4VovcxltHECVxTSRyCQHc8,2100
16
- treenode/models/proxy.py,sha256=6BFElk_NL1ARTEAikOOfMneUK5wEjofNnfXQWFSZUsA,21766
17
- treenode/static/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
- treenode/static/treenode/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
19
- treenode/static/treenode/css/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
20
- treenode/static/treenode/css/tree_widget.css,sha256=Qsnr9cExetL7BFKErRSns4APzM8-9DM4g6nqMelPzUI,1972
21
- treenode/static/treenode/css/treenode_admin.css,sha256=GiCJ_zNZs7JmJgjCHsn1tJinNEU_lTOYYPZ7S0fnvis,2195
22
- treenode/static/treenode/js/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
23
- treenode/static/treenode/js/tree_widget.js,sha256=Du3a76tojdIqWndji88Omt5MvajXK7GpypC9DYIFBMk,6571
24
- treenode/static/treenode/js/treenode_admin.js,sha256=3fdvy1VoHb3rmzI19YXw4JPt6ZGKn_AhTEky8YQEilU,6821
25
- treenode/templates/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
26
- treenode/templates/admin/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
27
- treenode/templates/admin/export_success.html,sha256=xN2D-BCH249CJB10fo_vHYUyFenQ9mFKqq7UTWcrXS4,747
28
- treenode/templates/admin/tree_node_changelist.html,sha256=NudAsaO6di_cDWQDewBe-1Bay61FdlGiEFzdvfP_Wk8,314
29
- treenode/templates/admin/tree_node_export.html,sha256=vJxEoGI-US6VdFddxAFgL5r3MgGt6mgA43vltCsbA2k,1043
30
- treenode/templates/admin/tree_node_import.html,sha256=ZFoveJ08X99EGiwFCfQowXI9oS9VgcFtRLYVDIWq-Fg,969
31
- treenode/templates/widgets/tree_widget.css,sha256=2bEaxu1x7QJZ7erbs2SLMaxeaiMkjQXadfcDEW8wfok,551
32
- treenode/templates/widgets/tree_widget.html,sha256=GKcCU-B2FkkJ2BSOuXOw9e_PdYTtADcvyITEXqOlZ9Y,723
33
- treenode/utils/__init__.py,sha256=_eKk3iiiyyk4GB5dupwJxl3RPWDEHZ1DW5vHteDrbVI,343
34
- treenode/utils/base36.py,sha256=ydgu9hqDaK-WyS8zG-mtSWo7hJqbB4iHqkGz4-IVrb4,834
35
- treenode/utils/exporter.py,sha256=hCTPnSdFdoE_s7s_iW1Xy6598fWQY5htJzPA0DpnG5s,6199
36
- treenode/utils/importer.py,sha256=RYtl0CHfH1tZjCbhrFf0-Qp-wm4QJp-DD5Y5nyLK7yQ,12643
37
- django_fast_treenode-2.0.10.dist-info/LICENSE,sha256=GiiEe4Y9oOCbn9eGuNew1mMYHU_bJWaCK9zOusnKvvU,1091
38
- django_fast_treenode-2.0.10.dist-info/METADATA,sha256=kQgDgrx9hH6bdz0wwbUZ4VxUQIxZLxFd3CPbvRmmhi0,23274
39
- django_fast_treenode-2.0.10.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
40
- django_fast_treenode-2.0.10.dist-info/top_level.txt,sha256=fmgxHbXyx1O2MPi_9kjx8aL9L-8TmV0gre4Go8XgqFk,9
41
- django_fast_treenode-2.0.10.dist-info/RECORD,,
treenode/admin.py DELETED
@@ -1,396 +0,0 @@
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.0.10
10
- Author: Timur Kady
11
- Email: kaduevtr@gmail.com
12
- """
13
-
14
-
15
- import os
16
- import importlib
17
- import numpy as np
18
- from datetime import datetime
19
- from django.contrib import admin
20
- from django.contrib.admin.views.main import ChangeList
21
- from django.db import models
22
- from django.shortcuts import render, redirect
23
- from django.urls import path
24
- from django.utils.encoding import force_str
25
- from django.utils.safestring import mark_safe
26
- from django.utils.translation import gettext_lazy as _
27
-
28
- from .forms import TreeNodeForm
29
- from .widgets import TreeWidget
30
-
31
- import logging
32
-
33
- logger = logging.getLogger(__name__)
34
-
35
-
36
- class SortedChangeList(ChangeList):
37
- """Custom ChangeList that sorts results in Python (after DB query)."""
38
-
39
- def get_ordering(self, request, queryset):
40
- """
41
- Override ordering.
42
-
43
- Overrides the sort order of objects in the list.
44
- Django Admin sorts by `-pk` (descending) by default.
45
- This method removes `-pk` so that objects are not sorted by ID.
46
- """
47
- # Remove the default '-pk' ordering if present.
48
- ordering = list(super().get_ordering(request, queryset))
49
- if '-pk' in ordering:
50
- ordering.remove('-pk')
51
- return tuple(ordering)
52
-
53
- def get_queryset(self, request):
54
- """Get QuerySet with select_related."""
55
- return super().get_queryset(request).select_related('tn_parent')
56
-
57
- def get_results(self, request):
58
- """Return sorted results for ChangeList rendering."""
59
- # Populate self.result_list with objects from the DB.
60
- super().get_results(request)
61
- result_list = self.result_list
62
- # Extract tn_order values from each object.
63
- tn_orders = np.array([obj.tn_order for obj in result_list])
64
- # Get sorted indices based on tn_order (ascending order).
65
- # Reorder the original result_list based on the sorted indices.
66
- self.result_list = [result_list[int(i)] for i in np.argsort(tn_orders)]
67
-
68
-
69
- class TreeNodeAdminModel(admin.ModelAdmin):
70
- """
71
- TreeNodeAdmin class.
72
-
73
- Admin configuration for TreeNodeModel with import/export functionality.
74
- """
75
-
76
- TREENODE_DISPLAY_MODE_ACCORDION = 'accordion'
77
- TREENODE_DISPLAY_MODE_BREADCRUMBS = 'breadcrumbs'
78
- TREENODE_DISPLAY_MODE_INDENTATION = 'indentation'
79
-
80
- treenode_display_mode = TREENODE_DISPLAY_MODE_ACCORDION
81
- import_export = False # Track import/export availability
82
- change_list_template = "admin/tree_node_changelist.html"
83
- ordering = []
84
- list_per_page = 1000
85
- list_sorting_mode_session_key = "treenode_sorting_mode"
86
-
87
- form = TreeNodeForm
88
- formfield_overrides = {
89
- models.ForeignKey: {"widget": TreeWidget()},
90
- }
91
-
92
- class Media:
93
- """Include custom CSS and JavaScript for admin interface."""
94
-
95
- css = {"all": (
96
- "treenode/css/treenode_admin.css",
97
- )}
98
- js = (
99
- 'admin/js/jquery.init.js',
100
- 'treenode/js/treenode_admin.js',
101
- )
102
-
103
- def __init__(self, model, admin_site):
104
- """Динамически добавляем поле `tn_order` в `list_display`."""
105
- super().__init__(model, admin_site)
106
-
107
- # If `list_display` is empty, take all `fields`
108
- if not self.list_display:
109
- self.list_display = [field.name for field in model._meta.fields]
110
-
111
- # Check for necessary dependencies
112
- self.import_export = all([
113
- importlib.util.find_spec(pkg) is not None
114
- for pkg in ["openpyxl", "yaml", "xlsxwriter"]
115
- ])
116
- if not self.import_export:
117
- check_results = [
118
- pkg for pkg in ["openpyxl", "pyyaml", "xlsxwriter"] if importlib.util.find_spec(pkg) is not None
119
- ]
120
- logger.info("Packages" + ", ".join(check_results) + " are \
121
- not installed. Export and import functions are disabled.")
122
-
123
- if self.import_export:
124
- from .utils import TreeNodeImporter, TreeNodeExporter
125
-
126
- self.TreeNodeImporter = TreeNodeImporter
127
- self.TreeNodeExporter = TreeNodeExporter
128
- else:
129
- self.TreeNodeImporter = None
130
- self.TreeNodeExporter = None
131
-
132
- def get_queryset(self, request):
133
- """Override get_queryset to simply return an optimized queryset."""
134
- queryset = super().get_queryset(request)
135
- # If a search term is present, leave the queryset as is.
136
- if request.GET.get("q"):
137
- return queryset
138
- return queryset.select_related('tn_parent')
139
-
140
- def get_search_fields(self, request):
141
- """Return the correct search field."""
142
- return [self.model.treenode_display_field]
143
-
144
- def get_list_display(self, request):
145
- """
146
- Generate list_display dynamically.
147
-
148
- Return list or tuple of field names that will be displayed in the
149
- change list view.
150
- """
151
- base_list_display = super().get_list_display(request)
152
- base_list_display = list(base_list_display)
153
-
154
- def treenode_field_display(obj):
155
- return self._get_treenode_field_display(request, obj)
156
-
157
- verbose_name = self.model._meta.verbose_name
158
- treenode_field_display.short_description = verbose_name
159
- treenode_field_display.allow_tags = True
160
-
161
- if len(base_list_display) == 1 and base_list_display[0] == '__str__':
162
- return (treenode_field_display,)
163
- else:
164
- treenode_display_field = getattr(
165
- self.model,
166
- 'treenode_display_field',
167
- '__str__'
168
- )
169
- if base_list_display[0] == treenode_display_field:
170
- base_list_display.pop(0)
171
- return (treenode_field_display,) + tuple(base_list_display)
172
-
173
- def get_changelist(self, request):
174
- """Use SortedChangeList to sort the results at render time."""
175
- return SortedChangeList
176
-
177
- def changelist_view(self, request, extra_context=None):
178
- """Changelist View."""
179
- extra_context = extra_context or {}
180
- extra_context['import_export_enabled'] = self.import_export
181
- return super().changelist_view(request, extra_context=extra_context)
182
-
183
- def get_ordering(self, request):
184
- """Get Ordering."""
185
- return None
186
-
187
- def _get_row_display(self, obj):
188
- """Return row display for accordion mode."""
189
- field = getattr(self.model, 'treenode_display_field')
190
- return force_str(getattr(obj, field, obj.pk))
191
-
192
- def _get_treenode_field_display(self, request, obj):
193
- """Define how to display nodes depending on the mode."""
194
- display_mode = self.treenode_display_mode
195
- if display_mode == self.TREENODE_DISPLAY_MODE_ACCORDION:
196
- return self._display_with_accordion(obj)
197
- elif display_mode == self.TREENODE_DISPLAY_MODE_BREADCRUMBS:
198
- return self._display_with_breadcrumbs(obj)
199
- elif display_mode == self.TREENODE_DISPLAY_MODE_INDENTATION:
200
- return self._display_with_indentation(obj)
201
- else:
202
- return self._display_with_breadcrumbs(obj)
203
-
204
- def _display_with_accordion(self, obj):
205
- """Display a tree in accordion style."""
206
- parent = str(obj.tn_parent_id or '')
207
- text = self._get_row_display(obj)
208
- html = (
209
- f'<div class="treenode-wrapper" '
210
- f'data-treenode-pk="{obj.pk}" '
211
- f'data-treenode-depth="{obj.depth}" '
212
- f'data-treenode-parent="{parent}">'
213
- f'<span class="treenode-content">{text}</span>'
214
- f'</div>'
215
- )
216
- return mark_safe(html)
217
-
218
- def _display_with_breadcrumbs(self, obj):
219
- """Display a tree as breadcrumbs."""
220
- field = getattr(self.model, 'treenode_display_field')
221
- if field is not None:
222
- obj_display = " / ".join(obj.get_breadcrumbs(attr=field))
223
- else:
224
- obj_display = obj.get_path(
225
- prefix=_("Node "),
226
- suffix=" / " + obj.__str__()
227
- )
228
- display = f'<span class="treenode-breadcrumbs">{obj_display}</span>'
229
- return mark_safe(display)
230
-
231
- def _display_with_indentation(self, obj):
232
- """Display tree with indents."""
233
- indent = '&mdash;' * obj.get_depth()
234
- display = f'<span class="treenode-indentation">{indent}</span> {obj}'
235
- return mark_safe(display)
236
-
237
- def get_form(self, request, obj=None, **kwargs):
238
- """Get Form method."""
239
- form = super().get_form(request, obj, **kwargs)
240
- if "tn_parent" in form.base_fields:
241
- form.base_fields["tn_parent"].widget = TreeWidget()
242
- return form
243
-
244
- def get_urls(self):
245
- """
246
- Extend admin URLs with custom import/export routes.
247
-
248
- Register these URLs only if all the required packages are installed.
249
- """
250
- urls = super().get_urls()
251
- if self.import_export:
252
- custom_urls = [
253
- path('import/', self.import_view, name='tree_node_import'),
254
- path('export/', self.export_view, name='tree_node_export'),
255
- ]
256
- else:
257
- custom_urls = []
258
- return custom_urls + urls
259
-
260
- def import_view(self, request):
261
- """
262
- Import View.
263
-
264
- File upload processing, auto-detection of format, validation and data
265
- import.
266
- """
267
- if not self.import_export:
268
- self.message_user(
269
- request,
270
- "Import functionality is disabled because required \
271
- packages are not installed."
272
- )
273
- return redirect("..")
274
-
275
- if request.method == 'POST':
276
- if 'file' not in request.FILES:
277
- return render(
278
- request,
279
- "admin/tree_node_import.html",
280
- {"errors": ["No file uploaded."]}
281
- )
282
-
283
- file = request.FILES['file']
284
- ext = os.path.splitext(file.name)[-1].lower().strip(".")
285
-
286
- allowed_formats = {"csv", "json", "xlsx", "yaml", "tsv"}
287
- if ext not in allowed_formats:
288
- return render(
289
- request,
290
- "admin/tree_node_import.html",
291
- {"errors": [f"Unsupported file format: {ext}"]}
292
- )
293
-
294
- # Import data from file
295
- importer = self.TreeNodeImporter(self.model, file, ext)
296
- raw_data = importer.import_data()
297
- clean_result = importer.clean(raw_data)
298
- errors = importer.finalize_import(clean_result)
299
- if errors:
300
- return render(
301
- request,
302
- "admin/tree_node_import.html",
303
- {"errors": errors}
304
- )
305
- self.message_user(
306
- request,
307
- f"Successfully imported {len(clean_result['create'])} records."
308
- )
309
- return redirect("..")
310
-
311
- # If the request is not POST, simply display the import form
312
- return render(request, "admin/tree_node_import.html")
313
-
314
- def export_view(self, request):
315
- """
316
- Export view.
317
-
318
- - If the GET parameters include download, we send the file directly.
319
- - If the format parameter is missing, we render the format selection
320
- page.
321
- - If the format is specified, we perform a test export to catch errors.
322
-
323
- If there are no errors, we render the success page with a message, a
324
- link for manual download,
325
- and a button to go to the model page.
326
- """
327
- if not self.import_export:
328
- self.message_user(
329
- request,
330
- "Export functionality is disabled because required \
331
- packages are not installed."
332
- )
333
- return redirect("..")
334
-
335
- # If the download parameter is present, we give the file directly
336
- if 'download' in request.GET:
337
- # Get file format
338
- export_format = request.GET.get('format', 'csv')
339
- # Filename
340
- now = force_str(datetime.now().strftime("%Y-%m-%d %H-%M"))
341
- filename = self.model._meta.label + " " + now
342
- # Init
343
- exporter = self.TreeNodeExporter(
344
- self.get_queryset(),
345
- filename=filename
346
- )
347
- # Export working
348
- response = exporter.export(export_format)
349
- logger.debug("DEBUG: File response generated.")
350
- return response
351
-
352
- # If the format parameter is not passed, we show the format
353
- # selection page
354
- if 'format' not in request.GET:
355
- return render(request, "admin/tree_node_export.html")
356
-
357
- # If the format is specified, we try to perform a test export
358
- # (without returning the file)
359
- export_format = request.GET['format']
360
- exporter = self.TreeNodeExporter(
361
- self.model.objects.all(),
362
- filename=self.model._meta.model_name
363
- )
364
- try:
365
- # Test call to check for export errors (result not used)
366
- exporter.export(export_format)
367
- except Exception as e:
368
- logger.error("Error during test export: %s", e)
369
- errors = [str(e)]
370
- return render(
371
- request,
372
- "admin/tree_node_export.html",
373
- {"errors": errors}
374
- )
375
-
376
- # Form the correct download URL. If the URL already contains
377
- # parameters, add them via &download=1, otherwise via ?download=1
378
- current_url = request.build_absolute_uri()
379
- if "?" in current_url:
380
- download_url = current_url + "&download=1"
381
- else:
382
- download_url = current_url + "?download=1"
383
-
384
- context = {
385
- "download_url": download_url,
386
- "message": "Your file is ready for export. \
387
- The download should start automatically.",
388
- "manual_download_label": "If the download does not start, \
389
- click this link.",
390
- # Can be replaced with the desired URL to return to the model
391
- "redirect_url": "../",
392
- "button_text": "Return to model"
393
- }
394
- return render(request, "admin/export_success.html", context)
395
-
396
- # The End