django-fast-treenode 2.0.10__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.10.dist-info → django_fast_treenode-2.0.11.dist-info}/METADATA +2 -2
- {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.0.11.dist-info}/RECORD +17 -16
- treenode/admin.py +56 -13
- treenode/docs/Documentation +7 -35
- treenode/forms.py +27 -14
- treenode/managers.py +306 -168
- treenode/models/closure.py +21 -46
- treenode/models/proxy.py +43 -24
- treenode/templates/admin/tree_node_import.html +27 -9
- treenode/templates/admin/tree_node_import_report.html +32 -0
- treenode/utils/exporter.py +61 -36
- treenode/utils/importer.py +169 -161
- treenode/views.py +18 -12
- treenode/widgets.py +21 -5
- {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.0.11.dist-info}/LICENSE +0 -0
- {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.0.11.dist-info}/WHEEL +0 -0
- {django_fast_treenode-2.0.10.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
|
@@ -95,7 +95,7 @@ My idea was to solve these problems by combining the adjacency list with the Clo
|
|
95
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);
|
96
96
|
* additionally, the package includes a tree view widget for the `tn_parent` field in the change form.
|
97
97
|
|
98
|
-
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.
|
99
99
|
|
100
100
|
## Theory
|
101
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
|
@@ -27,15 +27,16 @@ treenode/templates/admin/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPj
|
|
27
27
|
treenode/templates/admin/export_success.html,sha256=xN2D-BCH249CJB10fo_vHYUyFenQ9mFKqq7UTWcrXS4,747
|
28
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
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
|
"""
|
@@ -17,6 +17,7 @@ import importlib
|
|
17
17
|
import numpy as np
|
18
18
|
from datetime import datetime
|
19
19
|
from django.contrib import admin
|
20
|
+
from django.http import HttpResponseRedirect
|
20
21
|
from django.contrib.admin.views.main import ChangeList
|
21
22
|
from django.db import models
|
22
23
|
from django.shortcuts import render, redirect
|
@@ -24,6 +25,7 @@ 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
|
@@ -63,7 +65,9 @@ class SortedChangeList(ChangeList):
|
|
63
65
|
tn_orders = np.array([obj.tn_order for obj in result_list])
|
64
66
|
# Get sorted indices based on tn_order (ascending order).
|
65
67
|
# Reorder the original result_list based on the sorted indices.
|
66
|
-
self.result_list = [
|
68
|
+
self.result_list = [
|
69
|
+
result_list[int(i)] for i in np.argsort(tn_orders)
|
70
|
+
]
|
67
71
|
|
68
72
|
|
69
73
|
class TreeNodeAdminModel(admin.ModelAdmin):
|
@@ -101,7 +105,7 @@ class TreeNodeAdminModel(admin.ModelAdmin):
|
|
101
105
|
)
|
102
106
|
|
103
107
|
def __init__(self, model, admin_site):
|
104
|
-
"""
|
108
|
+
"""Init method."""
|
105
109
|
super().__init__(model, admin_site)
|
106
110
|
|
107
111
|
# If `list_display` is empty, take all `fields`
|
@@ -115,7 +119,8 @@ class TreeNodeAdminModel(admin.ModelAdmin):
|
|
115
119
|
])
|
116
120
|
if not self.import_export:
|
117
121
|
check_results = [
|
118
|
-
pkg for pkg in ["openpyxl", "pyyaml", "xlsxwriter"]
|
122
|
+
pkg for pkg in ["openpyxl", "pyyaml", "xlsxwriter"]
|
123
|
+
if importlib.util.find_spec(pkg) is not None
|
119
124
|
]
|
120
125
|
logger.info("Packages" + ", ".join(check_results) + " are \
|
121
126
|
not installed. Export and import functions are disabled.")
|
@@ -178,7 +183,32 @@ not installed. Export and import functions are disabled.")
|
|
178
183
|
"""Changelist View."""
|
179
184
|
extra_context = extra_context or {}
|
180
185
|
extra_context['import_export_enabled'] = self.import_export
|
181
|
-
|
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
|
182
212
|
|
183
213
|
def get_ordering(self, request):
|
184
214
|
"""Get Ordering."""
|
@@ -294,19 +324,32 @@ packages are not installed."
|
|
294
324
|
# Import data from file
|
295
325
|
importer = self.TreeNodeImporter(self.model, file, ext)
|
296
326
|
raw_data = importer.import_data()
|
297
|
-
clean_result = importer.
|
298
|
-
|
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
|
+
|
299
333
|
if errors:
|
300
334
|
return render(
|
301
335
|
request,
|
302
|
-
"admin/
|
303
|
-
{
|
336
|
+
"admin/tree_node_import_report.html",
|
337
|
+
{
|
338
|
+
"errors": errors,
|
339
|
+
"created_count": created_count,
|
340
|
+
"updated_count": updated_count,
|
341
|
+
}
|
304
342
|
)
|
305
|
-
|
343
|
+
|
344
|
+
# If there are no errors, redirect to the list of objects with
|
345
|
+
# a message
|
346
|
+
messages.success(
|
306
347
|
request,
|
307
|
-
f"Successfully imported {
|
348
|
+
f"Successfully imported {created_count} records. "
|
349
|
+
f"Successfully updated {updated_count} records."
|
308
350
|
)
|
309
|
-
|
351
|
+
path = request.path.replace("import/", "") + "?import_done=1"
|
352
|
+
return redirect(path)
|
310
353
|
|
311
354
|
# If the request is not POST, simply display the import form
|
312
355
|
return render(request, "admin/tree_node_import.html")
|
@@ -341,7 +384,7 @@ packages are not installed."
|
|
341
384
|
filename = self.model._meta.label + " " + now
|
342
385
|
# Init
|
343
386
|
exporter = self.TreeNodeExporter(
|
344
|
-
self.get_queryset(),
|
387
|
+
self.get_queryset(request),
|
345
388
|
filename=filename
|
346
389
|
)
|
347
390
|
# Export working
|
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,15 +1,16 @@
|
|
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
|
"""
|
@@ -17,6 +18,7 @@ Email: timurkady@yandex.com
|
|
17
18
|
from django import forms
|
18
19
|
import numpy as np
|
19
20
|
from django.forms.models import ModelChoiceField, ModelChoiceIterator
|
21
|
+
from django.utils.translation import gettext_lazy as _
|
20
22
|
|
21
23
|
from .widgets import TreeWidget
|
22
24
|
|
@@ -43,14 +45,18 @@ class SortedModelChoiceIterator(ModelChoiceIterator):
|
|
43
45
|
class SortedModelChoiceField(ModelChoiceField):
|
44
46
|
"""ModelChoiceField Class for tn_paret field."""
|
45
47
|
|
48
|
+
to_field_name = None
|
49
|
+
|
46
50
|
def _get_choices(self):
|
47
|
-
"""Get sorted choices."""
|
48
51
|
if hasattr(self, '_choices'):
|
49
52
|
return self._choices
|
50
|
-
|
53
|
+
|
54
|
+
choices = list(SortedModelChoiceIterator(self))
|
55
|
+
if self.empty_label is not None:
|
56
|
+
choices.insert(0, ("", self.empty_label))
|
57
|
+
return choices
|
51
58
|
|
52
59
|
def _set_choices(self, value):
|
53
|
-
"""Set choices."""
|
54
60
|
self._choices = value
|
55
61
|
|
56
62
|
choices = property(_get_choices, _set_choices)
|
@@ -77,19 +83,26 @@ class TreeNodeForm(forms.ModelForm):
|
|
77
83
|
"""Init Method."""
|
78
84
|
super().__init__(*args, **kwargs)
|
79
85
|
|
80
|
-
# Use a model bound to a form
|
81
86
|
model = self._meta.model
|
82
87
|
|
83
|
-
if "tn_parent" in self.fields
|
84
|
-
|
85
|
-
|
86
|
-
queryset = model.objects.
|
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
|
+
|
87
93
|
original_field = self.fields["tn_parent"]
|
88
94
|
self.fields["tn_parent"] = SortedModelChoiceField(
|
89
95
|
queryset=queryset,
|
90
|
-
label=
|
91
|
-
widget=original_field.widget
|
96
|
+
label=original_field.label,
|
97
|
+
widget=original_field.widget,
|
98
|
+
empty_label=original_field.empty_label,
|
99
|
+
required=False
|
92
100
|
)
|
101
|
+
self.fields["tn_parent"].widget.model = queryset.model
|
102
|
+
|
103
|
+
# Если есть текущее значение, устанавливаем его
|
104
|
+
if self.instance and self.instance.pk and self.instance.tn_parent:
|
105
|
+
self.fields["tn_parent"].initial = self.instance.tn_parent
|
93
106
|
|
94
107
|
@classmethod
|
95
108
|
def factory(cls, model):
|