django-fast-treenode 2.1.4__py3-none-any.whl → 3.0.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 (107) hide show
  1. django_fast_treenode-3.0.0.dist-info/METADATA +203 -0
  2. django_fast_treenode-3.0.0.dist-info/RECORD +90 -0
  3. {django_fast_treenode-2.1.4.dist-info → django_fast_treenode-3.0.0.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 +42 -20
  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.0.dist-info/licenses}/LICENSE +0 -0
  105. {django_fast_treenode-2.1.4.dist-info → django_fast_treenode-3.0.0.dist-info}/top_level.txt +0 -0
  106. /treenode/static/{treenode → css}/.gitkeep +0 -0
  107. /treenode/static/{treenode/css → js}/.gitkeep +0 -0
@@ -0,0 +1,531 @@
1
+ /**
2
+ * treenode_admin.js
3
+ *
4
+ * Advanced TreeNode Admin extension for Django Admin.
5
+ * Adds dynamic drag-and-drop sorting, AJAX-based subtree loading,
6
+ * real-time visual feedback, and persistent tree state.
7
+ *
8
+ * Features:
9
+ * - Drag-and-drop node movement with Shift for child placement
10
+ * - Visual animations for feedback (insert flash, drag highlight)
11
+ * - Inline AJAX expansion and collapse of subtrees
12
+ * - Server-side re-rendering and recovery after node movement
13
+ * - Lightweight localStorage persistence using compressed HTML
14
+ *
15
+ * Version: 3.0.0
16
+ * Author: Timur Kady
17
+ * Email: timurkady@yandex.com
18
+ */
19
+
20
+
21
+ (function($) {
22
+
23
+ // ------------------------------- //
24
+ // Service and auxiliary functions //
25
+ // ------------------------------- //
26
+
27
+ function debounce(func, wait) {
28
+ var timeout;
29
+ return function() {
30
+ var context = this, args = arguments;
31
+ clearTimeout(timeout);
32
+ timeout = setTimeout(function() {
33
+ func.apply(context, args);
34
+ }, wait);
35
+ };
36
+ }
37
+
38
+ function getCookie(name) {
39
+ let cookieValue = null;
40
+ if (document.cookie && document.cookie !== '') {
41
+ const cookies = document.cookie.split(';');
42
+ for (let i = 0; i < cookies.length; i++) {
43
+ const cookie = cookies[i].trim();
44
+ if (cookie.startsWith(name + '=')) {
45
+ cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
46
+ break;
47
+ }
48
+ }
49
+ }
50
+ return cookieValue;
51
+ }
52
+
53
+ function showAdminMessage(text, level = "info", duration = 6000) {
54
+ const $ = django.jQuery || window.jQuery;
55
+
56
+ // Make sure the block exists
57
+ let msgList = $(".messagelist");
58
+ if (!msgList.length) {
59
+ msgList = $('<ul class="messagelist"></ul>');
60
+ $("#content").before(msgList);
61
+ }
62
+
63
+ // Create a message
64
+ const msg = $(`<li class="${level}">${text}</li>`);
65
+
66
+ msgList.append(msg);
67
+
68
+ // Auto disappear after 6 seconds (default)
69
+ setTimeout(() => {
70
+ msg.fadeOut(500, () => msg.remove());
71
+ }, duration);
72
+ }
73
+
74
+ function hangleAjaxSuccess(data) {
75
+ const msg = data.message || "The request was successfully completed.";
76
+ showAdminMessage(msg, "success");
77
+ ChangeList.saveTree()
78
+ ChangeList.restoreTree(ChangeList.expandedNodes);
79
+ }
80
+
81
+ function handleAjaxError(xhr, status, error) {
82
+ ChangeList.restoreTree(ChangeList.expandedNodes);
83
+
84
+ const fallbackMessage = "An unknown error occurred while executing the request.";
85
+ const $ = django.jQuery || window.jQuery;
86
+ let message = "";
87
+
88
+ // Try to extract the JSON error
89
+ try {
90
+ const contentType = xhr.getResponseHeader("Content-Type") || "";
91
+ if (contentType.includes("application/json")) {
92
+ const data = xhr.responseJSON || JSON.parse(xhr.responseText);
93
+ if (data?.error) {
94
+ message = data.error;
95
+ } else if (typeof data?.detail === "string") {
96
+ message = data.detail;
97
+ } else if (typeof data === "string") {
98
+ message = data;
99
+ }
100
+ }
101
+ } catch (e) {
102
+ // Not JSON – silently continue
103
+ }
104
+
105
+ // Try to parse plain HTML Django error and extract exception summary
106
+ if (!message && xhr.responseText) {
107
+ try {
108
+ // Try to extract main exception message from Django debug HTML
109
+ const match = xhr.responseText.match(/<pre class="exception_value">([^<]+)<\/pre>/);
110
+ if (match && match[1]) {
111
+ message = match[1].trim();
112
+ } else {
113
+ // As fallback, extract first lines of cleaned HTML
114
+ const plain = xhr.responseText.replace(/<\/?[^>]+(>|$)/g, "").trim();
115
+ const lines = plain.split("\n").map(line => line.trim()).filter(Boolean);
116
+ if (lines.length) {
117
+ message = lines.slice(0, 3).join(" — "); // keep it brief
118
+ }
119
+ }
120
+ } catch (e) {
121
+ // Parsing failed, do nothing
122
+ }
123
+ }
124
+
125
+ // If there is still nothing, build a generic fallback
126
+ if (!message) {
127
+ if (xhr.status) {
128
+ message = `${xhr.status}: ${error || status}`;
129
+ } else {
130
+ message = fallbackMessage;
131
+ }
132
+ }
133
+
134
+ showAdminMessage(message, "error");
135
+ }
136
+
137
+ function isDarkTheme() {
138
+ return document.documentElement.getAttribute("data-theme") === "dark";
139
+ }
140
+
141
+ function applyTheme() {
142
+ var dark = isDarkTheme();
143
+ var $container = $(".tree-widget");
144
+ var $dropdown = $(".tree-widget-dropdown");
145
+
146
+ if (dark) {
147
+ $dropdown.addClass("dark-theme");
148
+ $container.addClass("dark-theme");
149
+ } else {
150
+ $dropdown.removeClass("dark-theme");
151
+ $container.removeClass("dark-theme");
152
+ }
153
+ }
154
+
155
+ let ajaxCounter = 0;
156
+
157
+ function clearCursor() {
158
+ ajaxCounter--;
159
+ if (ajaxCounter === 0) {
160
+ $('body').css('cursor', 'default');
161
+ }
162
+ }
163
+
164
+ // ------------------------------- //
165
+ // AJAX Setup //
166
+ // ------------------------------- //
167
+
168
+ const csrftoken = getCookie('csrftoken');
169
+
170
+ $.ajaxSetup({
171
+ beforeSend: function(xhr, settings) {
172
+ if (!/^https?:.*/.test(settings.url)) {
173
+ xhr.setRequestHeader("X-CSRFToken", csrftoken);
174
+ }
175
+ ajaxCounter++;
176
+ $('body').css('cursor', 'wait');
177
+ },
178
+ cache: false
179
+ });
180
+
181
+ // ------------------------------- //
182
+ // Animations and visual feedback //
183
+ // ------------------------------- //
184
+
185
+ const TreeFx = {
186
+ // Highlight the line where the element was moved
187
+ flashInsert(nodeId) {
188
+ const $row = $(`tr[data-node-id="${nodeId}"]`);
189
+ if (!$row.length) return;
190
+
191
+ $row.addClass("flash-insert");
192
+ setTimeout(() => $row.removeClass("flash-insert"), 1000);
193
+ },
194
+
195
+ // Fading in a new tbody (e.g. after reloadTree)
196
+ fadeInTbody(newHTML, callback) {
197
+ const $tbody = $('table#result_list tbody');
198
+ $tbody.stop(true, true).fadeOut(100, () => {
199
+ $tbody.html(newHTML).fadeIn(150, callback);
200
+ });
201
+ },
202
+
203
+ // Visual cue when drag starts and ends
204
+ markDragging($item, enable) {
205
+ $item.toggleClass('dragging', enable);
206
+ },
207
+ };
208
+
209
+
210
+ // ------------------------------- //
211
+ // Main Class //
212
+ // ------------------------------- //
213
+
214
+
215
+ var ChangeList = {
216
+ $tableBody: null,
217
+ isShiftPressed: false,
218
+ activeTargetRow: null,
219
+ isMoving: false,
220
+ expandedNodes: [],
221
+ label: '',
222
+
223
+ init: function() {
224
+ this.$tableBody = $('table#result_list tbody');
225
+ this.bindEvents();
226
+ this.restoreTree();
227
+ this.enableDragAndDrop();
228
+ },
229
+
230
+ saveTree: function() {
231
+ if (!this.$tableBody) return;
232
+
233
+ this.expandedNodes = [];
234
+ this.$tableBody.find(".treenode-toggle").each(function () {
235
+ $btn = $(this)
236
+ if ($btn.data('expanded')) {
237
+ ChangeList.expandedNodes.push($btn.data('node-id'));
238
+ }
239
+ });
240
+ if (!(this.expandedNodes ) || (this.expandedNodes.length === 0)) {
241
+ localStorage.removeItem("saved_tbody");
242
+ } else {
243
+ localStorage.setItem("saved_tbody", JSON.stringify(this.expandedNodes));
244
+ }
245
+ const count = $("#result_list tbody tr").length;
246
+ if (count > 0) {
247
+ $("p.paginator").first().text(`${count} ${ChangeList.label}`);
248
+ localStorage.setItem("label", ChangeList.label);
249
+ }
250
+
251
+ },
252
+
253
+ restoreTree: function(expandedList = null) {
254
+ const expanded = expandedList || JSON.parse(localStorage.getItem("saved_tbody") || "[]");
255
+ ChangeList.label = localStorage.getItem("label") || "";
256
+ if (!expanded.length) return;
257
+
258
+ const params = { expanded: JSON.stringify(expanded) };
259
+
260
+ $.ajax({
261
+ url: 'change_list/',
262
+ method: 'GET',
263
+ data: params,
264
+ dataType: 'json',
265
+ success: function(data) {
266
+ ChangeList.$tableBody.html(data.html);
267
+ ChangeList.expandedNodes = expanded;
268
+
269
+ ChangeList.$tableBody.find(".treenode-toggle").each(function () {
270
+ const $btn = $(this);
271
+ const nodeId = $btn.data('node-id');
272
+ if (ChangeList.expandedNodes.includes(nodeId)) {
273
+ $btn.data("expanded", true);
274
+ $btn.text("▼");
275
+ }
276
+ });
277
+
278
+ if (data.label) ChangeList.label = data.label;
279
+ const count = $("#result_list tbody tr").length;
280
+ $("p.paginator").first().text(`${count} ${ChangeList.label}`);
281
+ },
282
+ error: function(xhr, status, error) {
283
+ handleAjaxError(xhr, status, error);
284
+ // console.error("Restore error:", status, error);
285
+ },
286
+ complete: function() {
287
+ clearCursor();
288
+ }
289
+ });
290
+ },
291
+
292
+ searchRows: function(searchQuery) {
293
+ var params = { q: searchQuery };
294
+
295
+ $.ajax({
296
+ url: 'change_list/',
297
+ method: 'GET',
298
+ data: params,
299
+ dataType: 'json',
300
+ success: function(data) {
301
+ ChangeList.$tableBody.html(data.html);
302
+ },
303
+ error: function(xhr, status, error) {
304
+ handleAjaxError(xhr, status, error);
305
+ // console.error("Search error:", status, error);
306
+ },
307
+ complete: function() {
308
+ clearCursor();
309
+ }
310
+ });
311
+ },
312
+
313
+ insertRows: function($parentRow, parentId) {
314
+ var parent_id = parentId
315
+ var params = {parent_id: parent_id};
316
+
317
+ $.ajax({
318
+ url: 'change_list/',
319
+ method: 'GET',
320
+ data: params,
321
+ dataType: 'json',
322
+ success: function(data) {
323
+ $parentRow.after(data.html);
324
+ if (data.label) ChangeList.label = data.label;
325
+ },
326
+ error: function(xhr, status, error) {
327
+ handleAjaxError(xhr, status, error);
328
+ // console.error("Insert rows error:", status, error);
329
+ },
330
+ complete: function() {
331
+ ChangeList.saveTree();
332
+ clearCursor();
333
+ }
334
+ });
335
+ },
336
+
337
+ removeRows: function(parentId) {
338
+ var $children = this.$tableBody.find(`tr[data-parent-of="${parentId}"]`);
339
+ $children.each(function () {
340
+ var childId = $(this).data('node-id');
341
+ ChangeList.removeRows(childId);
342
+ });
343
+ $children.remove();
344
+ this.saveTree();
345
+ },
346
+
347
+ toggleNode: function($btn) {
348
+ var expanded = $btn.data('expanded');
349
+ var $parentRow = $btn.closest('tr');
350
+ var parentId = $btn.data('node-id');
351
+
352
+ if (expanded) {
353
+ $btn.html('►').data('expanded', false);
354
+ this.removeRows(parentId);
355
+ } else {
356
+ $btn.html('▼').data('expanded', true);
357
+ this.insertRows($parentRow, parentId);
358
+ }
359
+ },
360
+
361
+ bindEvents: function() {
362
+ var self = this;
363
+
364
+ // Search listener
365
+ $('input[name="q"]')
366
+ .on("focus", function() {
367
+ self.saveTree();
368
+ })
369
+ .on("keyup", debounce(function() {
370
+ var query = $.trim($(this).val());
371
+ if (query === '') {
372
+ self.restoreTree(ChangeList.expandedNodes);
373
+ } else {
374
+ self.searchRows(query);
375
+ }
376
+ }, 300));
377
+
378
+ // Toggle buttons listener
379
+ $(document).on("click", "button.treenode-toggle", function(e) {
380
+ e.preventDefault();
381
+ var $btn = $(this);
382
+ self.toggleNode($btn);
383
+ });
384
+
385
+ // Shift key hold listener
386
+ $(document).on("keydown", function(e) {
387
+ if (e.key === "Shift") {
388
+ ChangeList.isShiftPressed = true;
389
+ ChangeList.updateDndHighlight();
390
+ }
391
+ })
392
+ .on("keyup", function(e) {
393
+ if (e.key === "Shift") {
394
+ ChangeList.isShiftPressed = false;
395
+ ChangeList.updateDndHighlight();
396
+ }
397
+ });
398
+
399
+ // Listener for admin panel theme change button
400
+ $(document).on("click", "button.theme-toggle", function() {
401
+ applyTheme();
402
+ });
403
+
404
+ // Fix action selets
405
+ $('#result_list').on('change', '#action-toggle', function() {
406
+ $('input.action-select').prop('checked', this.checked);
407
+ });
408
+ },
409
+
410
+ enableDragAndDrop: function() {
411
+ const self = this;
412
+
413
+ this.$tableBody.sortable({
414
+ items: "tr",
415
+ handle: ".treenode-drag-handle",
416
+ placeholder: "treenode-placeholder",
417
+ activeTargetRow: null,
418
+ helper: function(e, tr) {
419
+ const $originals = tr.children();
420
+ const $helper = tr.clone();
421
+ $helper.find('[id]').each(function() {
422
+ $(this).removeAttr('id');
423
+ });
424
+ $helper.children().each(function(index) {
425
+ $(this).width($originals.eq(index).width());
426
+ });
427
+ return $helper;
428
+ },
429
+ start: function(e, ui) {
430
+ TreeFx.markDragging(ui.item, true);
431
+ // ChangeList.updateDndHighlight();
432
+ // console.log("Drag started:", ui.item.data("node-id"));
433
+ },
434
+ over: function(e, ui) {
435
+ ChangeList.updateDndHighlight();
436
+ // console.log("over")
437
+ },
438
+ stop: function(e, ui) {
439
+ TreeFx.markDragging(ui.item, false);
440
+
441
+ const $item = ui.item;
442
+ const nodeId = $item.data("node-id");
443
+ const prevId = $item.prev().data("node-id") || null;
444
+ const nextId = $item.next().data("node-id") || null;
445
+ const isChild = ChangeList.isShiftPressed;
446
+
447
+ if (ChangeList.activeTargetRow) {
448
+ ChangeList.activeTargetRow.removeClass("target-as-child");
449
+ ChangeList.activeTargetRow = null;
450
+ }
451
+
452
+ mode = isChild ? 'child' : 'after';
453
+ ChangeList.applyMove(nodeId, prevId, mode)
454
+
455
+ TreeFx.flashInsert(nodeId);
456
+ },
457
+ });
458
+ },
459
+
460
+ updateDndHighlight: function() {
461
+ const $placeholder = this.$tableBody.find("tr.treenode-placeholder");
462
+ const $target = $placeholder.prev();
463
+
464
+ if (!($target && $target.length && $target.data("node-id"))) {
465
+ this.$tableBody.find("tr.target-as-child").removeClass("target-as-child");
466
+ this.activeTargetRow = null;
467
+ return;
468
+ }
469
+
470
+ if (this.isShiftPressed) {
471
+ if (!this.activeTargetRow || !this.activeTargetRow.is($target)) {
472
+ this.$tableBody.find("tr.target-as-child").removeClass("target-as-child");
473
+ $target.addClass("target-as-child");
474
+ this.activeTargetRow = $target;
475
+ }
476
+ } else {
477
+ if (this.activeTargetRow) {
478
+ this.activeTargetRow.removeClass("target-as-child");
479
+ this.activeTargetRow = null;
480
+ }
481
+ }
482
+ },
483
+
484
+ applyMove: function(nodeId, targetId, mode) {
485
+ if (this.isMoving) return;
486
+
487
+ this.isMoving = true;
488
+ this.activeTargetRow = null;
489
+
490
+ const params = {
491
+ node_id: nodeId,
492
+ target_id: targetId,
493
+ mode: mode,
494
+ expanded: JSON.stringify(this.expandedNodes)
495
+ };
496
+
497
+ // console.log(params);
498
+
499
+ $.ajax({
500
+ url: 'move/',
501
+ method: 'POST',
502
+ data: params,
503
+ dataType: 'json',
504
+ success: function(data) {
505
+ hangleAjaxSuccess(data);
506
+ },
507
+ error: function(xhr, status, error) {
508
+ handleAjaxError(xhr, status, error);
509
+ },
510
+ complete: function() {
511
+ ChangeList.isMoving = false;
512
+ clearCursor();
513
+ }
514
+ });
515
+ }
516
+ }
517
+
518
+ // ------------------------------- //
519
+ // Init //
520
+ // ------------------------------- //
521
+
522
+ $(document).ready(function () {
523
+ document.body.style.cursor = '';
524
+ applyTheme();
525
+ if ($("table#result_list").length) {
526
+ ChangeList.init();
527
+ }
528
+ });
529
+
530
+ })(django.jQuery || window.jQuery);
531
+