django-fast-treenode 2.1.4__py3-none-any.whl → 3.0.1__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.
Files changed (107) hide show
  1. django_fast_treenode-3.0.1.dist-info/METADATA +203 -0
  2. django_fast_treenode-3.0.1.dist-info/RECORD +90 -0
  3. {django_fast_treenode-2.1.4.dist-info → django_fast_treenode-3.0.1.dist-info}/WHEEL +1 -1
  4. treenode/admin/__init__.py +2 -7
  5. treenode/admin/admin.py +138 -209
  6. treenode/admin/changelist.py +21 -39
  7. treenode/admin/exporter.py +170 -0
  8. treenode/admin/importer.py +171 -0
  9. treenode/admin/mixin.py +291 -0
  10. treenode/apps.py +41 -19
  11. treenode/cache.py +192 -303
  12. treenode/forms.py +45 -65
  13. treenode/managers/__init__.py +4 -20
  14. treenode/managers/managers.py +216 -0
  15. treenode/managers/queries.py +233 -0
  16. treenode/managers/tasks.py +167 -0
  17. treenode/models/__init__.py +8 -5
  18. treenode/models/decorators.py +54 -0
  19. treenode/models/factory.py +44 -68
  20. treenode/models/mixins/__init__.py +2 -1
  21. treenode/models/mixins/ancestors.py +44 -20
  22. treenode/models/mixins/children.py +33 -26
  23. treenode/models/mixins/descendants.py +33 -22
  24. treenode/models/mixins/family.py +25 -15
  25. treenode/models/mixins/logical.py +23 -21
  26. treenode/models/mixins/node.py +162 -104
  27. treenode/models/mixins/properties.py +22 -16
  28. treenode/models/mixins/roots.py +59 -15
  29. treenode/models/mixins/siblings.py +46 -43
  30. treenode/models/mixins/tree.py +212 -153
  31. treenode/models/mixins/update.py +154 -0
  32. treenode/models/models.py +365 -0
  33. treenode/settings.py +28 -0
  34. treenode/static/{treenode/css → css}/tree_widget.css +1 -1
  35. treenode/static/{treenode/css → css}/treenode_admin.css +43 -2
  36. treenode/static/css/treenode_tabs.css +51 -0
  37. treenode/static/js/lz-string.min.js +1 -0
  38. treenode/static/{treenode/js → js}/tree_widget.js +9 -23
  39. treenode/static/js/treenode_admin.js +531 -0
  40. treenode/static/vendors/jquery-ui/AUTHORS.txt +384 -0
  41. treenode/static/vendors/jquery-ui/LICENSE.txt +43 -0
  42. treenode/static/vendors/jquery-ui/external/jquery/jquery.js +10716 -0
  43. treenode/static/vendors/jquery-ui/images/ui-icons_444444_256x240.png +0 -0
  44. treenode/static/vendors/jquery-ui/images/ui-icons_555555_256x240.png +0 -0
  45. treenode/static/vendors/jquery-ui/images/ui-icons_777620_256x240.png +0 -0
  46. treenode/static/vendors/jquery-ui/images/ui-icons_777777_256x240.png +0 -0
  47. treenode/static/vendors/jquery-ui/images/ui-icons_cc0000_256x240.png +0 -0
  48. treenode/static/vendors/jquery-ui/images/ui-icons_ffffff_256x240.png +0 -0
  49. treenode/static/vendors/jquery-ui/index.html +297 -0
  50. treenode/static/vendors/jquery-ui/jquery-ui.css +438 -0
  51. treenode/static/vendors/jquery-ui/jquery-ui.js +5223 -0
  52. treenode/static/vendors/jquery-ui/jquery-ui.min.css +7 -0
  53. treenode/static/vendors/jquery-ui/jquery-ui.min.js +6 -0
  54. treenode/static/vendors/jquery-ui/jquery-ui.structure.css +16 -0
  55. treenode/static/vendors/jquery-ui/jquery-ui.structure.min.css +5 -0
  56. treenode/static/vendors/jquery-ui/jquery-ui.theme.css +439 -0
  57. treenode/static/vendors/jquery-ui/jquery-ui.theme.min.css +5 -0
  58. treenode/static/vendors/jquery-ui/package.json +82 -0
  59. treenode/templates/admin/treenode_changelist.html +25 -0
  60. treenode/templates/admin/treenode_import_export.html +85 -0
  61. treenode/templates/admin/treenode_rows.html +57 -0
  62. treenode/tests.py +3 -0
  63. treenode/urls.py +6 -27
  64. treenode/utils/__init__.py +0 -15
  65. treenode/utils/db/__init__.py +7 -0
  66. treenode/utils/db/compiler.py +114 -0
  67. treenode/utils/db/db_vendor.py +50 -0
  68. treenode/utils/db/service.py +84 -0
  69. treenode/utils/db/sqlcompat.py +60 -0
  70. treenode/utils/db/sqlquery.py +70 -0
  71. treenode/version.py +2 -2
  72. treenode/views/__init__.py +5 -0
  73. treenode/views/autoapi.py +91 -0
  74. treenode/views/autocomplete.py +52 -0
  75. treenode/views/children.py +41 -0
  76. treenode/views/common.py +23 -0
  77. treenode/views/crud.py +209 -0
  78. treenode/views/search.py +48 -0
  79. treenode/widgets.py +27 -44
  80. django_fast_treenode-2.1.4.dist-info/METADATA +0 -166
  81. django_fast_treenode-2.1.4.dist-info/RECORD +0 -63
  82. treenode/admin/mixins.py +0 -302
  83. treenode/managers/adjacency.py +0 -205
  84. treenode/managers/closure.py +0 -278
  85. treenode/models/adjacency.py +0 -342
  86. treenode/models/classproperty.py +0 -27
  87. treenode/models/closure.py +0 -122
  88. treenode/static/treenode/js/.gitkeep +0 -1
  89. treenode/static/treenode/js/treenode_admin.js +0 -131
  90. treenode/templates/admin/export_success.html +0 -26
  91. treenode/templates/admin/tree_node_changelist.html +0 -19
  92. treenode/templates/admin/tree_node_export.html +0 -27
  93. treenode/templates/admin/tree_node_import.html +0 -45
  94. treenode/templates/admin/tree_node_import_report.html +0 -32
  95. treenode/templates/widgets/tree_widget.css +0 -23
  96. treenode/utils/aid.py +0 -46
  97. treenode/utils/base16.py +0 -38
  98. treenode/utils/base36.py +0 -37
  99. treenode/utils/db.py +0 -116
  100. treenode/utils/exporter.py +0 -196
  101. treenode/utils/importer.py +0 -328
  102. treenode/utils/radix.py +0 -61
  103. treenode/views.py +0 -184
  104. {django_fast_treenode-2.1.4.dist-info → django_fast_treenode-3.0.1.dist-info/licenses}/LICENSE +0 -0
  105. {django_fast_treenode-2.1.4.dist-info → django_fast_treenode-3.0.1.dist-info}/top_level.txt +0 -0
  106. /treenode/static/{treenode → css}/.gitkeep +0 -0
  107. /treenode/static/{treenode/css → js}/.gitkeep +0 -0
treenode/admin/admin.py CHANGED
@@ -1,202 +1,190 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """
3
- TreeNode Admin Module
3
+ TreeNode Admin Model Class
4
4
 
5
- This module provides Django admin integration for the TreeNode model.
6
- It includes custom tree-based sorting, optimized queries, and
7
- import/export functionality for hierarchical data structures.
8
-
9
- Version: 2.1.0
5
+ Version: 3.0.0
10
6
  Author: Timur Kady
11
- Email: kaduevtr@gmail.com
7
+ Email: timurkady@yandex.com
12
8
  """
13
9
 
14
10
 
15
- import importlib
16
11
  from django.contrib import admin
17
12
  from django.db import models
18
13
  from django.http import HttpResponseRedirect
14
+ from django.urls import reverse
19
15
  from django.utils.safestring import mark_safe
20
- from django.urls import reverse, resolve
16
+ from django.utils.translation import gettext_lazy as _
21
17
 
22
- from .changelist import SortedChangeList
23
- from .mixins import AdminMixin
18
+ from .changelist import TreeNodeChangeList
19
+ from .mixin import AdminMixin
24
20
  from ..forms import TreeNodeForm
25
21
  from ..widgets import TreeWidget
22
+ from .importer import TreeNodeImporter
23
+ from .exporter import TreeNodeExporter
26
24
 
27
25
  import logging
28
26
 
29
27
  logger = logging.getLogger(__name__)
30
28
 
31
29
 
32
- class TreeNodeAdminModel(AdminMixin, admin.ModelAdmin):
33
- """
34
- TreeNodeAdmin class.
35
-
36
- Admin configuration for TreeNodeModel with import/export functionality.
37
- """
30
+ class TreeNodeModelAdmin(AdminMixin, admin.ModelAdmin):
31
+ """Admin for TreeNodeModel."""
38
32
 
33
+ # Режимы отображения
39
34
  TREENODE_DISPLAY_MODE_ACCORDION = 'accordion'
40
35
  TREENODE_DISPLAY_MODE_BREADCRUMBS = 'breadcrumbs'
41
36
  TREENODE_DISPLAY_MODE_INDENTATION = 'indentation'
42
-
43
37
  treenode_display_mode = TREENODE_DISPLAY_MODE_ACCORDION
44
- import_export = False # Track import/export availability
45
- change_list_template = "admin/tree_node_changelist.html"
38
+
39
+ form = TreeNodeForm
40
+ importer_class = None
41
+ exporter_class = None
46
42
  ordering = []
47
43
  list_per_page = 1000
48
- list_sorting_mode_session_key = "treenode_sorting_mode"
49
44
 
50
- form = TreeNodeForm
51
45
  formfield_overrides = {
52
- models.ForeignKey: {"widget": TreeWidget()},
46
+ models.ForeignKey: {'widget': TreeWidget()},
53
47
  }
54
48
 
49
+ change_list_template = "admin/treenode_changelist.html"
50
+
55
51
  class Media:
56
- """Include custom CSS and JavaScript for admin interface."""
52
+ """Meta Class."""
57
53
 
58
54
  css = {"all": (
59
- "treenode/css/treenode_admin.css",
55
+ "css/treenode_admin.css",
56
+ "vendors/jquery-ui/jquery-ui.css",
60
57
  )}
61
58
  js = (
62
- 'admin/js/jquery.init.js',
63
- 'treenode/js/treenode_admin.js',
59
+ "vendors/jquery-ui/jquery-ui.js",
60
+ # "js/lz-string.min.js",
61
+ "js/treenode_admin.js",
64
62
  )
65
63
 
64
+ def __init__(self, model, admin_site):
65
+ """Init method."""
66
+ super().__init__(model, admin_site)
67
+
68
+ if not self.list_display:
69
+ self.list_display = [field.name for field in model._meta.fields]
70
+
71
+ self.TreeNodeImporter = self.importer_class or TreeNodeImporter
72
+ self.TreeNodeExporter = self.exporter_class or TreeNodeExporter
73
+
66
74
  def drag(self, obj):
67
- """Display an empty column with an icon for future drag-and-drop."""
68
- icon = "" #  "
69
- return mark_safe(f'<span class="treenode-drag-handle">{icon}</span>')
75
+ """Drag and drop сolumn."""
76
+ return mark_safe('<span class="treenode-drag-handle">&#9776;</span>')
70
77
 
71
- drag.short_description = ""
78
+ drag.short_description = _("Move")
72
79
 
73
80
  def toggle(self, obj):
74
- """Добавление кнопки для открытия/закрытия поддерева, если есть дети."""
75
- icon = "►" # ➕➖
81
+ """Toggle column."""
76
82
  if obj.get_children_count() > 0:
77
83
  return mark_safe(
78
- f'<button class="treenode-toggle" '
79
- f'data-node-id="{obj.pk}">'
80
- f'{icon}'
81
- f'</button>')
84
+ f'<button class="treenode-toggle" data-node-id="{obj.pk}">►</button>' # noqa
85
+ )
82
86
  return mark_safe('<div class="treenode-space">&nbsp;</div>')
83
87
 
84
- toggle.short_description = ""
88
+ toggle.short_description = _("Expand")
85
89
 
86
- def __init__(self, model, admin_site):
87
- """Init method."""
88
- super().__init__(model, admin_site)
90
+ def _get_treenode_field_display(self, request, obj):
91
+ """Return HTML for the tree node in list view."""
92
+ level = obj.get_depth()
93
+ edit_url = reverse(
94
+ f"admin:{obj._meta.app_label}_{obj._meta.model_name}_change",
95
+ args=[obj.pk]
96
+ )
89
97
 
90
- # If `list_display` is empty, take all `fields`
91
- if not self.list_display:
92
- self.list_display = [field.name for field in model._meta.fields]
98
+ if self.treenode_display_mode == self.TREENODE_DISPLAY_MODE_ACCORDION:
99
+ icon = "📄 " if obj.is_leaf() else "📁 "
100
+ content = (
101
+ f'<span style="padding-left: {level * 1.5}em;">'
102
+ f'{icon}<a href="{edit_url}">{str(obj)}</a></span>'
103
+ )
104
+ elif self.treenode_display_mode == self.TREENODE_DISPLAY_MODE_BREADCRUMBS: # noqa
105
+ breadcrumbs = obj.get_breadcrumbs(
106
+ attr=getattr(obj, 'treenode_display_field', 'id'))
107
+ content = " / ".join(map(str, breadcrumbs))
108
+ elif self.treenode_display_mode == self.TREENODE_DISPLAY_MODE_INDENTATION: # noqa
109
+ indent = "&mdash;" * level
110
+ content = f'{indent}<a href="{edit_url}">{str(obj)}</a>'
111
+ else:
112
+ content = f'<a href="{edit_url}">{str(obj)}</a>'
113
+
114
+ html = (
115
+ f'<div class="treenode-wrapper" '
116
+ f'data-treenode-pk="{obj.pk}" '
117
+ f'data-treenode-depth="{level}" '
118
+ f'data-treenode-parent="{obj.parent_id or ""}">'
119
+ f'<span class="treenode-content">{content}</span>'
120
+ f'</div>'
121
+ )
122
+ return mark_safe(html)
123
+
124
+ def get_list_display(self, request):
125
+ """Generate list_display dynamically with tree-aware columns."""
126
+ # Define callable that replaces display field with tree field
127
+ def treenode_field(obj):
128
+ return self._get_treenode_field_display(request, obj)
129
+ treenode_field.short_description = self.model._meta.verbose_name
130
+
131
+ display_field = getattr(self.model, 'display_field', '__str__')
93
132
 
94
- # Check for necessary dependencies
95
- self.import_export = all([
96
- importlib.util.find_spec(pkg) is not None
97
- for pkg in ["openpyxl", "yaml", "xlsxwriter"]
98
- ])
99
- if not self.import_export:
100
- check_results = [
101
- pkg for pkg in ["openpyxl", "pyyaml", "xlsxwriter"]
102
- if importlib.util.find_spec(pkg) is not None
103
- ]
104
- logger.info("Packages" + ", ".join(check_results) + " are \
105
- not installed. Export and import functions are disabled.")
106
-
107
- if self.import_export:
108
- from ..utils import TreeNodeImporter, TreeNodeExporter
109
-
110
- self.TreeNodeImporter = TreeNodeImporter
111
- self.TreeNodeExporter = TreeNodeExporter
133
+ user_list_display = list(super().get_list_display(request))
134
+ # If the list is empty or only contains __str__, replace it entirely
135
+ if not user_list_display or user_list_display == ['__str__']:
136
+ user_list_display = [treenode_field]
112
137
  else:
113
- self.TreeNodeImporter = None
114
- self.TreeNodeExporter = None
138
+ try:
139
+ pos = user_list_display.index(display_field)
140
+ user_list_display.pop(pos)
141
+ user_list_display.insert(pos, treenode_field)
142
+ except ValueError:
143
+ user_list_display.insert(0, treenode_field)
115
144
 
116
- def get_queryset(self, request):
117
- """
118
- Get QuerySet.
145
+ return (self.drag, self.toggle) + tuple(user_list_display)
146
+
147
+ def get_list_display_links(self, request, list_display):
148
+ """Get display list links."""
149
+ return ('treenode_field',)
119
150
 
120
- Redefine the query so that by default only root nodes (nodes with
121
- tn_parent=None) are loaded. If there is a search query (parameter "q"),
122
- return the full list.
123
- """
151
+ def get_queryset(self, request):
152
+ """By default: only root nodes, unless searching or editing."""
124
153
  qs = super().get_queryset(request)
125
154
 
126
- resolved_match = resolve(request.path)
127
- app_label = self.model._meta.app_label
128
- model_name = self.model._meta.model_name
129
- if resolved_match.url_name == f"{app_label}_{model_name}_change":
155
+ if request.GET.get("q"):
130
156
  return qs
131
157
 
132
- if not request.GET.get("q"):
133
- qs = qs.filter(tn_parent__isnull=True)
158
+ resolved = request.resolver_match
159
+ if resolved and resolved.url_name.endswith("_change"):
160
+ return qs
134
161
 
135
- field_name = getattr(self.model, 'treenode_display_field')
136
- q = request.GET.get("q", "")
137
- if not field_name:
138
- return qs.none()
139
- return qs.select_related('tn_parent')\
140
- .filter(**{f"{field_name}__icontains": q})
162
+ return qs.filter(parent__isnull=True)
141
163
 
142
164
  def get_form(self, request, obj=None, **kwargs):
143
165
  """Get Form method."""
144
166
  form = super().get_form(request, obj, **kwargs)
145
- if "tn_parent" in form.base_fields:
146
- form.base_fields["tn_parent"].widget = TreeWidget()
167
+ if "parent" in form.base_fields:
168
+ form.base_fields["parent"].widget = TreeWidget()
147
169
  return form
148
170
 
149
171
  def get_search_fields(self, request):
150
- """Return the correct search field."""
151
- return [self.model.treenode_display_field]
152
-
153
- def get_list_display(self, request):
154
- """Generate list_display dynamically with user-defined preferences."""
155
- change_view_cols = (self.drag, self.toggle)
156
- user_list_display = list(super().get_list_display(request))
157
-
158
- treenode_display_field = getattr(
159
- self.model,
160
- 'treenode_display_field',
161
- '__str__'
162
- )
163
-
164
- def treenode_field(obj):
165
- return self._get_treenode_field_display(request, obj)
166
-
167
- treenode_field.short_description = self.model._meta.verbose_name
172
+ """Get search fields."""
173
+ return [getattr(self.model, 'treenode_display_field', 'id')]
168
174
 
169
- # If the custom list is empty or contains only '__str__'
170
- if not user_list_display or user_list_display == ['__str__']:
171
- result = (treenode_field,)
172
- else:
173
- # Remove `treenode_display_field` if it is the first one and
174
- # insert `treenode_field`
175
- if user_list_display[0] == treenode_display_field:
176
- clean_list = user_list_display[1:]
177
- else:
178
- clean_list = user_list_display
179
-
180
- # Гарантируем, что treenode_field есть в списке
181
- if treenode_field not in clean_list:
182
- clean_list.insert(0, treenode_field)
183
-
184
- result = tuple(clean_list)
175
+ def get_changelist(self, request, **kwargs):
176
+ """Get ChangeList Class."""
177
+ return TreeNodeChangeList
185
178
 
186
- return change_view_cols + result
187
-
188
- def get_list_display_links(self, request, list_display):
189
- """Specify that only `treenode_field` should be clickable."""
190
- return ('treenode_field',)
191
-
192
- def get_changelist(self, request):
193
- """Use SortedChangeList to sort the results at render time."""
194
- return SortedChangeList
179
+ def get_ordering(self, request):
180
+ """Get ordering."""
181
+ return None
195
182
 
196
183
  def changelist_view(self, request, extra_context=None):
197
184
  """Changelist View."""
198
185
  extra_context = extra_context or {}
199
186
  extra_context['import_export_enabled'] = self.import_export
187
+ extra_context['num_sorted_fields'] = len(self.get_ordering(request) or []) # noqa: D501
200
188
 
201
189
  response = super().changelist_view(request, extra_context=extra_context)
202
190
 
@@ -205,91 +193,32 @@ not installed. Export and import functions are disabled.")
205
193
  if isinstance(response, HttpResponseRedirect):
206
194
  return response
207
195
 
208
- if request.GET.get("import_done"):
209
- # Create a ChangeList instance manually
210
- ChangeListClass = self.get_changelist(request)
211
-
212
- cl = ChangeListClass(
213
- request, self.model, self.list_display, self.list_display_links,
214
- self.list_filter, self.date_hierarchy, self.search_fields,
215
- self.list_select_related, self.list_per_page,
216
- self.list_max_show_all, self.list_editable, self
217
- )
218
-
219
- # Force queryset update and apply sorting
220
- cl.get_queryset(request)
221
- cl.get_results(request)
196
+ ChangeListClass = self.get_changelist(request)
197
+ cl = ChangeListClass(
198
+ request,
199
+ self.model,
200
+ self.list_display,
201
+ self.list_display_links,
202
+ self.list_filter,
203
+ self.date_hierarchy,
204
+ self.search_fields,
205
+ self.list_select_related,
206
+ self.list_per_page,
207
+ self.list_max_show_all,
208
+ self.list_editable,
209
+ self,
210
+ self.get_sortable_by(request),
211
+ self.get_search_help_text(request),
212
+ )
222
213
 
223
- # Add updated ChangeList to context
224
- response.context_data["cl"] = cl
214
+ cl.get_results(request)
215
+ cl.result_list = self.render_changelist_rows(cl.result_list, request)
225
216
 
226
217
  return response
227
218
 
228
- def get_ordering(self, request):
229
- """Get Ordering."""
230
- return None
231
-
232
- # ------------------------------------------------------------------------
233
-
234
- def _get_treenode_field_display(self, request, obj):
235
- """
236
- Return the HTML display of the accordion node.
237
-
238
- Modes:
239
- - ACCORDION: '&nbsp;' * level + icon + str(node),
240
- where icon = "📄" if obj.is_leaf() returns True, otherwise "📁".
241
- - BREADCRUMBS: " / ".join(obj.get_breadcrumbs(attr=field)),
242
- where field = getattr(self.model, 'treenode_display_field', None)
243
- or "tn_priority" if None.
244
- - INDENTATION: '&mdash;' * level + str(node)
245
- """
246
- # Get a link to edit the object
247
- meta = self.model._meta
248
- edit_url = reverse(
249
- f'admin:{meta.app_label}_{meta.model_name}_change', args=[obj.pk]
250
- )
251
-
252
- # Determine the node level
253
- level = obj.get_depth()
254
-
255
- mode = self.treenode_display_mode
256
- if mode == self.TREENODE_DISPLAY_MODE_ACCORDION:
257
- icon = "📄 " if obj.is_leaf() else "📁 "
258
- obj_str = str(obj)
259
- content = (
260
- f'<span style="padding-left: {level * 1.5}em;">'
261
- f'{icon}<a href="{edit_url}">{obj_str}</a>'
262
- f'</span>'
263
- )
264
- elif mode == self.TREENODE_DISPLAY_MODE_BREADCRUMBS:
265
- field = getattr(
266
- self.model,
267
- 'treenode_display_field',
268
- None) or "tn_priority"
269
- content = " / ".join(obj.get_breadcrumbs(attr=field))
270
- elif mode == self.TREENODE_DISPLAY_MODE_INDENTATION:
271
- indent = "&mdash;" * level
272
- obj_str = str(obj)
273
- content = f'{indent}<a href="{edit_url}">{obj_str}</a>'
274
- else:
275
- # Just in case mode is not recognized, then use breadcrumbs
276
- field = getattr(
277
- self.model,
278
- 'treenode_display_field',
279
- None) or "tn_priority"
280
- content = " / ".join(obj.get_breadcrumbs(attr=field))
281
- content = f'<a href="{edit_url}"">{content}</a>'
282
-
283
- parent = str(getattr(obj, "tn_parent_id", "") or "")
284
- html = (
285
- f'<div class="treenode-wrapper" '
286
- f'data-treenode-pk="{obj.pk}" '
287
- f'data-treenode-depth="{level}" '
288
- f'data-treenode-parent="{parent}">'
289
- f'<span class="treenode-content">{content}</span>'
290
- f'</div>'
291
- )
292
- return mark_safe(html)
219
+ def get_list_per_page(self, request):
220
+ """Get list per page."""
221
+ return 999999
293
222
 
294
223
 
295
224
  # The End
@@ -1,20 +1,20 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """
3
- TreeNode Sorted ChangeList Class for TreeNodeAdminModel.
3
+ TreeNode Sorted ChangeList Class for TreeNodeModelAdmin.
4
4
 
5
- Version: 2.1.0
5
+ Version: 3.0.0
6
6
  Author: Timur Kady
7
- Email: kaduevtr@gmail.com
7
+ Email: timurkady@yandex.com
8
8
  """
9
9
 
10
10
  from django.contrib.admin.views.main import ChangeList
11
- from django.core.serializers import serialize, deserialize
11
+ from django.forms.models import modelformset_factory
12
+ from django.db.models import Q
12
13
 
13
- from ..cache import treenode_cache
14
14
 
15
-
16
- class SortedChangeList(ChangeList):
17
- """Custom ChangeList that sorts results in Python (after DB query)."""
15
+ class TreeNodeChangeList(ChangeList):
16
+ def __init__(self, *args, **kwargs):
17
+ super().__init__(*args, **kwargs)
18
18
 
19
19
  def get_ordering(self, request, queryset):
20
20
  """
@@ -30,36 +30,18 @@ class SortedChangeList(ChangeList):
30
30
  ordering.remove('-pk')
31
31
  return tuple(ordering)
32
32
 
33
- def get_queryset(self, request):
34
- """Get QuerySet with select_related."""
35
- return super().get_queryset(request).select_related('tn_parent')
36
-
37
33
  def get_results(self, request):
38
- """Return sorted results for ChangeList rendering."""
39
- # Populate self.result_list with objects from the DB.
40
34
  super().get_results(request)
41
- result_list = self.result_list
42
- result_list_pks = ",".join(map(str, [obj.pk for obj in result_list]))
43
-
44
- cache_key = treenode_cache.generate_cache_key(
45
- self.model._meta.label,
46
- self.get_results.__name__,
47
- id(self.__class__),
48
- result_list_pks
49
- )
50
-
51
- json_str = treenode_cache.get(cache_key)
52
- if json_str:
53
- sorted_results = [
54
- obj.object for obj in deserialize("json", json_str)
55
- ]
56
- self.result_list = sorted_results
57
- return
58
-
59
- sorted_result = self.model._sort_node_list(result_list)
60
- json_str = serialize("json", sorted_result)
61
- treenode_cache.set(cache_key, json_str)
62
-
63
- self.result_list = sorted_result
64
-
65
- # The End
35
+ model_name = self.model._meta.model_name
36
+
37
+ # Добавляем атрибуты к результатам
38
+ object_ids = [r.pk for r in self.result_list]
39
+ objects_dict = {
40
+ obj.pk: obj
41
+ for obj in self.model_admin.model.objects.filter(pk__in=object_ids)
42
+ }
43
+
44
+ for result in self.result_list:
45
+ result.obj = objects_dict.get(result.pk)
46
+ # Добавляем атрибуты строк
47
+ result.row_attrs = f'data-node-id="{result.pk}" data-parent-of="{result.obj.parent_id or ""}" class="model-{model_name} pk-{result.pk}"'