django-fast-treenode 1.1.3__py3-none-any.whl → 2.0.1__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.1.dist-info}/METADATA +127 -46
- django_fast_treenode-2.0.1.dist-info/RECORD +41 -0
- {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.1.dist-info}/WHEEL +1 -1
- treenode/__init__.py +0 -7
- treenode/admin.py +327 -82
- treenode/apps.py +20 -3
- treenode/cache.py +231 -0
- treenode/docs/Documentation +101 -54
- treenode/forms.py +75 -19
- treenode/managers.py +260 -48
- treenode/models/__init__.py +7 -0
- treenode/models/classproperty.py +24 -0
- treenode/models/closure.py +168 -0
- treenode/models/factory.py +71 -0
- treenode/models/proxy.py +650 -0
- treenode/static/treenode/css/tree_widget.css +62 -0
- treenode/static/treenode/css/treenode_admin.css +106 -0
- treenode/static/treenode/js/tree_widget.js +161 -0
- treenode/static/treenode/js/treenode_admin.js +171 -0
- treenode/templates/admin/export_success.html +26 -0
- treenode/templates/admin/tree_node_changelist.html +11 -0
- treenode/templates/admin/tree_node_export.html +27 -0
- treenode/templates/admin/tree_node_import.html +27 -0
- treenode/templates/widgets/tree_widget.css +23 -0
- treenode/templates/widgets/tree_widget.html +21 -0
- treenode/urls.py +34 -0
- treenode/utils/__init__.py +4 -0
- treenode/utils/base36.py +35 -0
- treenode/utils/exporter.py +141 -0
- treenode/utils/importer.py +296 -0
- treenode/version.py +11 -1
- treenode/views.py +102 -2
- treenode/widgets.py +49 -27
- django_fast_treenode-1.1.3.dist-info/RECORD +0 -33
- treenode/compat.py +0 -8
- treenode/factory.py +0 -68
- treenode/models.py +0 -668
- treenode/static/select2tree/.gitkeep +0 -1
- treenode/static/select2tree/select2tree.css +0 -176
- treenode/static/select2tree/select2tree.js +0 -181
- treenode/static/treenode/css/treenode.css +0 -85
- treenode/static/treenode/js/treenode.js +0 -201
- treenode/templates/widgets/.gitkeep +0 -1
- treenode/templates/widgets/attrs.html +0 -7
- treenode/templates/widgets/options.html +0 -1
- treenode/templates/widgets/select2tree.html +0 -22
- treenode/tests.py +0 -3
- {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.1.dist-info}/LICENSE +0 -0
- {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.1.dist-info}/top_level.txt +0 -0
- /treenode/{docs → templates/admin}/.gitkeep +0 -0
treenode/admin.py
CHANGED
@@ -2,135 +2,380 @@
|
|
2
2
|
"""
|
3
3
|
TreeNode Admin Module
|
4
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.0
|
10
|
+
Author: Timur Kady
|
11
|
+
Email: kaduevtr@gmail.com
|
5
12
|
"""
|
6
13
|
|
14
|
+
|
15
|
+
import os
|
16
|
+
import importlib
|
17
|
+
from datetime import datetime
|
7
18
|
from django.contrib import admin
|
8
|
-
from django.utils.safestring import mark_safe
|
9
19
|
from django.contrib.admin.views.main import ChangeList
|
20
|
+
from django.db import models
|
21
|
+
from django.db.models import Case, When, Value, IntegerField
|
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 .utils import TreeNodeImporter, TreeNodeExporter
|
10
29
|
from .forms import TreeNodeForm
|
30
|
+
from .widgets import TreeWidget
|
31
|
+
|
32
|
+
import logging
|
33
|
+
|
34
|
+
logger = logging.getLogger(__name__)
|
11
35
|
|
12
36
|
|
13
37
|
class NoPkDescOrderedChangeList(ChangeList):
|
38
|
+
"""Custom ChangeList to remove descending sorting `pk` (default)."""
|
39
|
+
|
14
40
|
def get_ordering(self, request, queryset):
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
41
|
+
"""
|
42
|
+
Override ordering.
|
43
|
+
|
44
|
+
Overrides the sort order of objects in the list.
|
45
|
+
Django Admin sorts by `-pk` (descending) by default.
|
46
|
+
This method removes `-pk` so that objects are not sorted by ID.
|
47
|
+
"""
|
48
|
+
rv = list(super().get_ordering(request, queryset))
|
49
|
+
if '-pk' in rv:
|
50
|
+
rv.remove('-pk')
|
51
|
+
return tuple(rv)
|
19
52
|
|
20
53
|
def get_queryset(self, request):
|
21
|
-
|
22
|
-
|
54
|
+
"""
|
55
|
+
Override QuerySet.
|
56
|
+
|
57
|
+
Overrides data selection to optimize queries. Also adds
|
58
|
+
`select_related('tn_parent')` to avoid N+1 queries.
|
59
|
+
"""
|
60
|
+
queryset = super(NoPkDescOrderedChangeList, self).get_queryset(request)
|
61
|
+
node_list = sorted(queryset, key=lambda x: x.tn_order)
|
62
|
+
pk_list = [node.pk for node in node_list]
|
23
63
|
|
64
|
+
return queryset.filter(pk__in=pk_list).order_by(
|
65
|
+
Case(*[When(pk=pk, then=Value(index))
|
66
|
+
for index, pk in enumerate(pk_list)],
|
67
|
+
default=Value(len(pk_list)),
|
68
|
+
output_field=IntegerField())
|
69
|
+
).select_related('tn_parent')
|
24
70
|
|
25
|
-
|
71
|
+
|
72
|
+
class TreeNodeAdminModel(admin.ModelAdmin):
|
73
|
+
"""
|
74
|
+
TreeNodeAdmin class.
|
75
|
+
|
76
|
+
Admin configuration for TreeNodeModel with import/export functionality.
|
77
|
+
"""
|
26
78
|
|
27
79
|
TREENODE_DISPLAY_MODE_ACCORDION = 'accordion'
|
28
80
|
TREENODE_DISPLAY_MODE_BREADCRUMBS = 'breadcrumbs'
|
29
81
|
TREENODE_DISPLAY_MODE_INDENTATION = 'indentation'
|
30
82
|
|
31
|
-
treenode_display_mode =
|
83
|
+
treenode_display_mode = TREENODE_DISPLAY_MODE_ACCORDION
|
32
84
|
|
33
|
-
|
85
|
+
change_list_template = "admin/tree_node_changelist.html"
|
86
|
+
ordering = []
|
34
87
|
list_per_page = 1000
|
88
|
+
list_sorting_mode_session_key = "treenode_sorting_mode"
|
89
|
+
|
90
|
+
form = TreeNodeForm
|
91
|
+
formfield_overrides = {
|
92
|
+
models.ForeignKey: {"widget": TreeWidget()},
|
93
|
+
}
|
94
|
+
|
95
|
+
class Media:
|
96
|
+
"""Include custom CSS and JavaScript for admin interface."""
|
97
|
+
|
98
|
+
css = {"all": (
|
99
|
+
"treenode/css/treenode_admin.css",
|
100
|
+
)}
|
101
|
+
js = (
|
102
|
+
'admin/js/jquery.init.js',
|
103
|
+
'treenode/js/treenode_admin.js',
|
104
|
+
)
|
105
|
+
|
106
|
+
def __init__(self, model, admin_site):
|
107
|
+
"""Динамически добавляем поле `tn_order` в `list_display`."""
|
108
|
+
super().__init__(model, admin_site)
|
109
|
+
|
110
|
+
# Если `list_display` пустой, берем все `fields`
|
111
|
+
if not self.list_display:
|
112
|
+
self.list_display = [field.name for field in model._meta.fields]
|
113
|
+
|
114
|
+
def get_queryset(self, request):
|
115
|
+
"""Override get_ueryset()."""
|
116
|
+
queryset = super().get_queryset(request)
|
117
|
+
|
118
|
+
search_term = request.GET.get("q")
|
119
|
+
if search_term:
|
120
|
+
"""
|
121
|
+
print(f"Поиск: {search_term}")
|
122
|
+
search_fields = self.get_search_fields(request)
|
123
|
+
print(f"Поиск по полям: {search_fields}")
|
124
|
+
|
125
|
+
q_objects = Q()
|
126
|
+
|
127
|
+
for field in search_fields:
|
128
|
+
q_objects |= Q(**{f"{field}__icontains": search_term})
|
129
|
+
|
130
|
+
|
131
|
+
queryset = queryset.filter(q_objects)
|
132
|
+
print(f"Найдено записей: {queryset.count()}")
|
133
|
+
"""
|
134
|
+
return queryset
|
135
|
+
|
136
|
+
node_list = sorted(queryset, key=lambda x: x.tn_order)
|
137
|
+
pk_list = [node.pk for node in node_list]
|
138
|
+
return queryset.filter(pk__in=pk_list).order_by(
|
139
|
+
Case(*[When(pk=pk, then=Value(index))
|
140
|
+
for index, pk in enumerate(pk_list)],
|
141
|
+
default=Value(len(pk_list)),
|
142
|
+
output_field=IntegerField())
|
143
|
+
)
|
144
|
+
|
145
|
+
def get_search_fields(self, request):
|
146
|
+
"""Return the correct search field."""
|
147
|
+
return [self.model.treenode_display_field]
|
35
148
|
|
36
149
|
def get_list_display(self, request):
|
37
|
-
|
38
|
-
|
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)
|
39
157
|
base_list_display = list(base_list_display)
|
40
158
|
|
41
159
|
def treenode_field_display(obj):
|
42
160
|
return self._get_treenode_field_display(request, obj)
|
43
161
|
|
44
|
-
|
162
|
+
verbose_name = self.model._meta.verbose_name
|
163
|
+
treenode_field_display.short_description = verbose_name
|
45
164
|
treenode_field_display.allow_tags = True
|
46
165
|
|
47
166
|
if len(base_list_display) == 1 and base_list_display[0] == '__str__':
|
48
|
-
return (treenode_field_display,
|
167
|
+
return (treenode_field_display,)
|
49
168
|
else:
|
50
169
|
treenode_display_field = getattr(
|
51
|
-
self.model,
|
52
|
-
|
170
|
+
self.model,
|
171
|
+
'treenode_display_field',
|
172
|
+
'__str__'
|
173
|
+
)
|
174
|
+
if base_list_display[0] == treenode_display_field:
|
53
175
|
base_list_display.pop(0)
|
54
|
-
return (treenode_field_display,
|
55
|
-
|
56
|
-
return base_list_display
|
176
|
+
return (treenode_field_display,) + tuple(base_list_display)
|
57
177
|
|
58
178
|
def get_changelist(self, request):
|
179
|
+
"""Get ChangeList."""
|
59
180
|
return NoPkDescOrderedChangeList
|
60
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'] = all(
|
186
|
+
importlib.util.find_spec(pkg)
|
187
|
+
is not None for pkg in ["openpyxl", "pyyaml", "pandas"]
|
188
|
+
)
|
189
|
+
return super().changelist_view(request, extra_context=extra_context)
|
190
|
+
|
61
191
|
def get_ordering(self, request):
|
192
|
+
"""Get Ordering."""
|
62
193
|
return None
|
63
194
|
|
64
|
-
def
|
65
|
-
|
195
|
+
def _get_row_display(self, obj):
|
196
|
+
"""Return row display for accordion mode."""
|
197
|
+
field = getattr(self.model, 'treenode_display_field')
|
198
|
+
return force_str(getattr(obj, field, obj.pk))
|
66
199
|
|
67
|
-
|
68
|
-
|
69
|
-
|
200
|
+
def _get_treenode_field_display(self, request, obj):
|
201
|
+
"""Define how to display nodes depending on the mode."""
|
202
|
+
display_mode = self.treenode_display_mode
|
203
|
+
if display_mode == self.TREENODE_DISPLAY_MODE_ACCORDION:
|
204
|
+
return self._display_with_accordion(obj)
|
205
|
+
elif display_mode == self.TREENODE_DISPLAY_MODE_BREADCRUMBS:
|
206
|
+
return self._display_with_breadcrumbs(obj)
|
207
|
+
elif display_mode == self.TREENODE_DISPLAY_MODE_INDENTATION:
|
208
|
+
return self._display_with_indentation(obj)
|
209
|
+
else:
|
210
|
+
return self._display_with_breadcrumbs(obj)
|
211
|
+
|
212
|
+
def _display_with_accordion(self, obj):
|
213
|
+
"""Display a tree in accordion style."""
|
214
|
+
parent = str(obj.tn_parent_id or '')
|
215
|
+
text = self._get_row_display(obj)
|
216
|
+
html = (
|
217
|
+
f'<div class="treenode-wrapper" '
|
218
|
+
f'data-treenode-pk="{obj.pk}" '
|
219
|
+
f'data-treenode-depth="{obj.depth}" '
|
220
|
+
f'data-treenode-parent="{parent}">'
|
221
|
+
f'<span class="treenode-content">{text}</span>'
|
222
|
+
f'</div>'
|
223
|
+
)
|
224
|
+
return mark_safe(html)
|
225
|
+
|
226
|
+
def _display_with_breadcrumbs(self, obj):
|
227
|
+
"""Display a tree as breadcrumbs."""
|
228
|
+
field = getattr(self.model, 'treenode_display_field')
|
229
|
+
if field is not None:
|
230
|
+
obj_display = " / ".join(obj.get_breadcrumbs(attr=field))
|
231
|
+
else:
|
232
|
+
obj_display = obj.get_path(
|
233
|
+
prefix=_("Node "),
|
234
|
+
suffix=" / " + obj.__str__()
|
70
235
|
)
|
71
|
-
|
72
|
-
|
73
|
-
|
236
|
+
display = f'<span class="treenode-breadcrumbs">{obj_display}</span>'
|
237
|
+
return mark_safe(display)
|
238
|
+
|
239
|
+
def _display_with_indentation(self, obj):
|
240
|
+
"""Display tree with indents."""
|
241
|
+
indent = '—' * obj.get_depth()
|
242
|
+
display = f'<span class="treenode-indentation">{indent}</span> {obj}'
|
243
|
+
return mark_safe(display)
|
244
|
+
|
245
|
+
def get_form(self, request, obj=None, **kwargs):
|
246
|
+
"""Get Form method."""
|
247
|
+
form = super().get_form(request, obj, **kwargs)
|
248
|
+
if "tn_parent" in form.base_fields:
|
249
|
+
form.base_fields["tn_parent"].widget = TreeWidget()
|
250
|
+
return form
|
251
|
+
|
252
|
+
def get_urls(self):
|
253
|
+
"""Extend admin URLs with custom import/export routes."""
|
254
|
+
urls = super().get_urls()
|
255
|
+
custom_urls = [
|
256
|
+
path('import/', self.import_view, name='tree_node_import'),
|
257
|
+
path('export/', self.export_view, name='tree_node_export'),
|
258
|
+
]
|
259
|
+
return custom_urls + urls
|
260
|
+
|
261
|
+
def import_view(self, request):
|
262
|
+
"""
|
263
|
+
Import View.
|
264
|
+
|
265
|
+
File upload processing, auto-detection of format, validation and data
|
266
|
+
import.
|
267
|
+
"""
|
268
|
+
if request.method == 'POST':
|
269
|
+
if 'file' not in request.FILES:
|
270
|
+
return render(
|
271
|
+
request,
|
272
|
+
"admin/tree_node_import.html",
|
273
|
+
{"errors": ["No file uploaded."]}
|
274
|
+
)
|
275
|
+
|
276
|
+
file = request.FILES['file']
|
277
|
+
ext = os.path.splitext(file.name)[-1].lower().strip(".")
|
278
|
+
|
279
|
+
allowed_formats = {"csv", "json", "xlsx", "yaml", "tsv"}
|
280
|
+
if ext not in allowed_formats:
|
281
|
+
return render(
|
282
|
+
request,
|
283
|
+
"admin/tree_node_import.html",
|
284
|
+
{"errors": [f"Unsupported file format: {ext}"]}
|
285
|
+
)
|
286
|
+
|
287
|
+
# Import data from file
|
288
|
+
importer = TreeNodeImporter(self.model, file, ext)
|
289
|
+
raw_data = importer.import_data()
|
290
|
+
clean_result = importer.clean(raw_data)
|
291
|
+
errors = importer.finalize_import(clean_result)
|
292
|
+
if errors:
|
293
|
+
return render(
|
294
|
+
request,
|
295
|
+
"admin/tree_node_import.html",
|
296
|
+
{"errors": errors}
|
297
|
+
)
|
298
|
+
self.message_user(
|
299
|
+
request,
|
300
|
+
f"Successfully imported {len(clean_result['create'])} records."
|
74
301
|
)
|
302
|
+
return redirect("..")
|
75
303
|
|
76
|
-
|
77
|
-
return
|
304
|
+
# If the request is not POST, simply display the import form
|
305
|
+
return render(request, "admin/tree_node_import.html")
|
78
306
|
|
79
|
-
def
|
80
|
-
|
81
|
-
|
307
|
+
def export_view(self, request):
|
308
|
+
"""
|
309
|
+
Export view.
|
82
310
|
|
83
|
-
|
84
|
-
|
311
|
+
- If the GET parameters include download, we send the file directly.
|
312
|
+
- If the format parameter is missing, we render the format selection
|
313
|
+
page.
|
314
|
+
- If the format is specified, we perform a test export to catch errors.
|
85
315
|
|
86
|
-
|
87
|
-
|
316
|
+
If there are no errors, we render the success page with a message, a
|
317
|
+
link for manual download,
|
318
|
+
and a button to go to the model page.
|
319
|
+
"""
|
320
|
+
# If the download parameter is present, we give the file directly
|
321
|
+
if 'download' in request.GET:
|
322
|
+
# Get file format
|
323
|
+
export_format = request.GET.get('format', 'csv')
|
324
|
+
# Important: This QuerySet provides a convenient ("friendly") order
|
325
|
+
# of tree node output during export/import.
|
326
|
+
queryset = self.get_queryset()
|
327
|
+
# Filename
|
328
|
+
now = force_str(datetime.now().strftime("%Y-%m-%d %H-%M"))
|
329
|
+
filename = self.model._meta.label + " " + now
|
330
|
+
# Init
|
331
|
+
exporter = TreeNodeExporter(queryset, filename=filename)
|
332
|
+
# Export working
|
333
|
+
response = exporter.export(export_format)
|
334
|
+
logger.debug("DEBUG: File response generated.")
|
335
|
+
return response
|
88
336
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
337
|
+
# If the format parameter is not passed, we show the format
|
338
|
+
# selection page
|
339
|
+
if 'format' not in request.GET:
|
340
|
+
return render(request, "admin/tree_node_export.html")
|
341
|
+
|
342
|
+
# If the format is specified, we try to perform a test export
|
343
|
+
# (without returning the file)
|
344
|
+
export_format = request.GET['format']
|
345
|
+
exporter = TreeNodeExporter(
|
346
|
+
self.model.objects.all(),
|
347
|
+
filename=self.model._meta.model_name
|
348
|
+
)
|
349
|
+
try:
|
350
|
+
# Test call to check for export errors (result not used)
|
351
|
+
exporter.export(export_format)
|
352
|
+
except Exception as e:
|
353
|
+
logger.error("Error during test export: %s", e)
|
354
|
+
errors = [str(e)]
|
355
|
+
return render(
|
356
|
+
request,
|
357
|
+
"admin/tree_node_export.html",
|
358
|
+
{"errors": errors}
|
359
|
+
)
|
360
|
+
|
361
|
+
# Form the correct download URL. If the URL already contains
|
362
|
+
# parameters, add them via &download=1, otherwise via ?download=1
|
363
|
+
current_url = request.build_absolute_uri()
|
364
|
+
if "?" in current_url:
|
365
|
+
download_url = current_url + "&download=1"
|
99
366
|
else:
|
100
|
-
|
101
|
-
|
102
|
-
def _get_treenode_field_display_with_accordion(self, obj):
|
103
|
-
tn_namespace = '%s.%s' % (obj.__module__, obj.__class__.__name__, )
|
104
|
-
tn_namespace_key = tn_namespace.lower().replace('.', '_')
|
105
|
-
return mark_safe(''
|
106
|
-
'<span class="treenode"'
|
107
|
-
' data-treenode-type="%s"'
|
108
|
-
' data-treenode-pk="%s"'
|
109
|
-
' data-treenode-accordion="1"'
|
110
|
-
' data-treenode-depth="%s"'
|
111
|
-
' data-treenode-level="%s"'
|
112
|
-
' data-treenode-parent="%s">%s</span>' % (
|
113
|
-
tn_namespace_key,
|
114
|
-
str(obj.pk),
|
115
|
-
str(obj.depth),
|
116
|
-
str(obj.level),
|
117
|
-
str(obj.tn_parent_id or ''),
|
118
|
-
obj.get_display(indent=False), ))
|
119
|
-
|
120
|
-
def _get_treenode_field_display_with_breadcrumbs(self, obj):
|
121
|
-
obj_display = ''
|
122
|
-
for obj_ancestor in obj.get_ancestors():
|
123
|
-
obj_ancestor_display = obj_ancestor.get_display(indent=False)
|
124
|
-
obj_display += '<span class="treenode-breadcrumbs">%s</span>' % (
|
125
|
-
obj_ancestor_display, )
|
126
|
-
obj_display += obj.get_display(indent=False)
|
127
|
-
return mark_safe('<span class="treenode">%s</span>' % (obj_display, ))
|
128
|
-
|
129
|
-
def _get_treenode_field_display_with_indentation(self, obj):
|
130
|
-
obj_display = '<span class="treenode-indentation">—</span>' * obj.ancestors_count
|
131
|
-
obj_display += obj.get_display(indent=False)
|
132
|
-
return mark_safe('<span class="treenode">%s</span>' % (obj_display, ))
|
367
|
+
download_url = current_url + "?download=1"
|
133
368
|
|
134
|
-
|
135
|
-
|
136
|
-
|
369
|
+
context = {
|
370
|
+
"download_url": download_url,
|
371
|
+
"message": "Your file is ready for export. \
|
372
|
+
The download should start automatically.",
|
373
|
+
"manual_download_label": "If the download does not start, \
|
374
|
+
click this link.",
|
375
|
+
# Can be replaced with the desired URL to return to the model
|
376
|
+
"redirect_url": "../",
|
377
|
+
"button_text": "Return to model"
|
378
|
+
}
|
379
|
+
return render(request, "admin/export_success.html", context)
|
380
|
+
|
381
|
+
# The End
|
treenode/apps.py
CHANGED
@@ -1,6 +1,23 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Application Configuration
|
4
|
+
|
5
|
+
This module defines the application configuration for the TreeNode app.
|
6
|
+
It sets the default auto field and specifies the app's name.
|
7
|
+
|
8
|
+
Version: 2.0.0
|
9
|
+
Author: Timur Kady
|
10
|
+
Email: timurkady@yandex.com
|
11
|
+
"""
|
12
|
+
|
13
|
+
|
1
14
|
from django.apps import AppConfig
|
2
15
|
|
3
16
|
|
4
|
-
class
|
5
|
-
|
6
|
-
|
17
|
+
class TreeNodeConfig(AppConfig):
|
18
|
+
"""TreeNodeConfig Class."""
|
19
|
+
|
20
|
+
default_auto_field = "django.db.models.BigAutoField"
|
21
|
+
name = "treenode"
|
22
|
+
|
23
|
+
|