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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: django-fast-treenode
3
- Version: 2.0.10
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. 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.
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=VkGuqc8KI1_SPgvM6yynyfYLEmZRfgMm2M5d9OoDxZE,14720
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=KJOVqIhMt8Z3J38LTl1exO2JyS46mZKzkQJZI-Uem9Q,3573
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=N026nzqY62FfP7DsCH-I4j_HLGbbsrJQrQ_-rAUUboA,5361
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
@@ -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=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
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=hCTPnSdFdoE_s7s_iW1Xy6598fWQY5htJzPA0DpnG5s,6199
36
- treenode/utils/importer.py,sha256=RYtl0CHfH1tZjCbhrFf0-Qp-wm4QJp-DD5Y5nyLK7yQ,12643
37
- django_fast_treenode-2.0.10.dist-info/LICENSE,sha256=GiiEe4Y9oOCbn9eGuNew1mMYHU_bJWaCK9zOusnKvvU,1091
38
- django_fast_treenode-2.0.10.dist-info/METADATA,sha256=kQgDgrx9hH6bdz0wwbUZ4VxUQIxZLxFd3CPbvRmmhi0,23274
39
- django_fast_treenode-2.0.10.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
40
- django_fast_treenode-2.0.10.dist-info/top_level.txt,sha256=fmgxHbXyx1O2MPi_9kjx8aL9L-8TmV0gre4Go8XgqFk,9
41
- django_fast_treenode-2.0.10.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.10
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 = [result_list[int(i)] for i in np.argsort(tn_orders)]
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
- """Динамически добавляем поле `tn_order` в `list_display`."""
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"] if importlib.util.find_spec(pkg) is not None
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
- 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
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.clean(raw_data)
298
- 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
+
299
333
  if errors:
300
334
  return render(
301
335
  request,
302
- "admin/tree_node_import.html",
303
- {"errors": errors}
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
- self.message_user(
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 {len(clean_result['create'])} records."
348
+ f"Successfully imported {created_count} records. "
349
+ f"Successfully updated {updated_count} records."
308
350
  )
309
- return redirect("..")
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
@@ -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,15 +1,16 @@
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.10
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
- return SortedModelChoiceIterator(self)
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 and self.instance.pk:
84
- excluded_ids = [self.instance.pk] + \
85
- list(self.instance.get_descendants_pks())
86
- queryset = model.objects.exclude(pk__in=excluded_ids)
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=self.fields["tn_parent"].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):