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.
- {django_fast_treenode-3.0.7.dist-info → django_fast_treenode-3.2.0.dist-info}/METADATA +3 -1
- django_fast_treenode-3.2.0.dist-info/RECORD +97 -0
- treenode/admin/admin.py +109 -131
- treenode/admin/mixin.py +6 -6
- treenode/managers/managers.py +1 -1
- treenode/managers/queries.py +1 -1
- treenode/managers/tasks.py +1 -1
- treenode/models/mixins/node.py +4 -4
- treenode/models/models.py +14 -9
- treenode/settings.py +4 -1
- treenode/static/treenode/.gitkeep +0 -0
- treenode/static/{css → treenode/css}/treenode_admin.css +21 -0
- treenode/static/treenode/js/treenode_admin.js +322 -0
- treenode/static/treenode/vendors/.gitkeep +0 -0
- treenode/static/treenode/vendors/jquery-ui/.gitkeep +0 -0
- treenode/templates/treenode/admin/treenode_changelist.html +61 -15
- treenode/templates/treenode/admin/treenode_import_export.html +3 -2
- treenode/templates/treenode/admin/treenode_rows.html +37 -40
- treenode/templatetags/treenode_admin.py +57 -14
- treenode/utils/jwt_auth.py +25 -0
- treenode/version.py +2 -2
- treenode/views/autoapi.py +5 -1
- treenode/views/autocomplete.py +52 -52
- treenode/views/children.py +41 -41
- treenode/views/common.py +23 -23
- treenode/widgets.py +2 -2
- django_fast_treenode-3.0.7.dist-info/RECORD +0 -93
- treenode/static/js/treenode_admin.js +0 -531
- {django_fast_treenode-3.0.7.dist-info → django_fast_treenode-3.2.0.dist-info}/WHEEL +0 -0
- {django_fast_treenode-3.0.7.dist-info → django_fast_treenode-3.2.0.dist-info}/licenses/LICENSE +0 -0
- {django_fast_treenode-3.0.7.dist-info → django_fast_treenode-3.2.0.dist-info}/top_level.txt +0 -0
- /treenode/static/{css → treenode/css}/.gitkeep +0 -0
- /treenode/static/{css → treenode/css}/tree_widget.css +0 -0
- /treenode/static/{css → treenode/css}/treenode_tabs.css +0 -0
- /treenode/static/{js → treenode/js}/.gitkeep +0 -0
- /treenode/static/{js → treenode/js}/lz-string.min.js +0 -0
- /treenode/static/{js → treenode/js}/tree_widget.js +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/AUTHORS.txt +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/LICENSE.txt +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/external/jquery/jquery.js +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_444444_256x240.png +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_555555_256x240.png +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_777620_256x240.png +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_777777_256x240.png +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_cc0000_256x240.png +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_ffffff_256x240.png +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/index.html +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.css +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.js +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.min.css +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.min.js +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.structure.css +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.structure.min.css +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.theme.css +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.theme.min.css +0 -0
- /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
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
{%
|
15
|
-
|
16
|
-
|
17
|
-
{% endif %}
|
47
|
+
{% if action_form and actions_on_top and cl.show_admin_actions %}
|
48
|
+
{% admin_actions %}
|
49
|
+
{% endif %}
|
18
50
|
|
19
|
-
|
51
|
+
{% block result_list %}
|
52
|
+
{% tree_result_list cl %}
|
53
|
+
{% endblock %}
|
20
54
|
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
7
|
-
<link rel="stylesheet" href="{% static '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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
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
|
-
</
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
3
|
+
Custom tags for changelist template.
|
4
4
|
|
5
|
-
Version: 3.
|
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"> </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":
|
75
|
+
"attrs": " ".join(f'{k}="{v}"' for k, v in row_attrs.items()),
|
35
76
|
"cells": cells,
|
36
|
-
"form": None,
|
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
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):
|