django-fast-treenode 1.1.3__py3-none-any.whl → 2.0.1__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-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
|
+
|