django-fast-treenode 2.0.9__py3-none-any.whl → 2.0.11__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.9.dist-info → django_fast_treenode-2.0.11.dist-info}/METADATA +4 -3
- {django_fast_treenode-2.0.9.dist-info → django_fast_treenode-2.0.11.dist-info}/RECORD +19 -18
- treenode/admin.py +95 -71
- treenode/docs/Documentation +7 -35
- treenode/forms.py +64 -23
- treenode/managers.py +306 -168
- treenode/models/closure.py +22 -46
- treenode/models/proxy.py +43 -24
- treenode/templates/admin/tree_node_changelist.html +2 -0
- treenode/templates/admin/tree_node_import.html +27 -9
- treenode/templates/admin/tree_node_import_report.html +32 -0
- treenode/utils/__init__.py +4 -5
- treenode/utils/exporter.py +80 -27
- treenode/utils/importer.py +185 -152
- treenode/views.py +18 -12
- treenode/widgets.py +21 -5
- {django_fast_treenode-2.0.9.dist-info → django_fast_treenode-2.0.11.dist-info}/LICENSE +0 -0
- {django_fast_treenode-2.0.9.dist-info → django_fast_treenode-2.0.11.dist-info}/WHEEL +0 -0
- {django_fast_treenode-2.0.9.dist-info → django_fast_treenode-2.0.11.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: django-fast-treenode
|
3
|
-
Version: 2.0.
|
3
|
+
Version: 2.0.11
|
4
4
|
Summary: Application for supporting tree (hierarchical) data structure in Django projects
|
5
5
|
Home-page: https://github.com/TimurKady/django-fast-treenode
|
6
6
|
Author: Timur Kady
|
@@ -53,11 +53,12 @@ Description-Content-Type: text/markdown
|
|
53
53
|
License-File: LICENSE
|
54
54
|
Requires-Dist: Django>=4.0
|
55
55
|
Requires-Dist: pympler>=1.0
|
56
|
+
Requires-Dist: numpy>=2.0
|
56
57
|
Requires-Dist: django-widget-tweaks>=1.5
|
57
58
|
Provides-Extra: import-export
|
58
59
|
Requires-Dist: openpyxl; extra == "import-export"
|
59
60
|
Requires-Dist: pyyaml; extra == "import-export"
|
60
|
-
Requires-Dist:
|
61
|
+
Requires-Dist: xlsxwriter; extra == "import-export"
|
61
62
|
|
62
63
|
# Django-fast-treenode
|
63
64
|
__Combination of Adjacency List and Closure Table__
|
@@ -94,7 +95,7 @@ My idea was to solve these problems by combining the adjacency list with the Clo
|
|
94
95
|
* useful functionality added for some methods (e.g. the `include_self=False` and `depth` parameters has been added to functions that return lists/querysets);
|
95
96
|
* additionally, the package includes a tree view widget for the `tn_parent` field in the change form.
|
96
97
|
|
97
|
-
Of course, at large levels of nesting, the use of the Closure Table leads to an increase in resource costs.
|
98
|
+
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.
|
98
99
|
|
99
100
|
## Theory
|
100
101
|
You can get a basic understanding of what is a Closure Table from:
|
@@ -1,19 +1,19 @@
|
|
1
1
|
treenode/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
treenode/admin.py,sha256=
|
2
|
+
treenode/admin.py,sha256=fQCB9AOzplpWyfr1TYxUHbi7pCZmK2-tNeZMG90Gp7s,16391
|
3
3
|
treenode/apps.py,sha256=M0O9IKEnJZFfhfz12v4wksYJ-0ECyj1Cy3qXrfywos8,472
|
4
4
|
treenode/cache.py,sha256=Z_FpaS0vTKXqAI4n1QkZ7A_ILsLU3Q8rLgerA6pYyAA,7210
|
5
|
-
treenode/forms.py,sha256=
|
6
|
-
treenode/managers.py,sha256=
|
5
|
+
treenode/forms.py,sha256=nEeOtia1xhlthqAiowSp7DxuBSubyU3uJvKdbrkiRD0,4059
|
6
|
+
treenode/managers.py,sha256=BBhttJo6eODlPBEyf9t1DgSx9KVn4GiyLm6XMuYNEXE,18303
|
7
7
|
treenode/urls.py,sha256=7N0d4XiI6880sc8P89eWGr-ZjmOqPorA-fWfcnviqAM,876
|
8
8
|
treenode/version.py,sha256=-zaHoXRvTvJ0QzwA9ocYp7O38iBtIarACZbCNzwyc4s,222
|
9
|
-
treenode/views.py,sha256=
|
10
|
-
treenode/widgets.py,sha256=
|
11
|
-
treenode/docs/Documentation,sha256=
|
9
|
+
treenode/views.py,sha256=dqHrr89LunmLu3zJGY0fAXSjqbOzeUQdJ4OAoZt4Aio,3370
|
10
|
+
treenode/widgets.py,sha256=P8Xd3uzjilRU0ammsErHJSfZG-XXNMg_cJAfVCo5eOg,2700
|
11
|
+
treenode/docs/Documentation,sha256=5JwGCfQV4UmCKJzI3xF9yHER7wnqXMYNGg8jdRncsac,20245
|
12
12
|
treenode/models/__init__.py,sha256=gjDwVai0jf-l0hMaeeEBTYLR-DXkxUZMLUMGGs_tnuo,83
|
13
13
|
treenode/models/classproperty.py,sha256=IrwBWpmyjsAXpkpfDSOIMsnX6EMcbXql3mZjurHgRcw,556
|
14
|
-
treenode/models/closure.py,sha256=
|
14
|
+
treenode/models/closure.py,sha256=5vhi5HgeY9LhocyUsxMvchV90lgj6n3h4vSKQc28sFI,4510
|
15
15
|
treenode/models/factory.py,sha256=Wt1szWhbeICPwm0-RUy9p4VovcxltHECVxTSRyCQHc8,2100
|
16
|
-
treenode/models/proxy.py,sha256=
|
16
|
+
treenode/models/proxy.py,sha256=o0wU_7APj87zC5qWxRMCi9u_tbuT7zgHzax69qLDEd8,22479
|
17
17
|
treenode/static/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
18
18
|
treenode/static/treenode/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
19
19
|
treenode/static/treenode/css/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
@@ -25,17 +25,18 @@ treenode/static/treenode/js/treenode_admin.js,sha256=3fdvy1VoHb3rmzI19YXw4JPt6ZG
|
|
25
25
|
treenode/templates/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
26
26
|
treenode/templates/admin/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
27
27
|
treenode/templates/admin/export_success.html,sha256=xN2D-BCH249CJB10fo_vHYUyFenQ9mFKqq7UTWcrXS4,747
|
28
|
-
treenode/templates/admin/tree_node_changelist.html,sha256=
|
28
|
+
treenode/templates/admin/tree_node_changelist.html,sha256=NudAsaO6di_cDWQDewBe-1Bay61FdlGiEFzdvfP_Wk8,314
|
29
29
|
treenode/templates/admin/tree_node_export.html,sha256=vJxEoGI-US6VdFddxAFgL5r3MgGt6mgA43vltCsbA2k,1043
|
30
|
-
treenode/templates/admin/tree_node_import.html,sha256=
|
30
|
+
treenode/templates/admin/tree_node_import.html,sha256=unksxTAO2bJbxRkZfrCltHn61MgfqGt2sxIsUOW5dVk,1513
|
31
|
+
treenode/templates/admin/tree_node_import_report.html,sha256=azHJ8JFrSRu60lF1Uh22zs9JXQxZdvOjYdwCtlbaE3I,1133
|
31
32
|
treenode/templates/widgets/tree_widget.css,sha256=2bEaxu1x7QJZ7erbs2SLMaxeaiMkjQXadfcDEW8wfok,551
|
32
33
|
treenode/templates/widgets/tree_widget.html,sha256=GKcCU-B2FkkJ2BSOuXOw9e_PdYTtADcvyITEXqOlZ9Y,723
|
33
|
-
treenode/utils/__init__.py,sha256=
|
34
|
+
treenode/utils/__init__.py,sha256=_eKk3iiiyyk4GB5dupwJxl3RPWDEHZ1DW5vHteDrbVI,343
|
34
35
|
treenode/utils/base36.py,sha256=ydgu9hqDaK-WyS8zG-mtSWo7hJqbB4iHqkGz4-IVrb4,834
|
35
|
-
treenode/utils/exporter.py,sha256=
|
36
|
-
treenode/utils/importer.py,sha256=
|
37
|
-
django_fast_treenode-2.0.
|
38
|
-
django_fast_treenode-2.0.
|
39
|
-
django_fast_treenode-2.0.
|
40
|
-
django_fast_treenode-2.0.
|
41
|
-
django_fast_treenode-2.0.
|
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
CHANGED
@@ -6,7 +6,7 @@ This module provides Django admin integration for the TreeNode model.
|
|
6
6
|
It includes custom tree-based sorting, optimized queries, and
|
7
7
|
import/export functionality for hierarchical data structures.
|
8
8
|
|
9
|
-
Version: 2.0.
|
9
|
+
Version: 2.0.11
|
10
10
|
Author: Timur Kady
|
11
11
|
Email: kaduevtr@gmail.com
|
12
12
|
"""
|
@@ -14,16 +14,18 @@ Email: kaduevtr@gmail.com
|
|
14
14
|
|
15
15
|
import os
|
16
16
|
import importlib
|
17
|
+
import numpy as np
|
17
18
|
from datetime import datetime
|
18
19
|
from django.contrib import admin
|
20
|
+
from django.http import HttpResponseRedirect
|
19
21
|
from django.contrib.admin.views.main import ChangeList
|
20
22
|
from django.db import models
|
21
|
-
from django.db.models import Case, When, Value, IntegerField
|
22
23
|
from django.shortcuts import render, redirect
|
23
24
|
from django.urls import path
|
24
25
|
from django.utils.encoding import force_str
|
25
26
|
from django.utils.safestring import mark_safe
|
26
27
|
from django.utils.translation import gettext_lazy as _
|
28
|
+
from django.contrib import messages
|
27
29
|
|
28
30
|
from .forms import TreeNodeForm
|
29
31
|
from .widgets import TreeWidget
|
@@ -33,8 +35,8 @@ import logging
|
|
33
35
|
logger = logging.getLogger(__name__)
|
34
36
|
|
35
37
|
|
36
|
-
class
|
37
|
-
"""Custom ChangeList
|
38
|
+
class SortedChangeList(ChangeList):
|
39
|
+
"""Custom ChangeList that sorts results in Python (after DB query)."""
|
38
40
|
|
39
41
|
def get_ordering(self, request, queryset):
|
40
42
|
"""
|
@@ -44,28 +46,28 @@ class NoPkDescOrderedChangeList(ChangeList):
|
|
44
46
|
Django Admin sorts by `-pk` (descending) by default.
|
45
47
|
This method removes `-pk` so that objects are not sorted by ID.
|
46
48
|
"""
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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)
|
51
54
|
|
52
55
|
def get_queryset(self, request):
|
53
|
-
"""
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
).select_related('tn_parent')
|
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
|
+
]
|
69
71
|
|
70
72
|
|
71
73
|
class TreeNodeAdminModel(admin.ModelAdmin):
|
@@ -103,7 +105,7 @@ class TreeNodeAdminModel(admin.ModelAdmin):
|
|
103
105
|
)
|
104
106
|
|
105
107
|
def __init__(self, model, admin_site):
|
106
|
-
"""
|
108
|
+
"""Init method."""
|
107
109
|
super().__init__(model, admin_site)
|
108
110
|
|
109
111
|
# If `list_display` is empty, take all `fields`
|
@@ -111,10 +113,17 @@ class TreeNodeAdminModel(admin.ModelAdmin):
|
|
111
113
|
self.list_display = [field.name for field in model._meta.fields]
|
112
114
|
|
113
115
|
# Check for necessary dependencies
|
114
|
-
self.import_export = all(
|
116
|
+
self.import_export = all([
|
115
117
|
importlib.util.find_spec(pkg) is not None
|
116
|
-
for pkg in ["openpyxl", "
|
117
|
-
)
|
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.")
|
118
127
|
|
119
128
|
if self.import_export:
|
120
129
|
from .utils import TreeNodeImporter, TreeNodeExporter
|
@@ -126,35 +135,12 @@ class TreeNodeAdminModel(admin.ModelAdmin):
|
|
126
135
|
self.TreeNodeExporter = None
|
127
136
|
|
128
137
|
def get_queryset(self, request):
|
129
|
-
"""Override
|
138
|
+
"""Override get_queryset to simply return an optimized queryset."""
|
130
139
|
queryset = super().get_queryset(request)
|
131
|
-
|
132
|
-
|
133
|
-
if search_term:
|
134
|
-
"""
|
135
|
-
print(f"Поиск: {search_term}")
|
136
|
-
search_fields = self.get_search_fields(request)
|
137
|
-
print(f"Поиск по полям: {search_fields}")
|
138
|
-
|
139
|
-
q_objects = Q()
|
140
|
-
|
141
|
-
for field in search_fields:
|
142
|
-
q_objects |= Q(**{f"{field}__icontains": search_term})
|
143
|
-
|
144
|
-
|
145
|
-
queryset = queryset.filter(q_objects)
|
146
|
-
print(f"Найдено записей: {queryset.count()}")
|
147
|
-
"""
|
140
|
+
# If a search term is present, leave the queryset as is.
|
141
|
+
if request.GET.get("q"):
|
148
142
|
return queryset
|
149
|
-
|
150
|
-
node_list = sorted(queryset, key=lambda x: x.tn_order)
|
151
|
-
pk_list = [node.pk for node in node_list]
|
152
|
-
return queryset.filter(pk__in=pk_list).order_by(
|
153
|
-
Case(*[When(pk=pk, then=Value(index))
|
154
|
-
for index, pk in enumerate(pk_list)],
|
155
|
-
default=Value(len(pk_list)),
|
156
|
-
output_field=IntegerField())
|
157
|
-
)
|
143
|
+
return queryset.select_related('tn_parent')
|
158
144
|
|
159
145
|
def get_search_fields(self, request):
|
160
146
|
"""Return the correct search field."""
|
@@ -190,14 +176,39 @@ class TreeNodeAdminModel(admin.ModelAdmin):
|
|
190
176
|
return (treenode_field_display,) + tuple(base_list_display)
|
191
177
|
|
192
178
|
def get_changelist(self, request):
|
193
|
-
"""
|
194
|
-
return
|
179
|
+
"""Use SortedChangeList to sort the results at render time."""
|
180
|
+
return SortedChangeList
|
195
181
|
|
196
182
|
def changelist_view(self, request, extra_context=None):
|
197
183
|
"""Changelist View."""
|
198
184
|
extra_context = extra_context or {}
|
199
185
|
extra_context['import_export_enabled'] = self.import_export
|
200
|
-
|
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
|
201
212
|
|
202
213
|
def get_ordering(self, request):
|
203
214
|
"""Get Ordering."""
|
@@ -311,21 +322,34 @@ packages are not installed."
|
|
311
322
|
)
|
312
323
|
|
313
324
|
# Import data from file
|
314
|
-
importer = TreeNodeImporter(self.model, file, ext)
|
325
|
+
importer = self.TreeNodeImporter(self.model, file, ext)
|
315
326
|
raw_data = importer.import_data()
|
316
|
-
clean_result = importer.
|
317
|
-
|
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
|
+
|
318
333
|
if errors:
|
319
334
|
return render(
|
320
335
|
request,
|
321
|
-
"admin/
|
322
|
-
{
|
336
|
+
"admin/tree_node_import_report.html",
|
337
|
+
{
|
338
|
+
"errors": errors,
|
339
|
+
"created_count": created_count,
|
340
|
+
"updated_count": updated_count,
|
341
|
+
}
|
323
342
|
)
|
324
|
-
|
343
|
+
|
344
|
+
# If there are no errors, redirect to the list of objects with
|
345
|
+
# a message
|
346
|
+
messages.success(
|
325
347
|
request,
|
326
|
-
f"Successfully imported {
|
348
|
+
f"Successfully imported {created_count} records. "
|
349
|
+
f"Successfully updated {updated_count} records."
|
327
350
|
)
|
328
|
-
|
351
|
+
path = request.path.replace("import/", "") + "?import_done=1"
|
352
|
+
return redirect(path)
|
329
353
|
|
330
354
|
# If the request is not POST, simply display the import form
|
331
355
|
return render(request, "admin/tree_node_import.html")
|
@@ -355,14 +379,14 @@ packages are not installed."
|
|
355
379
|
if 'download' in request.GET:
|
356
380
|
# Get file format
|
357
381
|
export_format = request.GET.get('format', 'csv')
|
358
|
-
# Important: This QuerySet provides a convenient ("friendly") order
|
359
|
-
# of tree node output during export/import.
|
360
|
-
queryset = self.get_queryset()
|
361
382
|
# Filename
|
362
383
|
now = force_str(datetime.now().strftime("%Y-%m-%d %H-%M"))
|
363
384
|
filename = self.model._meta.label + " " + now
|
364
385
|
# Init
|
365
|
-
exporter = TreeNodeExporter(
|
386
|
+
exporter = self.TreeNodeExporter(
|
387
|
+
self.get_queryset(request),
|
388
|
+
filename=filename
|
389
|
+
)
|
366
390
|
# Export working
|
367
391
|
response = exporter.export(export_format)
|
368
392
|
logger.debug("DEBUG: File response generated.")
|
@@ -376,7 +400,7 @@ packages are not installed."
|
|
376
400
|
# If the format is specified, we try to perform a test export
|
377
401
|
# (without returning the file)
|
378
402
|
export_format = request.GET['format']
|
379
|
-
exporter = TreeNodeExporter(
|
403
|
+
exporter = self.TreeNodeExporter(
|
380
404
|
self.model.objects.all(),
|
381
405
|
filename=self.model._meta.model_name
|
382
406
|
)
|
treenode/docs/Documentation
CHANGED
@@ -33,7 +33,7 @@ My idea was to solve these problems by combining the adjacency list with the Clo
|
|
33
33
|
* useful functionality added for some methods (e.g. the `include_self=False` and `depth` parameters has been added to functions that return lists/querysets);
|
34
34
|
* additionally, the package includes a tree view widget for the `tn_parent` field in the change form.
|
35
35
|
|
36
|
-
Of course, at large levels of nesting, the use of the Closure Table leads to an increase in resource costs.
|
36
|
+
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.
|
37
37
|
|
38
38
|
## Theory
|
39
39
|
You can get a basic understanding of what is a Closure Table from:
|
@@ -128,37 +128,6 @@ class YoursForm(TreeNodeForm):
|
|
128
128
|
# Your code is here
|
129
129
|
```
|
130
130
|
|
131
|
-
## Updating to django-fast-treenode 2.X
|
132
|
-
### Overview
|
133
|
-
If you are upgrading from a previous version, you need to follow these steps to ensure compatibility and proper functionality.
|
134
|
-
|
135
|
-
### Update Process
|
136
|
-
1. **Upgrade the package**
|
137
|
-
Run the following command to install the latest version:
|
138
|
-
|
139
|
-
```bash
|
140
|
-
pip install --upgrade django-fast-treenode
|
141
|
-
```
|
142
|
-
|
143
|
-
2. **Apply database migrations**
|
144
|
-
After upgrading, you need to apply the necessary database migrations:
|
145
|
-
|
146
|
-
```bash
|
147
|
-
python manage.py makemigrations
|
148
|
-
python manage.py migrate
|
149
|
-
```
|
150
|
-
|
151
|
-
3. **Restart the application**
|
152
|
-
Finally, restart your Django application to apply all changes:
|
153
|
-
|
154
|
-
```bash
|
155
|
-
python manage.py runserver
|
156
|
-
```
|
157
|
-
|
158
|
-
**Important Notes**: Failing to apply migrations (`migrate`) after upgrading may lead to errors when interacting with tree nodes.
|
159
|
-
|
160
|
-
By following these steps, you will ensure a smooth transition to the latest version of django-fast-treenode without data inconsistencies.
|
161
|
-
|
162
131
|
|
163
132
|
## Usage
|
164
133
|
### Methods/Properties
|
@@ -578,7 +547,7 @@ obj.is_sibling_of(target_obj)
|
|
578
547
|
```
|
579
548
|
|
580
549
|
#### `update_tree`
|
581
|
-
**Update tree** manually
|
550
|
+
**Update tree** manually:
|
582
551
|
```python
|
583
552
|
cls.update_tree()
|
584
553
|
```
|
@@ -592,8 +561,9 @@ In v2.0, the caching mechanism has been improved to prevent excessive memory usa
|
|
592
561
|
``` python
|
593
562
|
TREENODE_CACHE_LIMIT = 100
|
594
563
|
```
|
595
|
-
**Automatic Management
|
596
|
-
|
564
|
+
**Automatic Management**. In most cases, users don’t need to manually manage cache operations.All methods that somehow change the state of models reset the tree cache automatically.
|
565
|
+
|
566
|
+
**Manual Cache Clearing**. If for some reason you need to reset the cache, you can do it in two ways:
|
597
567
|
- **Clear cache for a single model**: Use `clear_cache()` at the model level:
|
598
568
|
```python
|
599
569
|
MyTreeNodeModel.clear_cache()
|
@@ -637,6 +607,7 @@ python manage.py migrate
|
|
637
607
|
```
|
638
608
|
This will apply any necessary database changes automatically.
|
639
609
|
|
610
|
+
|
640
611
|
## To do
|
641
612
|
These improvements aim to enhance usability, performance, and maintainability for all users of `django-fast-treenode`:
|
642
613
|
* **Cache Algorithm Optimization**: Testing and integrating more advanced cache eviction strategies.
|
@@ -647,6 +618,7 @@ Your wishes, objections, comments are welcome.
|
|
647
618
|
|
648
619
|
|
649
620
|
# Django-fast-treenode
|
621
|
+
|
650
622
|
## License
|
651
623
|
Released under [MIT License](https://github.com/TimurKady/django-fast-treenode/blob/main/LICENSE).
|
652
624
|
|
treenode/forms.py
CHANGED
@@ -1,22 +1,65 @@
|
|
1
1
|
"""
|
2
2
|
TreeNode Form Module.
|
3
3
|
|
4
|
-
This module defines the TreeNodeForm class, which dynamically determines
|
5
|
-
|
6
|
-
|
4
|
+
This module defines the TreeNodeForm class, which dynamically determines
|
5
|
+
the TreeNode model.
|
6
|
+
It utilizes TreeWidget and automatically excludes the current node and its
|
7
|
+
descendants from the parent choices.
|
7
8
|
|
8
9
|
Functions:
|
9
10
|
- __init__: Initializes the form and filters out invalid parent choices.
|
10
11
|
- factory: Dynamically creates a form class for a given TreeNode model.
|
11
12
|
|
12
|
-
Version: 2.0.
|
13
|
+
Version: 2.0.11
|
13
14
|
Author: Timur Kady
|
14
15
|
Email: timurkady@yandex.com
|
15
16
|
"""
|
16
17
|
|
17
18
|
from django import forms
|
19
|
+
import numpy as np
|
20
|
+
from django.forms.models import ModelChoiceField, ModelChoiceIterator
|
21
|
+
from django.utils.translation import gettext_lazy as _
|
22
|
+
|
18
23
|
from .widgets import TreeWidget
|
19
|
-
|
24
|
+
|
25
|
+
|
26
|
+
class SortedModelChoiceIterator(ModelChoiceIterator):
|
27
|
+
"""Iterator Class for ModelChoiceField."""
|
28
|
+
|
29
|
+
def __iter__(self):
|
30
|
+
"""Return sorted choices based on tn_order."""
|
31
|
+
qs_list = list(self.queryset.all())
|
32
|
+
# Sort objects by their tn_order using NumPy.
|
33
|
+
tn_orders = np.array([obj.tn_order for obj in qs_list])
|
34
|
+
sorted_indices = np.argsort(tn_orders)
|
35
|
+
# Iterate over sorted indices and yield (value, label) pairs.
|
36
|
+
for idx in sorted_indices:
|
37
|
+
# Cast the index to int if it is numpy.int64.
|
38
|
+
obj = qs_list[int(idx)]
|
39
|
+
yield (
|
40
|
+
self.field.prepare_value(obj),
|
41
|
+
self.field.label_from_instance(obj)
|
42
|
+
)
|
43
|
+
|
44
|
+
|
45
|
+
class SortedModelChoiceField(ModelChoiceField):
|
46
|
+
"""ModelChoiceField Class for tn_paret field."""
|
47
|
+
|
48
|
+
to_field_name = None
|
49
|
+
|
50
|
+
def _get_choices(self):
|
51
|
+
if hasattr(self, '_choices'):
|
52
|
+
return self._choices
|
53
|
+
|
54
|
+
choices = list(SortedModelChoiceIterator(self))
|
55
|
+
if self.empty_label is not None:
|
56
|
+
choices.insert(0, ("", self.empty_label))
|
57
|
+
return choices
|
58
|
+
|
59
|
+
def _set_choices(self, value):
|
60
|
+
self._choices = value
|
61
|
+
|
62
|
+
choices = property(_get_choices, _set_choices)
|
20
63
|
|
21
64
|
|
22
65
|
class TreeNodeForm(forms.ModelForm):
|
@@ -40,28 +83,26 @@ class TreeNodeForm(forms.ModelForm):
|
|
40
83
|
"""Init Method."""
|
41
84
|
super().__init__(*args, **kwargs)
|
42
85
|
|
43
|
-
# Get the model from the form instance
|
44
|
-
# Use a model bound to a form
|
45
86
|
model = self._meta.model
|
46
87
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
default=Value(len(pk_list)),
|
60
|
-
output_field=IntegerField())
|
88
|
+
if "tn_parent" in self.fields:
|
89
|
+
self.fields["tn_parent"].required = False
|
90
|
+
self.fields["tn_parent"].empty_label = _("Root")
|
91
|
+
queryset = model.objects.all()
|
92
|
+
|
93
|
+
original_field = self.fields["tn_parent"]
|
94
|
+
self.fields["tn_parent"] = SortedModelChoiceField(
|
95
|
+
queryset=queryset,
|
96
|
+
label=original_field.label,
|
97
|
+
widget=original_field.widget,
|
98
|
+
empty_label=original_field.empty_label,
|
99
|
+
required=False
|
61
100
|
)
|
101
|
+
self.fields["tn_parent"].widget.model = queryset.model
|
62
102
|
|
63
|
-
#
|
64
|
-
self.
|
103
|
+
# Если есть текущее значение, устанавливаем его
|
104
|
+
if self.instance and self.instance.pk and self.instance.tn_parent:
|
105
|
+
self.fields["tn_parent"].initial = self.instance.tn_parent
|
65
106
|
|
66
107
|
@classmethod
|
67
108
|
def factory(cls, model):
|