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.
- {django_fast_treenode-2.0.10.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.10.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 +33 -22
- 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 +39 -65
- 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/tree_node_import.html +27 -9
- treenode/templates/admin/tree_node_import_report.html +32 -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 +63 -36
- treenode/utils/importer.py +168 -161
- treenode/utils/radix.py +61 -0
- treenode/version.py +2 -2
- treenode/views.py +119 -38
- treenode/widgets.py +104 -40
- django_fast_treenode-2.0.10.dist-info/METADATA +0 -698
- django_fast_treenode-2.0.10.dist-info/RECORD +0 -41
- treenode/admin.py +0 -396
- treenode/docs/Documentation +0 -664
- treenode/managers.py +0 -281
- treenode/models/proxy.py +0 -650
- {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/top_level.txt +0 -0
treenode/admin/mixins.py
ADDED
@@ -0,0 +1,302 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
Views Mixin for TreeNodeAdminModel
|
4
|
+
|
5
|
+
Version: 2.1.0
|
6
|
+
Author: Timur Kady
|
7
|
+
Email: kaduevtr@gmail.com
|
8
|
+
"""
|
9
|
+
|
10
|
+
import os
|
11
|
+
from datetime import datetime
|
12
|
+
from django.contrib import admin
|
13
|
+
from django.contrib import messages
|
14
|
+
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
|
15
|
+
from django.db import models
|
16
|
+
from django.http import JsonResponse
|
17
|
+
from django.shortcuts import render, redirect
|
18
|
+
from django.shortcuts import resolve_url
|
19
|
+
from django.template.loader import render_to_string
|
20
|
+
from django.urls import path
|
21
|
+
from django.utils.encoding import force_str
|
22
|
+
from django.utils.safestring import mark_safe
|
23
|
+
|
24
|
+
import logging
|
25
|
+
|
26
|
+
logger = logging.getLogger(__name__)
|
27
|
+
|
28
|
+
|
29
|
+
class AdminMixin(admin.ModelAdmin):
|
30
|
+
"""Admin Mixin with views."""
|
31
|
+
|
32
|
+
def change_list_view(self, request):
|
33
|
+
"""
|
34
|
+
View for lazy loading of child nodes.
|
35
|
+
|
36
|
+
Clicking the expand button sends an AJAX request to this view, which
|
37
|
+
returns JSON with the data of the selected node's immediate children.
|
38
|
+
All returned nodes are collapsed by default.
|
39
|
+
"""
|
40
|
+
parent_id = request.GET.get("tn_parent_id")
|
41
|
+
if not parent_id:
|
42
|
+
# If there is no AJAX parameter, then it's normal work: show roots
|
43
|
+
return super().change_list_view(request)
|
44
|
+
|
45
|
+
parent = self.model.objects.filter(pk=parent_id).first()
|
46
|
+
if not parent:
|
47
|
+
# If there is no parent, return an empty response
|
48
|
+
return JsonResponse({'html': ''})
|
49
|
+
|
50
|
+
children = parent.get_children_queryset()
|
51
|
+
|
52
|
+
# We take list_display to understand which "columns" are needed
|
53
|
+
list_display = self.get_list_display(request)
|
54
|
+
|
55
|
+
# We collect future "rows"; each as a list of cells
|
56
|
+
rows = []
|
57
|
+
td_classes = []
|
58
|
+
for obj in children:
|
59
|
+
row_data = []
|
60
|
+
checkbox = mark_safe(
|
61
|
+
f'<input type="checkbox" name="{ACTION_CHECKBOX_NAME}"'
|
62
|
+
f' value="{obj.pk}" class="action-select" />'
|
63
|
+
)
|
64
|
+
row_data.append(checkbox)
|
65
|
+
td_classes.append("action-checkbox")
|
66
|
+
for field in list_display:
|
67
|
+
if callable(field):
|
68
|
+
# If it is a method (drag, toggle, etc.), call
|
69
|
+
row_data.append(field(obj))
|
70
|
+
field_name = field.__name__
|
71
|
+
else:
|
72
|
+
# If it is a string, we try to get the attribute from
|
73
|
+
# the object
|
74
|
+
value = getattr(obj, field, '')
|
75
|
+
if callable(value):
|
76
|
+
# If suddenly this is also a method
|
77
|
+
# (for example, property), then call it
|
78
|
+
value = value()
|
79
|
+
row_data.append(value)
|
80
|
+
field_name = field
|
81
|
+
td_classes.append(f"field-{field_name}")
|
82
|
+
|
83
|
+
row_info = {
|
84
|
+
"node_id": obj.pk,
|
85
|
+
"parent_id": parent_id,
|
86
|
+
"cells": zip(row_data, td_classes),
|
87
|
+
}
|
88
|
+
rows.append(row_info)
|
89
|
+
|
90
|
+
# Pass rows to the template
|
91
|
+
html = render_to_string(
|
92
|
+
'admin/treenode_ajax_rows.html',
|
93
|
+
{"rows": rows},
|
94
|
+
request=request
|
95
|
+
)
|
96
|
+
|
97
|
+
return JsonResponse({'html': html})
|
98
|
+
|
99
|
+
def search_view(self, request):
|
100
|
+
"""View for finding nodes using get_list_display."""
|
101
|
+
# Get a search query
|
102
|
+
q = request.GET.get("q", "")
|
103
|
+
|
104
|
+
# Perform filtering with annotation to calculate the level
|
105
|
+
queryset = self.model.objects\
|
106
|
+
.annotate(cl_depth=models.Max("parents_set__depth"))\
|
107
|
+
.filter(name__icontains=q)
|
108
|
+
queryset_list = list(queryset)[:20]
|
109
|
+
sorted_list = self.model._sort_node_list(queryset_list)
|
110
|
+
|
111
|
+
# Get the set of columns as it is formed in change_list.
|
112
|
+
# The first two columns (drag and toggle) are not needed for searching,
|
113
|
+
# and the main display column is at index 2.
|
114
|
+
list_display = self.get_list_display(request)
|
115
|
+
display_func = list_display[2]
|
116
|
+
|
117
|
+
results = []
|
118
|
+
for node in sorted_list:
|
119
|
+
# Get the HTML display of the node via the function generated by
|
120
|
+
# get_list_display
|
121
|
+
display_html = display_func(node)
|
122
|
+
results.append({
|
123
|
+
"id": node.pk,
|
124
|
+
"text": display_html,
|
125
|
+
"level": node.cl_depth,
|
126
|
+
"is_leaf": node.is_leaf(),
|
127
|
+
})
|
128
|
+
|
129
|
+
return JsonResponse({"results": results})
|
130
|
+
|
131
|
+
def import_view(self, request):
|
132
|
+
"""
|
133
|
+
Import View.
|
134
|
+
|
135
|
+
File upload processing, auto-detection of format, validation and data
|
136
|
+
import.
|
137
|
+
"""
|
138
|
+
if not self.import_export:
|
139
|
+
self.message_user(
|
140
|
+
request,
|
141
|
+
"Import functionality is disabled because required \
|
142
|
+
packages are not installed."
|
143
|
+
)
|
144
|
+
return redirect("..")
|
145
|
+
|
146
|
+
if request.method == 'POST':
|
147
|
+
if 'file' not in request.FILES:
|
148
|
+
return render(
|
149
|
+
request,
|
150
|
+
"admin/tree_node_import.html",
|
151
|
+
{"errors": ["No file uploaded."]}
|
152
|
+
)
|
153
|
+
|
154
|
+
file = request.FILES['file']
|
155
|
+
ext = os.path.splitext(file.name)[-1].lower().strip(".")
|
156
|
+
|
157
|
+
allowed_formats = {"csv", "json", "xlsx", "yaml", "tsv"}
|
158
|
+
if ext not in allowed_formats:
|
159
|
+
return render(
|
160
|
+
request,
|
161
|
+
"admin/tree_node_import.html",
|
162
|
+
{"errors": [f"Unsupported file format: {ext}"]}
|
163
|
+
)
|
164
|
+
|
165
|
+
# Import data from file
|
166
|
+
importer = self.TreeNodeImporter(self.model, file, ext)
|
167
|
+
raw_data = importer.import_data()
|
168
|
+
clean_result = importer.finalize(raw_data)
|
169
|
+
|
170
|
+
errors = clean_result.get("errors", [])
|
171
|
+
created_count = len(clean_result.get("create", []))
|
172
|
+
updated_count = len(clean_result.get("update", []))
|
173
|
+
|
174
|
+
if errors:
|
175
|
+
return render(
|
176
|
+
request,
|
177
|
+
"admin/tree_node_import_report.html",
|
178
|
+
{
|
179
|
+
"errors": errors,
|
180
|
+
"created_count": created_count,
|
181
|
+
"updated_count": updated_count,
|
182
|
+
}
|
183
|
+
)
|
184
|
+
|
185
|
+
# If there are no errors, redirect to the list of objects with
|
186
|
+
# a message
|
187
|
+
messages.success(
|
188
|
+
request,
|
189
|
+
f"Successfully imported {created_count} records. "
|
190
|
+
f"Successfully updated {updated_count} records."
|
191
|
+
)
|
192
|
+
|
193
|
+
app_label = self.model._meta.app_label
|
194
|
+
model_name = self.model._meta.model_name
|
195
|
+
admin_changelist_url = f"admin:{app_label}_{model_name}_changelist"
|
196
|
+
path = resolve_url(admin_changelist_url) + "?import_done=1"
|
197
|
+
return redirect(path)
|
198
|
+
|
199
|
+
# If the request is not POST, simply display the import form
|
200
|
+
return render(request, "admin/tree_node_import.html")
|
201
|
+
|
202
|
+
def export_view(self, request):
|
203
|
+
"""
|
204
|
+
Export view.
|
205
|
+
|
206
|
+
- If the GET parameters include download, we send the file directly.
|
207
|
+
- If the format parameter is missing, we render the format selection
|
208
|
+
page.
|
209
|
+
- If the format is specified, we perform a test export to catch errors.
|
210
|
+
|
211
|
+
If there are no errors, we render the success page with a message, a
|
212
|
+
link for manual download,
|
213
|
+
and a button to go to the model page.
|
214
|
+
"""
|
215
|
+
if not self.import_export:
|
216
|
+
self.message_user(
|
217
|
+
request,
|
218
|
+
"Export functionality is disabled because required \
|
219
|
+
packages are not installed."
|
220
|
+
)
|
221
|
+
return redirect("..")
|
222
|
+
|
223
|
+
# If the download parameter is present, we give the file directly
|
224
|
+
if 'download' in request.GET:
|
225
|
+
# Get file format
|
226
|
+
export_format = request.GET.get('format', 'csv')
|
227
|
+
# Filename
|
228
|
+
now = force_str(datetime.now().strftime("%Y-%m-%d %H-%M"))
|
229
|
+
filename = self.model._meta.label + " " + now
|
230
|
+
# Init
|
231
|
+
exporter = self.TreeNodeExporter(
|
232
|
+
self.get_queryset(request),
|
233
|
+
filename=filename
|
234
|
+
)
|
235
|
+
# Export working
|
236
|
+
response = exporter.export(export_format)
|
237
|
+
logger.debug("DEBUG: File response generated.")
|
238
|
+
return response
|
239
|
+
|
240
|
+
# If the format parameter is not passed, we show the format
|
241
|
+
# selection page
|
242
|
+
if 'format' not in request.GET:
|
243
|
+
return render(request, "admin/tree_node_export.html")
|
244
|
+
|
245
|
+
# If the format is specified, we try to perform a test export
|
246
|
+
# (without returning the file)
|
247
|
+
export_format = request.GET['format']
|
248
|
+
exporter = self.TreeNodeExporter(
|
249
|
+
self.model.objects.all(),
|
250
|
+
filename=self.model._meta.model_name
|
251
|
+
)
|
252
|
+
try:
|
253
|
+
# Test call to check for export errors (result not used)
|
254
|
+
exporter.export(export_format)
|
255
|
+
except Exception as e:
|
256
|
+
logger.error("Error during test export: %s", e)
|
257
|
+
errors = [str(e)]
|
258
|
+
return render(
|
259
|
+
request,
|
260
|
+
"admin/tree_node_export.html",
|
261
|
+
{"errors": errors}
|
262
|
+
)
|
263
|
+
|
264
|
+
# Form the correct download URL. If the URL already contains
|
265
|
+
# parameters, add them via &download=1, otherwise via ?download=1
|
266
|
+
current_url = request.build_absolute_uri()
|
267
|
+
if "?" in current_url:
|
268
|
+
download_url = current_url + "&download=1"
|
269
|
+
else:
|
270
|
+
download_url = current_url + "?download=1"
|
271
|
+
|
272
|
+
context = {
|
273
|
+
"download_url": download_url,
|
274
|
+
"message": "Your file is ready for export. \
|
275
|
+
The download should start automatically.",
|
276
|
+
"manual_download_label": "If the download does not start, \
|
277
|
+
click this link.",
|
278
|
+
# Can be replaced with the desired URL to return to the model
|
279
|
+
"redirect_url": "../",
|
280
|
+
"button_text": "Return to model"
|
281
|
+
}
|
282
|
+
return render(request, "admin/export_success.html", context)
|
283
|
+
|
284
|
+
def get_urls(self):
|
285
|
+
"""
|
286
|
+
Extend admin URLs with custom import/export routes.
|
287
|
+
|
288
|
+
Register these URLs only if all the required packages are installed.
|
289
|
+
"""
|
290
|
+
urls = super().get_urls()
|
291
|
+
if self.import_export:
|
292
|
+
custom_urls = [
|
293
|
+
path('change_list/', self.change_list_view, name='change_list'),
|
294
|
+
path('search/', self.search_view, name='search'),
|
295
|
+
path('import/', self.import_view, name='tree_node_import'),
|
296
|
+
path('export/', self.export_view, name='tree_node_export'),
|
297
|
+
]
|
298
|
+
else:
|
299
|
+
custom_urls = []
|
300
|
+
return custom_urls + urls
|
301
|
+
|
302
|
+
# The End
|
treenode/apps.py
CHANGED
@@ -5,13 +5,17 @@ TreeNode Application Configuration
|
|
5
5
|
This module defines the application configuration for the TreeNode app.
|
6
6
|
It sets the default auto field and specifies the app's name.
|
7
7
|
|
8
|
-
Version: 2.
|
8
|
+
Version: 2.1.0
|
9
9
|
Author: Timur Kady
|
10
10
|
Email: timurkady@yandex.com
|
11
11
|
"""
|
12
12
|
|
13
13
|
|
14
|
+
import logging
|
14
15
|
from django.apps import AppConfig
|
16
|
+
from django.db.models.signals import post_migrate
|
17
|
+
|
18
|
+
logger = logging.getLogger(__name__)
|
15
19
|
|
16
20
|
|
17
21
|
class TreeNodeConfig(AppConfig):
|
@@ -20,4 +24,11 @@ class TreeNodeConfig(AppConfig):
|
|
20
24
|
default_auto_field = "django.db.models.BigAutoField"
|
21
25
|
name = "treenode"
|
22
26
|
|
27
|
+
def ready(self):
|
28
|
+
"""
|
29
|
+
Attach a post_migrate handler.
|
23
30
|
|
31
|
+
This allows you to perform operations after the migration is complete.
|
32
|
+
"""
|
33
|
+
from .utils.db import post_migrate_update
|
34
|
+
post_migrate.connect(post_migrate_update, sender=self)
|
treenode/cache.py
CHANGED
treenode/docs/.gitignore
ADDED
File without changes
|
treenode/docs/about.md
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
## About the project
|
2
|
+
### The Debut Idea
|
3
|
+
The idea of this package belongs to **[Fabio Caccamo](https://github.com/fabiocaccamo)**. His idea was to use the **Adjacency List** method to store the data tree. The most probable and time-consuming requests are calculated in advance and stored in the database. Also, most requests are cached. As a result, query processing is carried out in one call to the database or without it at all.
|
4
|
+
|
5
|
+
The original application **[django-treenode](https://github.com/fabiocaccamo/django-treenode)** has significant advantages over other analogues, and indeed, is one of the best implementations of support for hierarchical structures for Django.
|
6
|
+
|
7
|
+
However, this application has a number of undeniable shortcomings:
|
8
|
+
* the selected pre-calculations scheme entails high costs for adding a new element;
|
9
|
+
* inserting new elements uses signals, which leads to failures when using bulk-operations;
|
10
|
+
* the problem of ordering elements by priority inside the parent node has not been resolved.
|
11
|
+
|
12
|
+
That is, an excellent debut idea, in my humble opinion, should have been improved.
|
13
|
+
|
14
|
+
### The Development of the Idea
|
15
|
+
My idea was to solve these problems by combining the **Adjacency List** with the **Closure Table**. Main advantages:
|
16
|
+
* the Closure Model is generated automatically;
|
17
|
+
* maintained compatibility with the original package at the level of documented functions;
|
18
|
+
* most requests are satisfied in one call to the database;
|
19
|
+
* inserting a new element takes two calls to the database without signals usage;
|
20
|
+
* bulk-operations are supported;
|
21
|
+
* the cost of creating a new dependency is reduced many times;
|
22
|
+
* useful functionality added for some methods (e.g. the `include_self=False` and `depth` parameters has been added to functions that return lists/querysets);
|
23
|
+
* additionally, the package includes a tree view widget for the `tn_parent` field in the change form.
|
24
|
+
|
25
|
+
Of course, at large levels of nesting, the use of the Closure Table leads to an increase in resource costs. However, the combined approach still outperforms both the original application and other available Django solutions in terms of performance, especially in large trees with over 100k nodes.
|
26
|
+
|
27
|
+
### The Theory
|
28
|
+
You can get a basic understanding of what is a **Closure Table** from:
|
29
|
+
* [presentation](https://www.slideshare.net/billkarwin/models-for-hierarchical-data) by **Bill Karwin**;
|
30
|
+
* [article](https://dirtsimple.org/2010/11/simplest-way-to-do-tree-based-queries.html) by blogger **Dirt Simple**;
|
31
|
+
* [article](https://towardsdatascience.com/closure-table-pattern-to-model-hierarchies-in-nosql-c1be6a87e05b) by **Andriy Zabavskyy**.
|
32
|
+
|
33
|
+
You can easily find additional information on your own on the Internet.
|
34
|
+
|
35
|
+
### Our days
|
36
|
+
Over the course of development, the package has undergone significant improvements, with a strong emphasis on performance optimization, database efficiency, and seamless integration with Django’s admin interface. The introduction of a hybrid model combining **Adjacency List** and **Closure Table** has substantially reduced query overhead, improved scalability, and enhanced flexibility when managing hierarchical data. These advancements have made the package not only a powerful but also a practical solution for working with large tree structures. Moving forward, the project will continue to evolve, focusing on refining caching mechanisms, expanding compatibility with Django’s ecosystem, and introducing further optimizations to ensure maximum efficiency and ease of use.
|
treenode/docs/admin.md
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
## Working with Admin Classes
|
2
|
+
|
3
|
+
### Using `TreeNodeModelAdmin`
|
4
|
+
The easiest way to integrate tree structures into Django’s admin panel is by inheriting from `TreeNodeModelAdmin`. This base class provides all the necessary functionality for managing hierarchical data.
|
5
|
+
|
6
|
+
##### admin.py:
|
7
|
+
```python
|
8
|
+
from django.contrib import admin
|
9
|
+
from treenode.admin import TreeNodeModelAdmin
|
10
|
+
|
11
|
+
from .models import Category
|
12
|
+
|
13
|
+
@admin.register(Category)
|
14
|
+
class CategoryAdmin(TreeNodeModelAdmin):
|
15
|
+
|
16
|
+
# Set the display mode: 'accordion', 'breadcrumbs', or 'indentation'
|
17
|
+
treenode_display_mode = TreeNodeModelAdmin.TREENODE_DISPLAY_MODE_ACCORDION
|
18
|
+
# treenode_display_mode = TreeNodeModelAdmin.TREENODE_DISPLAY_MODE_BREADCRUMBS
|
19
|
+
# treenode_display_mode = TreeNodeModelAdmin.TREENODE_DISPLAY_MODE_INDENTATION
|
20
|
+
|
21
|
+
list_display = ("name",)
|
22
|
+
search_fields = ("name",)
|
23
|
+
```
|
24
|
+
|
25
|
+
The tree structure in the admin panel **loads dynamically as nodes are expanded**. This allows handling **large datasets** efficiently, preventing performance issues.
|
26
|
+
|
27
|
+
You can choose from three display modes:
|
28
|
+
- **`TREENODE_DISPLAY_MODE_ACCORDION` (default)**
|
29
|
+
Expands/collapses nodes dynamically.
|
30
|
+
- **`TREENODE_DISPLAY_MODE_BREADCRUMBS`**
|
31
|
+
Displays the tree as a sequence of **breadcrumbs**, making it easy to navigate.
|
32
|
+
- **`TREENODE_DISPLAY_MODE_INDENTATION`**
|
33
|
+
Uses a **long dash** (`———`) to indicate nesting levels, providing a simple visual structure.
|
34
|
+
|
35
|
+
The accordion mode is **always active**, and the setting only affects how nodes are displayed.
|
36
|
+
|
37
|
+
**Why Dynamic Loading**: Traditional pagination does not work well for **deep hierarchical trees**, as collapsed trees may contain a **huge number of nodes**, which is in the hundreds of thousands. The dynamic approach allows efficient loading, reducing database load while keeping large trees manageable.
|
38
|
+
|
39
|
+
#### Search Functionality
|
40
|
+
The search bar helps quickly locate nodes within large trees. As you type, **an AJAX request retrieves up to 20 results** based on relevance. If you don’t find the desired node, keep typing to refine the search until fewer than 20 results remain.
|
41
|
+
|
42
|
+
### Working with Forms
|
43
|
+
|
44
|
+
#### Using TreeNodeForm
|
45
|
+
If you need to customize forms for tree-based models, inherit from `TreeNodeForm`. It provides:
|
46
|
+
- A **custom tree widget** for selecting parent nodes.
|
47
|
+
- Automatic **exclusion of self and descendants** from the parent selection to prevent circular references.
|
48
|
+
|
49
|
+
##### `forms.py`:
|
50
|
+
```python
|
51
|
+
from treenode.forms import TreeNodeForm
|
52
|
+
from .models import Category
|
53
|
+
|
54
|
+
class CategoryForm(TreeNodeForm):
|
55
|
+
"""Form for Category model with hierarchical selection."""
|
56
|
+
|
57
|
+
class Meta(TreeNodeForm.Meta):
|
58
|
+
model = Category
|
59
|
+
```
|
60
|
+
|
61
|
+
Key Considerations:
|
62
|
+
- This form automatically ensures that **a node cannot be its own parent**.
|
63
|
+
- It uses **`TreeWidget`**, a custom hierarchical dropdown for selecting parent nodes.
|
64
|
+
- If you need a form for another tree-based model, use the **dynamic factory method**:
|
65
|
+
|
66
|
+
```python
|
67
|
+
CategoryForm = TreeNodeForm.factory(Category)
|
68
|
+
```
|
69
|
+
|
70
|
+
This method ensures that the form correctly associates with different tree models dynamically.
|
71
|
+
|
72
|
+
|
73
|
+
### Using TreeWidget Widget
|
74
|
+
|
75
|
+
#### The TreeWidget Class
|
76
|
+
The `TreeWidget` class is a **custom Select2-like widget** that enables hierarchical selection in forms. While it is used inside the Django admin panel by default, it can **also be used in regular forms** outside the admin panel.
|
77
|
+
|
78
|
+
##### `widgets.py`
|
79
|
+
|
80
|
+
```python
|
81
|
+
from django import forms
|
82
|
+
from treenode.widgets import TreeWidget
|
83
|
+
from .models import Category
|
84
|
+
|
85
|
+
class CategorySelectionForm(forms.Form):
|
86
|
+
parent = forms.ModelChoiceField(
|
87
|
+
queryset=Category.objects.all(),
|
88
|
+
widget=TreeWidget(),
|
89
|
+
required=False
|
90
|
+
)
|
91
|
+
```
|
92
|
+
|
93
|
+
Important Notes:
|
94
|
+
- **Requires jQuery**: The widget relies on AJAX requests, so ensure jQuery is available when using it outside Django’s admin.
|
95
|
+
- **Dynamically Fetches Data**: It loads the tree structure asynchronously, preventing performance issues with large datasets.
|
96
|
+
- **Customizable Data Source**: The `data-url` attribute can be adjusted to fetch tree data from a custom endpoint.
|
97
|
+
|
98
|
+
If you plan to use this widget in non-admin templates, make sure the necessary **JavaScript and CSS files** are included:
|
99
|
+
```html
|
100
|
+
<link rel="stylesheet" href="/static/treenode/tree_widget.css">
|
101
|
+
<script src="/static/treenode/js/tree_widget.js"></script>
|
102
|
+
```
|
103
|
+
|
104
|
+
By following these guidelines, you can seamlessly integrate `TreeNodeModelAdmin`, `TreeNodeForm`, and `TreeWidget` into your Django project, ensuring efficient management of hierarchical data.
|