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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: django-fast-treenode
3
- Version: 2.0.9
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: pandas; extra == "import-export"
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. But at the same time, the combined scheme still generally outperforms the original application in terms of performance.
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=N7WdbFT2hTM3mp4YrfgSVkLGFHXruUqATn-OPMKDajg,15154
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=imoLzr7qFlOBdeMQK12rAFp6hINpe0PS_U4-V8RRcDU,2802
6
- treenode/managers.py,sha256=7z8GU64A2_jEonJyQDTyIpdOocaBbM352DkwZTHjdQk,10828
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=vBDIxmYXBwmJxRnInvUVGy75FQmt-XN_HnEaKIu-fVs,3365
10
- treenode/widgets.py,sha256=4Q6WlPPT5fggEuTXiZ_Z40pjb46CylSp28pa0xBT_Ps,2079
11
- treenode/docs/Documentation,sha256=6USAESU8MuY1vlj95yY8S6T0o_0RsktGv6S_0SdSkwk,21030
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=chTa9T9yC3H_2LaZXR7KLdNh1pc50iUolut5GB4Bm-Q,5297
14
+ treenode/models/closure.py,sha256=5vhi5HgeY9LhocyUsxMvchV90lgj6n3h4vSKQc28sFI,4510
15
15
  treenode/models/factory.py,sha256=Wt1szWhbeICPwm0-RUy9p4VovcxltHECVxTSRyCQHc8,2100
16
- treenode/models/proxy.py,sha256=6BFElk_NL1ARTEAikOOfMneUK5wEjofNnfXQWFSZUsA,21766
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=ykksipIBMy2LvH6UqPuM-y00UlXAjsypMHqiMm_KlTg,261
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=ZFoveJ08X99EGiwFCfQowXI9oS9VgcFtRLYVDIWq-Fg,969
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=CWXGaFcNI7nEApZeMzLxx8P7qT8G2EterlV-j6ivNbc,317
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=l-IEsk2fK8xe-E-Hi7SMOV7ZIwxM6mZih5-0rhvsx9E,5396
36
- treenode/utils/importer.py,sha256=n23PBFPXN-6S_Dl8Qmx56sp0eLy_lWfpxrzJqwygQf0,14190
37
- django_fast_treenode-2.0.9.dist-info/LICENSE,sha256=GiiEe4Y9oOCbn9eGuNew1mMYHU_bJWaCK9zOusnKvvU,1091
38
- django_fast_treenode-2.0.9.dist-info/METADATA,sha256=8VpCVo5aaG7VirWvqv0cflloIZaSM2xGYQhb6C9VCCQ,23242
39
- django_fast_treenode-2.0.9.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
40
- django_fast_treenode-2.0.9.dist-info/top_level.txt,sha256=fmgxHbXyx1O2MPi_9kjx8aL9L-8TmV0gre4Go8XgqFk,9
41
- django_fast_treenode-2.0.9.dist-info/RECORD,,
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.6
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 NoPkDescOrderedChangeList(ChangeList):
37
- """Custom ChangeList to remove descending sorting `pk` (default)."""
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
- rv = list(super().get_ordering(request, queryset))
48
- if '-pk' in rv:
49
- rv.remove('-pk')
50
- return tuple(rv)
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
- Override QuerySet.
55
-
56
- Overrides data selection to optimize queries. Also adds
57
- `select_related('tn_parent')` to avoid N+1 queries.
58
- """
59
- queryset = super(NoPkDescOrderedChangeList, self).get_queryset(request)
60
- node_list = sorted(queryset, key=lambda x: x.tn_order)
61
- pk_list = [node.pk for node in node_list]
62
-
63
- return queryset.filter(pk__in=pk_list).order_by(
64
- Case(*[When(pk=pk, then=Value(index))
65
- for index, pk in enumerate(pk_list)],
66
- default=Value(len(pk_list)),
67
- output_field=IntegerField())
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
- """Динамически добавляем поле `tn_order` в `list_display`."""
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", "pyyaml", "pandas"]
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 get_ueryset()."""
138
+ """Override get_queryset to simply return an optimized queryset."""
130
139
  queryset = super().get_queryset(request)
131
-
132
- search_term = request.GET.get("q")
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
- """Get ChangeList."""
194
- return NoPkDescOrderedChangeList
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
- return super().changelist_view(request, extra_context=extra_context)
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.clean(raw_data)
317
- errors = importer.finalize_import(clean_result)
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/tree_node_import.html",
322
- {"errors": errors}
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
- self.message_user(
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 {len(clean_result['create'])} records."
348
+ f"Successfully imported {created_count} records. "
349
+ f"Successfully updated {updated_count} records."
327
350
  )
328
- return redirect("..")
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(queryset, filename=filename)
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
  )
@@ -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. But at the same time, the combined scheme still generally outperforms the original application in terms of performance.
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, useful after **bulk updates**:
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**: In most cases, users don’t need to manually manage cache operations.
596
- **Manual Cache Clearing**:
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 the TreeNode model.
5
- It utilizes TreeWidget and automatically excludes the current node and its descendants
6
- from the parent choices.
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.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
- from django.db.models import Case, When, Value, IntegerField
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
- # Проверяем наличие tn_parent и исключаем текущий узел и его потомков
48
- if "tn_parent" in self.fields and self.instance.pk:
49
- excluded_ids = [self.instance.pk] + list(
50
- self.instance.get_descendants_pks())
51
-
52
- # Sort by tn_order
53
- queryset = model.objects.exclude(pk__in=excluded_ids)
54
- node_list = sorted(queryset, key=lambda x: x.tn_order)
55
- pk_list = [node.pk for node in node_list]
56
- queryset = queryset.filter(pk__in=pk_list).order_by(
57
- Case(*[When(pk=pk, then=Value(index))
58
- for index, pk in enumerate(pk_list)],
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
- # Set QuerySet
64
- self.fields["tn_parent"].queryset = queryset
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):