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