django-fast-treenode 3.0.7__py3-none-any.whl → 3.2.0__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 (56) hide show
  1. {django_fast_treenode-3.0.7.dist-info → django_fast_treenode-3.2.0.dist-info}/METADATA +3 -1
  2. django_fast_treenode-3.2.0.dist-info/RECORD +97 -0
  3. treenode/admin/admin.py +109 -131
  4. treenode/admin/mixin.py +6 -6
  5. treenode/managers/managers.py +1 -1
  6. treenode/managers/queries.py +1 -1
  7. treenode/managers/tasks.py +1 -1
  8. treenode/models/mixins/node.py +4 -4
  9. treenode/models/models.py +14 -9
  10. treenode/settings.py +4 -1
  11. treenode/static/treenode/.gitkeep +0 -0
  12. treenode/static/{css → treenode/css}/treenode_admin.css +21 -0
  13. treenode/static/treenode/js/treenode_admin.js +322 -0
  14. treenode/static/treenode/vendors/.gitkeep +0 -0
  15. treenode/static/treenode/vendors/jquery-ui/.gitkeep +0 -0
  16. treenode/templates/treenode/admin/treenode_changelist.html +61 -15
  17. treenode/templates/treenode/admin/treenode_import_export.html +3 -2
  18. treenode/templates/treenode/admin/treenode_rows.html +37 -40
  19. treenode/templatetags/treenode_admin.py +57 -14
  20. treenode/utils/jwt_auth.py +25 -0
  21. treenode/version.py +2 -2
  22. treenode/views/autoapi.py +5 -1
  23. treenode/views/autocomplete.py +52 -52
  24. treenode/views/children.py +41 -41
  25. treenode/views/common.py +23 -23
  26. treenode/widgets.py +2 -2
  27. django_fast_treenode-3.0.7.dist-info/RECORD +0 -93
  28. treenode/static/js/treenode_admin.js +0 -531
  29. {django_fast_treenode-3.0.7.dist-info → django_fast_treenode-3.2.0.dist-info}/WHEEL +0 -0
  30. {django_fast_treenode-3.0.7.dist-info → django_fast_treenode-3.2.0.dist-info}/licenses/LICENSE +0 -0
  31. {django_fast_treenode-3.0.7.dist-info → django_fast_treenode-3.2.0.dist-info}/top_level.txt +0 -0
  32. /treenode/static/{css → treenode/css}/.gitkeep +0 -0
  33. /treenode/static/{css → treenode/css}/tree_widget.css +0 -0
  34. /treenode/static/{css → treenode/css}/treenode_tabs.css +0 -0
  35. /treenode/static/{js → treenode/js}/.gitkeep +0 -0
  36. /treenode/static/{js → treenode/js}/lz-string.min.js +0 -0
  37. /treenode/static/{js → treenode/js}/tree_widget.js +0 -0
  38. /treenode/static/{vendors → treenode/vendors}/jquery-ui/AUTHORS.txt +0 -0
  39. /treenode/static/{vendors → treenode/vendors}/jquery-ui/LICENSE.txt +0 -0
  40. /treenode/static/{vendors → treenode/vendors}/jquery-ui/external/jquery/jquery.js +0 -0
  41. /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_444444_256x240.png +0 -0
  42. /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_555555_256x240.png +0 -0
  43. /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_777620_256x240.png +0 -0
  44. /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_777777_256x240.png +0 -0
  45. /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_cc0000_256x240.png +0 -0
  46. /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_ffffff_256x240.png +0 -0
  47. /treenode/static/{vendors → treenode/vendors}/jquery-ui/index.html +0 -0
  48. /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.css +0 -0
  49. /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.js +0 -0
  50. /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.min.css +0 -0
  51. /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.min.js +0 -0
  52. /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.structure.css +0 -0
  53. /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.structure.min.css +0 -0
  54. /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.theme.css +0 -0
  55. /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.theme.min.css +0 -0
  56. /treenode/static/{vendors → treenode/vendors}/jquery-ui/package.json +0 -0
@@ -0,0 +1,322 @@
1
+ /**
2
+ * treenode_admin.js
3
+ *
4
+ * Cleaned version 3.1.0 — TreeNode Admin extension for Django Admin.
5
+ * - No AJAX loading of children
6
+ * - No persistence in localStorage
7
+ * - Local-only expand/collapse logic
8
+ * - Compatible with pre-rendered full tree
9
+ *
10
+ * Version: 3.1.0
11
+ * Author: Timur Kady
12
+ * Email: timurkady@yandex.com
13
+ */
14
+
15
+ (function($) {
16
+
17
+ // ------------------------------- //
18
+ // Helpers //
19
+ // ------------------------------- //
20
+
21
+ function getCookie(name) {
22
+ let cookieValue = null;
23
+ if (document.cookie && document.cookie !== '') {
24
+ const cookies = document.cookie.split(';');
25
+ for (let i = 0; i < cookies.length; i++) {
26
+ const cookie = cookies[i].trim();
27
+ if (cookie.startsWith(name + '=')) {
28
+ cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
29
+ break;
30
+ }
31
+ }
32
+ }
33
+ return cookieValue;
34
+ }
35
+
36
+ // ------------------------------- //
37
+ // AJAX Setup //
38
+ // ------------------------------- //
39
+
40
+ const csrftoken = getCookie('csrftoken');
41
+
42
+ $.ajaxSetup({
43
+ beforeSend: function(xhr, settings) {
44
+ if (!/^https?:.*/.test(settings.url)) {
45
+ xhr.setRequestHeader("X-CSRFToken", csrftoken);
46
+ }
47
+ $('body').css('cursor', 'wait');
48
+ },
49
+ complete: function() {
50
+ $('body').css('cursor', 'default');
51
+ },
52
+ cache: false
53
+ });
54
+
55
+ // ------------------------------- //
56
+ // Visual feedback //
57
+ // ------------------------------- //
58
+
59
+ const TreeFx = {
60
+ flashInsert(nodeId) {
61
+ const $row = $(`tr[data-node-id="${nodeId}"]`);
62
+ if (!$row.length) return;
63
+
64
+ $row.addClass("flash-insert");
65
+ setTimeout(() => $row.removeClass("flash-insert"), 1000);
66
+ },
67
+
68
+ markDragging($item, enable) {
69
+ $item.toggleClass('dragging', enable);
70
+ }
71
+ };
72
+
73
+ // ------------------------------- //
74
+ // Tree logic //
75
+ // ------------------------------- //
76
+
77
+ var ChangeList = {
78
+ $tableBody: null,
79
+ isShiftPressed: false,
80
+ activeTargetRow: null,
81
+ isMoving: false,
82
+
83
+ init: function() {
84
+ this.$tableBody = $('table#result_list tbody');
85
+ this.bindEvents();
86
+ this.enableDragAndDrop();
87
+ },
88
+
89
+ // Save expanded nodes to localStorage
90
+ saveTree: function() {
91
+ if (!this.$tableBody) return;
92
+
93
+ // Сохраняем ТОЛЬКО те node_id, которые раскрыты
94
+ this.expandedNodes = this.$tableBody.find(".treenode-toggle").map(function() {
95
+ const $btn = $(this);
96
+ return $btn.data("expanded") ? $btn.data("node-id") : null;
97
+ }).get().filter(Boolean);
98
+
99
+ if (this.expandedNodes.length === 0) {
100
+ localStorage.removeItem("treenode_expanded");
101
+ } else {
102
+ localStorage.setItem("treenode_expanded", JSON.stringify(this.expandedNodes));
103
+ }
104
+
105
+ const count = $("#result_list tbody tr").length;
106
+ if (count > 0) {
107
+ $("p.paginator").first().text(`${count} ${ChangeList.label}`);
108
+ localStorage.setItem("label", ChangeList.label);
109
+ }
110
+ },
111
+
112
+
113
+ // Restore expanded nodes from localStorage
114
+ restoreTreeState: function() {
115
+ const expanded = JSON.parse(localStorage.getItem("treenode_expanded") || "[]");
116
+ for (const nodeId of expanded) {
117
+ ChangeList.expandNode(nodeId);
118
+ }
119
+ },
120
+
121
+ expandNode: function(nodeId) {
122
+ const $row = this.$tableBody.find(`tr[data-node-id="${nodeId}"]`);
123
+ const $btn = $row.find(".treenode-toggle");
124
+ $btn.text("▼").data("expanded", true);
125
+ this.showChildren(nodeId);
126
+ this.saveTree();
127
+ },
128
+
129
+ collapseNode: function(nodeId) {
130
+ const $row = this.$tableBody.find(`tr[data-node-id="${nodeId}"]`);
131
+ const $btn = $row.find(".treenode-toggle");
132
+ $btn.text("►").data("expanded", false); // <-- ВАЖНО!
133
+ this.hideChildrenRecursive(nodeId);
134
+ this.saveTree();
135
+ },
136
+
137
+ toggleNode: function($btn) {
138
+ const nodeId = $btn.data("node-id");
139
+ const isExpanded = $btn.data("expanded");
140
+ if (isExpanded) {
141
+ this.collapseNode(nodeId);
142
+ } else {
143
+ this.expandNode(nodeId);
144
+ }
145
+ },
146
+
147
+ showChildren: function(parentId) {
148
+ const $children = this.$tableBody.find(`tr[data-parent-id="${parentId}"]`);
149
+ $children.removeClass("treenode-hidden");
150
+
151
+ $children.each((_, child) => {
152
+ const $child = $(child);
153
+ const childId = $child.data("node-id");
154
+ const $toggle = $child.find(".treenode-toggle");
155
+ if ($toggle.data("expanded")) {
156
+ this.showChildren(childId);
157
+ }
158
+ });
159
+ },
160
+
161
+ hideChildrenRecursive: function(parentId) {
162
+ const $children = this.$tableBody.find(`tr[data-parent-id="${parentId}"]`);
163
+ $children.addClass("treenode-hidden");
164
+
165
+ $children.each((_, child) => {
166
+ const childId = $(child).data("node-id");
167
+ this.hideChildrenRecursive(childId);
168
+ });
169
+ },
170
+
171
+ expandAll: function() {
172
+ const self = this;
173
+ this.$tableBody.find("button.treenode-toggle").each(function() {
174
+ self.expandNode($(this).data("node-id"));
175
+ });
176
+ },
177
+
178
+ collapseAll: function() {
179
+ const self = this;
180
+ this.$tableBody.find("button.treenode-toggle").each(function() {
181
+ self.collapseNode($(this).data("node-id"));
182
+ });
183
+ },
184
+
185
+ bindEvents: function() {
186
+ const self = this;
187
+
188
+ $(document).on("click", "button.treenode-toggle", function(e) {
189
+ e.preventDefault();
190
+ self.toggleNode($(this));
191
+ });
192
+
193
+ $(document).on("click", ".treenode-expand-all", function() {
194
+ self.expandAll();
195
+ });
196
+
197
+ $(document).on("click", ".treenode-collapse-all", function() {
198
+ self.collapseAll();
199
+ });
200
+
201
+ $(document).on("keydown", function(e) {
202
+ if (e.key === "Shift") {
203
+ self.isShiftPressed = true;
204
+ self.updateDndHighlight();
205
+ }
206
+ }).on("keyup", function(e) {
207
+ if (e.key === "Shift") {
208
+ self.isShiftPressed = false;
209
+ self.updateDndHighlight();
210
+ }
211
+ });
212
+
213
+ $('#result_list').on('change', '#action-toggle', function() {
214
+ $('input.action-select').prop('checked', this.checked);
215
+ });
216
+ },
217
+
218
+ enableDragAndDrop: function() {
219
+ const self = this;
220
+
221
+ this.$tableBody.sortable({
222
+ items: "tr",
223
+ handle: ".treenode-drag-handle",
224
+ placeholder: "treenode-placeholder",
225
+ helper: function(e, tr) {
226
+ const $originals = tr.children();
227
+ const $helper = tr.clone();
228
+ $helper.find('[id]').removeAttr('id');
229
+ $helper.children().each(function(index) {
230
+ $(this).width($originals.eq(index).width());
231
+ });
232
+ return $helper;
233
+ },
234
+ start: function(e, ui) {
235
+ TreeFx.markDragging(ui.item, true);
236
+ },
237
+ over: function(e, ui) {
238
+ self.updateDndHighlight();
239
+ },
240
+ stop: function(e, ui) {
241
+ TreeFx.markDragging(ui.item, false);
242
+ const $item = ui.item;
243
+ const nodeId = $item.data("node-id");
244
+ const prevId = $item.prev().data("node-id") || null;
245
+ const mode = self.isShiftPressed ? 'child' : 'after';
246
+
247
+ if (self.activeTargetRow) {
248
+ self.activeTargetRow.removeClass("target-as-child");
249
+ self.activeTargetRow = null;
250
+ }
251
+
252
+ self.applyMove(nodeId, prevId, mode);
253
+ TreeFx.flashInsert(nodeId);
254
+ },
255
+ });
256
+ },
257
+
258
+ updateDndHighlight: function() {
259
+ const $placeholder = this.$tableBody.find("tr.treenode-placeholder");
260
+ const $target = $placeholder.prev();
261
+
262
+ if (!$target.length || !$target.data("node-id")) {
263
+ this.$tableBody.find("tr.target-as-child").removeClass("target-as-child");
264
+ this.activeTargetRow = null;
265
+ return;
266
+ }
267
+
268
+ if (this.isShiftPressed) {
269
+ if (!this.activeTargetRow || !this.activeTargetRow.is($target)) {
270
+ this.$tableBody.find("tr.target-as-child").removeClass("target-as-child");
271
+ $target.addClass("target-as-child");
272
+ this.activeTargetRow = $target;
273
+ }
274
+ } else {
275
+ if (this.activeTargetRow) {
276
+ this.activeTargetRow.removeClass("target-as-child");
277
+ this.activeTargetRow = null;
278
+ }
279
+ }
280
+ },
281
+
282
+ applyMove: function(nodeId, targetId, mode) {
283
+ if (this.isMoving) return;
284
+
285
+ this.isMoving = true;
286
+ this.activeTargetRow = null;
287
+
288
+ const params = {
289
+ node_id: nodeId,
290
+ target_id: targetId,
291
+ mode: mode
292
+ };
293
+
294
+ $.ajax({
295
+ url: 'move/',
296
+ method: 'POST',
297
+ data: params,
298
+ dataType: 'json',
299
+ success: function(data) {
300
+ const msg = data.message || "Node moved successfully.";
301
+ $("<li class='success'>" + msg + "</li>").appendTo(".messagelist");
302
+ },
303
+ error: function(xhr, status, error) {
304
+ const fallback = "Error moving node.";
305
+ $("<li class='error'>" + (xhr.responseText || fallback) + "</li>").appendTo(".messagelist");
306
+ },
307
+ complete: function() {
308
+ ChangeList.isMoving = false;
309
+ location.reload();
310
+ }
311
+ });
312
+ }
313
+ };
314
+
315
+ $(document).ready(function () {
316
+ if ($("table#result_list").length) {
317
+ ChangeList.init();
318
+ ChangeList.restoreTreeState();
319
+ }
320
+ });
321
+
322
+ })(django.jQuery || window.jQuery);
File without changes
File without changes
@@ -2,24 +2,70 @@
2
2
  {% load admin_list %}
3
3
  {% load i18n %}
4
4
  {% load treenode_admin %}
5
+ {% load static %}
5
6
 
6
- {% block object-tools-items %}
7
- {{ block.super }}
8
- {% if import_export_enabled %}
9
- <li><a href="import/" class="button">Import</a></li>
10
- <li><a href="export/" class="button">Export</a></li>
7
+ {% block content %}
8
+
9
+ {% block object-tools %}
10
+ {% if has_add_permission %}
11
+ <ul class="object-tools">
12
+ {% block object-tools-items %}
13
+ {{ block.super }}
14
+ <li><a href="import/" class="button">Import</a></li>
15
+ <li><a href="export/" class="button">Export</a></li>
16
+ {% endblock %}
17
+ </ul>
11
18
  {% endif %}
12
- {% endblock %}
19
+ {% endblock %}
20
+
21
+ <div id="changelist" class="module filtered">
22
+ <div class="changelist-form-container">
23
+
24
+ {# Search bar + expand/collapse buttons #}
25
+ {% block search %}
26
+ <div id="toolbar">
27
+ <form id="changelist-search" method="get" role="search">
28
+ <label for="searchbar">
29
+ <img src="{% static 'admin/img/search.svg' %}" alt="{% translate 'Search' %}">
30
+ </label>
31
+ <input type="text" size="40" name="q" value="{{ cl.query }}" id="searchbar">
32
+ <input type="submit" value="{% translate 'Search' %}">
33
+ <button type="button" class="button treenode-button treenode-expand-all">
34
+ {% trans "Expand All" %}
35
+ </button>
36
+ <button type="button" class="button treenode-button treenode-collapse-all">
37
+ {% trans "Collapse All" %}
38
+ </button>
39
+ </form>
40
+ </div>
41
+ {% endblock %}
42
+
43
+ {# Form with list and actions #}
44
+ <form id="changelist-form" method="post" {% if cl.formset or action_form %} enctype="multipart/form-data"{% endif %}>
45
+ {% csrf_token %}
13
46
 
14
- {% block result_list %}
15
- {% if action_form and actions_on_top and cl.show_admin_actions %}
16
- {% admin_actions %}
17
- {% endif %}
47
+ {% if action_form and actions_on_top and cl.show_admin_actions %}
48
+ {% admin_actions %}
49
+ {% endif %}
18
50
 
19
- {% tree_result_list cl %}
51
+ {% block result_list %}
52
+ {% tree_result_list cl %}
53
+ {% endblock %}
20
54
 
21
- {% if action_form and actions_on_bottom and cl.show_admin_actions %}
22
- {% admin_actions %}
23
- {% endif %}
55
+ {% if action_form and actions_on_bottom and cl.show_admin_actions %}
56
+ {% admin_actions %}
57
+ {% endif %}
58
+
59
+ {% block pagination %}
60
+ {{ block.super }}
61
+ {% endblock %}
62
+ </form>
63
+ </div>
64
+
65
+ {# 📎 Боковая панель фильтров #}
66
+ {% block filters %}
67
+ {{ block.super }}
68
+ {% endblock %}
69
+ </div>
70
+ {% endblock %}
24
71
 
25
- {% endblock %}
@@ -3,8 +3,9 @@
3
3
 
4
4
  {% block extrastyle %}
5
5
  {{ block.super }}
6
- <link rel="stylesheet" href="{% static 'admin/css/forms.css' %}">
7
- <link rel="stylesheet" href="{% static 'css/treenode_tabs.css' %}">
6
+ {# Load built-in admin form styles #}
7
+ <link rel="stylesheet" href="{% static '/admin/css/forms.css' %}">
8
+ <link rel="stylesheet" href="{% static 'treenode/css/treenode_tabs.css' %}">
8
9
  {% endblock %}
9
10
 
10
11
  {% block content %}
@@ -2,49 +2,45 @@
2
2
 
3
3
  <div class="results">
4
4
  <table id="result_list">
5
-
6
-
7
- <thead>
8
- <tr>
9
- {% for h in headers %}
10
- <th scope="col"{{ h.class_attrib|safe }}>
11
- {% if "action-checkbox-column" in h.class_attrib %}
12
- <div class="text">
13
- <span>
14
- <input type="checkbox" id="action-toggle" aria-label="{% translate 'Select all objects on this page for an action' %}">
15
- </span>
16
- </div>
17
- <div class="clear"></div>
18
- {% else %}
19
- {% if h.sortable %}
20
- <div class="sortoptions">
21
- {% if h.url_remove %}
22
- <a class="sortremove" href="{{ h.url_remove }}" title="{% translate 'Remove from sorting' %}"></a>
23
- {% endif %}
24
- {% if h.url_toggle %}
25
- <a href="{{ h.url_toggle }}" class="toggle {% if h.ascending %}ascending{% else %}descending{% endif %}" title="{% translate 'Toggle sorting' %}"></a>
26
- {% endif %}
27
- </div>
28
- {% else %}
29
- <div class="sortoptions"></div>
30
- {% endif %}
31
-
32
- <div class="text">
33
- {% if h.sortable %}
34
- <a href="{{ h.url_primary }}">{{ h.text|safe }}</a>
5
+ <thead>
6
+ <tr>
7
+ {% for h in headers %}
8
+ <th scope="col"{{ h.class_attrib|safe }}>
9
+ {% if "action-checkbox-column" in h.class_attrib %}
10
+ <div class="text">
11
+ <span>
12
+ <input type="checkbox" id="action-toggle" aria-label="{% translate 'Select all objects on this page for an action' %}">
13
+ </span>
14
+ </div>
15
+ <div class="clear"></div>
35
16
  {% else %}
36
- <a href="#">{{ h.text|safe }}</a>
17
+ {% if h.sortable %}
18
+ <div class="sortoptions">
19
+ {% if h.url_remove %}
20
+ <a class="sortremove" href="{{ h.url_remove }}" title="{% translate 'Remove from sorting' %}"></a>
21
+ {% endif %}
22
+ {% if h.url_toggle %}
23
+ <a href="{{ h.url_toggle }}" class="toggle {% if h.ascending %}ascending{% else %}descending{% endif %}" title="{% translate 'Toggle sorting' %}"></a>
24
+ {% endif %}
25
+ </div>
26
+ {% else %}
27
+ <div class="sortoptions"></div>
28
+ {% endif %}
29
+ <div class="text">
30
+ {% if h.sortable %}
31
+ <a href="{{ h.url_primary }}">{{ h.text|safe }}</a>
32
+ {% else %}
33
+ <a href="#">{{ h.text|safe }}</a>
34
+ {% endif %}
35
+ </div>
36
+ <div class="clear"></div>
37
37
  {% endif %}
38
- </div>
39
- <div class="clear"></div>
40
- {% endif %}
41
- </th>
42
- {% endfor %}
43
- </tr>
44
- </thead>
38
+ </th>
39
+ {% endfor %}
40
+ </tr>
41
+ </thead>
45
42
 
46
-
47
- <tbody>
43
+ <tbody id="treenode-table">
48
44
  {% for row in rows %}
49
45
  <tr {{ row.attrs|safe }}>
50
46
  {% for cell in row.cells %}
@@ -53,5 +49,6 @@
53
49
  </tr>
54
50
  {% endfor %}
55
51
  </tbody>
52
+
56
53
  </table>
57
54
  </div>
@@ -1,8 +1,8 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """
3
- Custon tags for changelist tamplate.
3
+ Custom tags for changelist template.
4
4
 
5
- Version: 3.0.0
5
+ Version: 3.1.0
6
6
  Author: Timur Kady
7
7
  Email: timurkady@yandex.com
8
8
  """
@@ -10,30 +10,73 @@ Email: timurkady@yandex.com
10
10
 
11
11
  from django import template
12
12
  from django.contrib.admin.templatetags import admin_list
13
+ from django.utils.html import format_html, mark_safe
13
14
 
14
15
  register = template.Library()
15
16
 
16
17
 
17
- @register.inclusion_tag(
18
- "treenode/admin/treenode_rows.html",
19
- takes_context=True,
20
- )
18
+ @register.inclusion_tag("treenode/admin/treenode_rows.html", takes_context=True)
21
19
  def tree_result_list(context, cl):
22
- """
23
- Return custom results for change_list.
24
-
25
- Заголовки берём из стандартного admin_list.result_headers(),
26
- а строки формируем как угодно.
27
- """
20
+ """Get result list."""
28
21
  headers = list(admin_list.result_headers(cl))
29
22
 
23
+ # Add a checkbox title manually if it is missing
24
+ if cl.actions and not any("action-checkbox-column" in h["class_attrib"] for h in headers):
25
+ headers.insert(0, {
26
+ "text": "",
27
+ "class_attrib": ' class="action-checkbox-column"',
28
+ "sortable": False
29
+ })
30
+
30
31
  rows = []
32
+
31
33
  for obj in cl.result_list:
32
34
  cells = list(admin_list.items_for_result(cl, obj, None))
35
+
36
+ # Insert checkbox manually
37
+ checkbox = format_html(
38
+ '<td class="action-checkbox">'
39
+ '<input type="checkbox" name="_selected_action" value="{}" class="action-select" /></td>',
40
+ obj.pk
41
+ )
42
+ cells.insert(0, checkbox)
43
+
44
+ # Replace the toggle cell (3rd after inserting checkbox and move)
45
+ is_leaf = getattr(obj, "is_leaf", lambda: True)()
46
+ toggle_html = format_html(
47
+ '<td class="field-toggle">{}</td>',
48
+ format_html(
49
+ '<button class="treenode-toggle" data-node-id="{}">►</button>',
50
+ obj.pk
51
+ ) if not is_leaf else mark_safe('<div class="treenode-space">&nbsp;</div>')
52
+ )
53
+ if len(cells) >= 3:
54
+ cells.pop(2)
55
+ cells.insert(2, toggle_html)
56
+
57
+ depth = getattr(obj, "get_depth", lambda: 0)()
58
+ parent_id = getattr(obj, "parent_id", "")
59
+ is_root = not parent_id
60
+
61
+ classes = ["treenode-row"]
62
+ if is_root:
63
+ classes.append("treenode-root")
64
+ else:
65
+ classes.append("treenode-hidden")
66
+
67
+ row_attrs = {
68
+ "class": " ".join(classes),
69
+ "data-node-id": obj.pk,
70
+ "data-parent-id": parent_id or "",
71
+ "data-depth": depth,
72
+ }
73
+
33
74
  rows.append({
34
- "attrs": getattr(obj, "row_attrs", ""),
75
+ "attrs": " ".join(f'{k}="{v}"' for k, v in row_attrs.items()),
35
76
  "cells": cells,
36
- "form": None, # поддержка list_editable, если нужно
77
+ "form": None,
78
+ "is_leaf": is_leaf,
79
+ "node_id": obj.pk,
37
80
  })
38
81
 
39
82
  return {
@@ -0,0 +1,25 @@
1
+ import jwt
2
+ from functools import wraps
3
+
4
+ from django.conf import settings
5
+ from django.http import JsonResponse
6
+
7
+
8
+ def jwt_required(view_func):
9
+ """Ensure that request has valid JWT token."""
10
+
11
+ @wraps(view_func)
12
+ def _wrapped(request, *args, **kwargs):
13
+ auth_header = request.META.get("HTTP_AUTHORIZATION", "")
14
+ if not auth_header.startswith("Bearer "):
15
+ return JsonResponse({"detail": "Authorization header missing"}, status=401)
16
+ token = auth_header.split(" ", 1)[1]
17
+ try:
18
+ payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
19
+ except jwt.PyJWTError:
20
+ return JsonResponse({"detail": "Invalid token"}, status=401)
21
+ request.jwt_payload = payload
22
+ return view_func(request, *args, **kwargs)
23
+
24
+ return _wrapped
25
+
treenode/version.py CHANGED
@@ -4,9 +4,9 @@ TreeNode Version Module
4
4
 
5
5
  This module defines the current version of the TreeNode package.
6
6
 
7
- Version: 3.0.7
7
+ Version: 3.2.0
8
8
  Author: Timur Kady
9
9
  Email: timurkady@yandex.com
10
10
  """
11
11
 
12
- __version__ = '3.0.7'
12
+ __version__ = '3.2.0'
treenode/views/autoapi.py CHANGED
@@ -14,10 +14,12 @@ from django.conf import settings
14
14
  from django.contrib.auth.decorators import login_required
15
15
 
16
16
  from ..models import TreeNodeModel
17
+ from ..settings import API_USE_JWT
18
+ from ..utils.jwt_auth import jwt_required
17
19
  from .autocomplete import TreeNodeAutocompleteView
18
20
  from .children import TreeChildrenView
19
- from .search import TreeSearchView
20
21
  from .crud import TreeNodeBaseAPIView
22
+ from .search import TreeSearchView
21
23
 
22
24
 
23
25
  class AutoTreeAPI:
@@ -35,6 +37,8 @@ class AutoTreeAPI:
35
37
  Protects view with login_required if needed, based on model attribute
36
38
  or global settings.
37
39
  """
40
+ if API_USE_JWT:
41
+ return jwt_required(view)
38
42
  if getattr(model, 'api_login_required', None) is True:
39
43
  return login_required(view)
40
44
  if getattr(settings, 'TREENODE_API_LOGIN_REQUIRED', False):