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.
- {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.0.dist-info}/LICENSE +2 -2
- django_fast_treenode-2.1.0.dist-info/METADATA +161 -0
- django_fast_treenode-2.1.0.dist-info/RECORD +75 -0
- {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.0.dist-info}/WHEEL +1 -1
- treenode/admin/__init__.py +9 -0
- treenode/admin/admin.py +295 -0
- treenode/admin/changelist.py +65 -0
- treenode/admin/mixins.py +302 -0
- treenode/apps.py +12 -1
- treenode/cache.py +2 -2
- treenode/docs/.gitignore +0 -0
- treenode/docs/about.md +36 -0
- treenode/docs/admin.md +104 -0
- treenode/docs/api.md +739 -0
- treenode/docs/cache.md +187 -0
- treenode/docs/import_export.md +35 -0
- treenode/docs/index.md +30 -0
- treenode/docs/installation.md +74 -0
- treenode/docs/migration.md +145 -0
- treenode/docs/models.md +128 -0
- treenode/docs/roadmap.md +45 -0
- treenode/forms.py +8 -10
- treenode/managers/__init__.py +21 -0
- treenode/managers/adjacency.py +203 -0
- treenode/managers/closure.py +278 -0
- treenode/models/__init__.py +2 -1
- treenode/models/adjacency.py +343 -0
- treenode/models/classproperty.py +3 -0
- treenode/models/closure.py +23 -24
- treenode/models/factory.py +12 -2
- treenode/models/mixins/__init__.py +23 -0
- treenode/models/mixins/ancestors.py +65 -0
- treenode/models/mixins/children.py +81 -0
- treenode/models/mixins/descendants.py +66 -0
- treenode/models/mixins/family.py +63 -0
- treenode/models/mixins/logical.py +68 -0
- treenode/models/mixins/node.py +210 -0
- treenode/models/mixins/properties.py +156 -0
- treenode/models/mixins/roots.py +96 -0
- treenode/models/mixins/siblings.py +99 -0
- treenode/models/mixins/tree.py +344 -0
- treenode/signals.py +26 -0
- treenode/static/treenode/css/tree_widget.css +201 -31
- treenode/static/treenode/css/treenode_admin.css +48 -41
- treenode/static/treenode/js/tree_widget.js +269 -131
- treenode/static/treenode/js/treenode_admin.js +131 -171
- treenode/templates/admin/tree_node_changelist.html +6 -0
- treenode/templates/admin/treenode_ajax_rows.html +7 -0
- treenode/tests/tests.py +488 -0
- treenode/urls.py +10 -6
- treenode/utils/__init__.py +2 -0
- treenode/utils/aid.py +46 -0
- treenode/utils/base16.py +38 -0
- treenode/utils/base36.py +3 -1
- treenode/utils/db.py +116 -0
- treenode/utils/exporter.py +2 -0
- treenode/utils/importer.py +0 -1
- treenode/utils/radix.py +61 -0
- treenode/version.py +2 -2
- treenode/views.py +118 -43
- treenode/widgets.py +91 -43
- django_fast_treenode-2.0.11.dist-info/METADATA +0 -698
- django_fast_treenode-2.0.11.dist-info/RECORD +0 -42
- treenode/admin.py +0 -439
- treenode/docs/Documentation +0 -636
- treenode/managers.py +0 -419
- treenode/models/proxy.py +0 -669
- {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 = '—' * 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
|