django-fast-treenode 2.0.10__py3-none-any.whl → 2.1.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 (70) hide show
  1. {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/LICENSE +2 -2
  2. django_fast_treenode-2.1.0.dist-info/METADATA +161 -0
  3. django_fast_treenode-2.1.0.dist-info/RECORD +75 -0
  4. {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/WHEEL +1 -1
  5. treenode/admin/__init__.py +9 -0
  6. treenode/admin/admin.py +295 -0
  7. treenode/admin/changelist.py +65 -0
  8. treenode/admin/mixins.py +302 -0
  9. treenode/apps.py +12 -1
  10. treenode/cache.py +2 -2
  11. treenode/docs/.gitignore +0 -0
  12. treenode/docs/about.md +36 -0
  13. treenode/docs/admin.md +104 -0
  14. treenode/docs/api.md +739 -0
  15. treenode/docs/cache.md +187 -0
  16. treenode/docs/import_export.md +35 -0
  17. treenode/docs/index.md +30 -0
  18. treenode/docs/installation.md +74 -0
  19. treenode/docs/migration.md +145 -0
  20. treenode/docs/models.md +128 -0
  21. treenode/docs/roadmap.md +45 -0
  22. treenode/forms.py +33 -22
  23. treenode/managers/__init__.py +21 -0
  24. treenode/managers/adjacency.py +203 -0
  25. treenode/managers/closure.py +278 -0
  26. treenode/models/__init__.py +2 -1
  27. treenode/models/adjacency.py +343 -0
  28. treenode/models/classproperty.py +3 -0
  29. treenode/models/closure.py +39 -65
  30. treenode/models/factory.py +12 -2
  31. treenode/models/mixins/__init__.py +23 -0
  32. treenode/models/mixins/ancestors.py +65 -0
  33. treenode/models/mixins/children.py +81 -0
  34. treenode/models/mixins/descendants.py +66 -0
  35. treenode/models/mixins/family.py +63 -0
  36. treenode/models/mixins/logical.py +68 -0
  37. treenode/models/mixins/node.py +210 -0
  38. treenode/models/mixins/properties.py +156 -0
  39. treenode/models/mixins/roots.py +96 -0
  40. treenode/models/mixins/siblings.py +99 -0
  41. treenode/models/mixins/tree.py +344 -0
  42. treenode/signals.py +26 -0
  43. treenode/static/treenode/css/tree_widget.css +201 -31
  44. treenode/static/treenode/css/treenode_admin.css +48 -41
  45. treenode/static/treenode/js/tree_widget.js +269 -131
  46. treenode/static/treenode/js/treenode_admin.js +131 -171
  47. treenode/templates/admin/tree_node_changelist.html +6 -0
  48. treenode/templates/admin/tree_node_import.html +27 -9
  49. treenode/templates/admin/tree_node_import_report.html +32 -0
  50. treenode/templates/admin/treenode_ajax_rows.html +7 -0
  51. treenode/tests/tests.py +488 -0
  52. treenode/urls.py +10 -6
  53. treenode/utils/__init__.py +2 -0
  54. treenode/utils/aid.py +46 -0
  55. treenode/utils/base16.py +38 -0
  56. treenode/utils/base36.py +3 -1
  57. treenode/utils/db.py +116 -0
  58. treenode/utils/exporter.py +63 -36
  59. treenode/utils/importer.py +168 -161
  60. treenode/utils/radix.py +61 -0
  61. treenode/version.py +2 -2
  62. treenode/views.py +119 -38
  63. treenode/widgets.py +104 -40
  64. django_fast_treenode-2.0.10.dist-info/METADATA +0 -698
  65. django_fast_treenode-2.0.10.dist-info/RECORD +0 -41
  66. treenode/admin.py +0 -396
  67. treenode/docs/Documentation +0 -664
  68. treenode/managers.py +0 -281
  69. treenode/models/proxy.py +0 -650
  70. {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/top_level.txt +0 -0
@@ -1,171 +1,131 @@
1
- /*
2
- TreeNode Admin JavaScript
3
-
4
- This script enhances the Django admin interface with interactive
5
- tree structure visualization. It provides expand/collapse functionality
6
- for hierarchical data representation.
7
-
8
- Features:
9
- - Dynamically detects parent-child relationships in the admin table.
10
- - Allows nodes to be expanded and collapsed with smooth animations.
11
- - Saves expanded/collapsed states in localStorage for persistence.
12
- - Optimizes tree traversal with efficient DOM manipulation.
13
-
14
- Version: 2.0.0
15
- Author: Timur Kady
16
- Email: timurkady@yandex.com
17
- */
18
-
19
- (function($) {
20
- $(document).ready(function() {
21
- // Mapping parent node -> list of child nodes
22
- var childrenMap = {};
23
-
24
- // Iterate over all tree nodes to build a mapping of parent-child relationships
25
- $('.treenode, .treenode-wrapper').each(function() {
26
- var parent = $(this).data('treenode-parent'); // Get parent ID
27
- var pk = $(this).data('treenode-pk'); // Get node primary key
28
-
29
- if (parent) {
30
- if (!childrenMap[parent]) {
31
- childrenMap[parent] = [];
32
- }
33
- childrenMap[parent].push(pk);
34
- }
35
- });
36
-
37
- // Initialize tree nodes, set up toggles
38
- $('.treenode, .treenode-wrapper').each(function() {
39
- var $node = $(this);
40
- var pk = $node.data('treenode-pk'); // Get current node ID
41
- var depth = parseInt($node.data('treenode-depth'), 10) || 0; // Get tree depth level
42
- var $tr = $node.closest('tr'); // Find the corresponding row in the table
43
- $tr.attr('data-treenode-depth', depth);
44
- $tr.attr('data-treenode-pk', pk);
45
-
46
- var $th = $tr.find('th:first');
47
- if ($th.find('.treenode-space, .treenode-toggle').length === 0) {
48
- var $placeholder = $('<span class="treenode-space"> </span>')
49
- .css('margin-left', (depth * 20) + 'px'); // Indentation based on depth
50
- $th.prepend($placeholder);
51
- }
52
-
53
- // If node has children, add expand/collapse toggle
54
- if (childrenMap[pk]) {
55
- var expanded = loadState(pk);
56
- var $placeholder = $th.find('.treenode-space');
57
- var $toggle = $('<span class="treenode-toggle button"></span>')
58
- .css('margin-left', (depth * 20) + 'px');
59
-
60
- if (expanded) {
61
- $toggle.addClass('expanded').text('-');
62
- } else {
63
- $toggle.addClass('collapsed').text('+');
64
- }
65
- $placeholder.replaceWith($toggle);
66
-
67
- if (!expanded) {
68
- collapseNode($tr, true);
69
- }
70
-
71
- // Toggle node expansion/collapse on click
72
- $toggle.on('click', function() {
73
- if ($(this).hasClass('expanded')) {
74
- collapseNode($tr, false);
75
- $(this).removeClass('expanded').addClass('collapsed').text('+');
76
- saveState(pk, false);
77
- } else {
78
- expandNode($tr);
79
- $(this).removeClass('collapsed').addClass('expanded').text('-');
80
- saveState(pk, true);
81
- }
82
- });
83
- }
84
- });
85
-
86
- // Apply initial collapsed states based on saved preferences
87
- function applyCollapsedStates() {
88
- $('tr').each(function() {
89
- var $tr = $(this);
90
- var currentDepth = parseInt($tr.attr('data-treenode-depth'), 10);
91
- var hide = false;
92
-
93
- // Check if any ancestor is collapsed
94
- $tr.prevAll('tr').each(function() {
95
- var $prev = $(this);
96
- var prevDepth = parseInt($prev.attr('data-treenode-depth'), 10);
97
- if (prevDepth < currentDepth) {
98
- var parentPk = $prev.attr('data-treenode-pk');
99
- if (loadState(parentPk) === false) {
100
- hide = true;
101
- }
102
- return false;
103
- }
104
- });
105
-
106
- if (hide) {
107
- $tr.hide();
108
- }
109
- });
110
- }
111
- applyCollapsedStates();
112
-
113
- // Collapse a node and all its descendants
114
- function collapseNode($tr, immediate) {
115
- var parentDepth = parseInt($tr.attr('data-treenode-depth'), 10);
116
- var $nextRows = $tr.nextAll('tr');
117
- $nextRows.each(function() {
118
- var $row = $(this);
119
- var rowDepth = parseInt($row.attr('data-treenode-depth'), 10);
120
- if (rowDepth > parentDepth) {
121
- if (immediate) {
122
- $row.hide();
123
- } else {
124
- $row.slideUp(300);
125
- }
126
- var childPk = $row.attr('data-treenode-pk');
127
- saveState(childPk, false);
128
- } else {
129
- return false;
130
- }
131
- });
132
- }
133
-
134
- // Expand a node and reveal its immediate children
135
- function expandNode($tr) {
136
- var parentDepth = parseInt($tr.attr('data-treenode-depth'), 10);
137
- var $nextRows = $tr.nextAll('tr');
138
- $nextRows.each(function() {
139
- var $row = $(this);
140
- var rowDepth = parseInt($row.attr('data-treenode-depth'), 10);
141
- if (rowDepth > parentDepth) {
142
- if (rowDepth === parentDepth + 1) {
143
- $row.slideDown('slow');
144
- var childPk = $row.attr('data-treenode-pk');
145
- if (loadState(childPk)) {
146
- expandNode($row);
147
- }
148
- }
149
- } else {
150
- return false;
151
- }
152
- });
153
- }
154
-
155
- // Save expansion state to localStorage
156
- function saveState(pk, isExpanded) {
157
- if (window.localStorage) {
158
- localStorage.setItem('treenode_state_' + pk, isExpanded ? '1' : '0');
159
- }
160
- }
161
-
162
- // Load expansion state from localStorage
163
- function loadState(pk) {
164
- if (window.localStorage) {
165
- var state = localStorage.getItem('treenode_state_' + pk);
166
- return state === '1';
167
- }
168
- return true;
169
- }
170
- });
171
- })(django.jQuery || window.jQuery);
1
+ (function($) {
2
+ // Function "debounce" to delay execution
3
+ function debounce(func, wait) {
4
+ var timeout;
5
+ return function() {
6
+ var context = this, args = arguments;
7
+ clearTimeout(timeout);
8
+ timeout = setTimeout(function() {
9
+ func.apply(context, args);
10
+ }, wait);
11
+ };
12
+ }
13
+
14
+ // Function to recursively delete descendants of the current child
15
+ function removeAllDescendants(nodeId) {
16
+ var $children = $tableBody.find(`tr[data-parent-of="${nodeId}"]`);
17
+
18
+ $children.each(function () {
19
+ var childId = $(this).data('node-id');
20
+ removeAllDescendants(childId);
21
+ });
22
+
23
+ $children.remove();
24
+ }
25
+
26
+ // Function to reveal nodes stored in storage
27
+ function restoreExpandedNodes() {
28
+ var expandedNodes = JSON.parse(localStorage.getItem('expandedNodes')) || [];
29
+ if (expandedNodes.length === 0) return;
30
+
31
+ function expandNext(nodes) {
32
+ if (nodes.length === 0) return;
33
+
34
+ var nodeId = nodes.shift();
35
+
36
+ $.getJSON('change_list/', { tn_parent_id: nodeId }, function (response) {
37
+ if (response.html) {
38
+ var $btn = $tableBody.find(`.treenode-toggle[data-node-id="${nodeId}"]`);
39
+ var $parentRow = $btn.closest('tr');
40
+ $parentRow.after(response.html);
41
+ $btn.html('▼').data('expanded', true);
42
+ // Раскрываем следующий узел после завершения AJAX-запроса
43
+ expandNext(nodes);
44
+ }
45
+ });
46
+ }
47
+
48
+ // Начинаем раскрытие узлов по порядку
49
+ expandNext([...expandedNodes]);
50
+ }
51
+
52
+ // Global variables ---------------------------------
53
+ var $tableBody;
54
+ var originalTableHtml;
55
+
56
+ var expandedNodes = JSON.parse(localStorage.getItem('expandedNodes')) || [];
57
+
58
+ // Events -------------------------------------------
59
+
60
+ $(document).ready(function () {
61
+ // Сохраняем оригинальное содержимое таблицы (корневые узлы)
62
+ $tableBody = $('table#result_list tbody');
63
+ originalTableHtml = $tableBody.html();
64
+ restoreExpandedNodes();
65
+ });
66
+
67
+ // Обработчик клика для кнопок treenode-toggle через делегирование на document
68
+ $(document).on('click', '.treenode-toggle', function(e) {
69
+ e.preventDefault();
70
+ var $btn = $(this);
71
+ var nodeId = $btn.data('node-id');
72
+
73
+ // Если узел уже развёрнут, сворачиваем его
74
+ if ($btn.data('expanded')) {
75
+ removeAllDescendants(nodeId);
76
+ $btn.html('►').data('expanded', false);
77
+
78
+ // Убираем узел из списка сохранённых
79
+ expandedNodes = expandedNodes.filter(id => id !== nodeId);
80
+
81
+ } else {
82
+ // Иначе запрашиваем дочерние узлы через AJAX
83
+ $.getJSON('change_list/', { tn_parent_id: nodeId }, function(response) {
84
+ if (response.html) {
85
+ var $parentRow = $btn.closest('tr');
86
+ $parentRow.after(response.html);
87
+ $btn.html('▼').data('expanded', true);
88
+
89
+ // Сохраняем узел в localStorage
90
+ if (!expandedNodes.includes(nodeId)) {
91
+ expandedNodes.push(nodeId);
92
+ }
93
+ localStorage.setItem('expandedNodes', JSON.stringify(expandedNodes));
94
+ }
95
+ });
96
+ }
97
+ localStorage.setItem('expandedNodes', JSON.stringify(expandedNodes));
98
+ });
99
+
100
+
101
+ // Обработчик ввода для поля поиска с делегированием на document
102
+ $(document).on('keyup', 'input[name="q"]', debounce(function(e) {
103
+ var query = $.trim($(this).val());
104
+
105
+ if (query === '') {
106
+ $tableBody.html(originalTableHtml);
107
+ return;
108
+ }
109
+
110
+ $.getJSON('search/', { q: query }, function(response) {
111
+ var rowsHtml = '';
112
+ if (response.results && response.results.length > 0) {
113
+ $.each(response.results, function(index, node) {
114
+ var dragCell = '<td class="drag-cell"><span class="treenode-drag-handle">↕</span></td>';
115
+ var toggleCell = '';
116
+ if (!node.is_leaf) {
117
+ toggleCell = '<td class="toggle-cell"><button class="treenode-toggle" data-node-id="' + node.id + '">▶</button></td>';
118
+ } else {
119
+ toggleCell = '<td class="toggle-cell"><div class="treenode-space">&nbsp;</div></td>';
120
+ }
121
+ var displayCell = '<td class="display-cell">' + node.text + '</td>';
122
+ rowsHtml += '<tr>' + dragCell + toggleCell + displayCell + '</tr>';
123
+ });
124
+ } else {
125
+ rowsHtml = '<tr><td colspan="3">Ничего не найдено</td></tr>';
126
+ }
127
+ $tableBody.html(rowsHtml);
128
+ });
129
+ }, 500));
130
+
131
+ })(django.jQuery || window.jQuery);
@@ -11,3 +11,9 @@
11
11
  </li>
12
12
  {% endif %}
13
13
  {% endblock %}
14
+
15
+ {% block extrahead %}
16
+
17
+ {{ block.super }}
18
+
19
+ {% endblock %}
@@ -1,7 +1,20 @@
1
1
  {% extends "admin/base_site.html" %}
2
+ {% load static %}
3
+
4
+ {% block extrahead %}
5
+ <link rel="stylesheet" type="text/css" href="{% static 'admin/css/forms.css' %}">
6
+ {% endblock %}
7
+
2
8
  {% block content %}
3
9
  <div class="module">
4
10
  <h2>Import Data</h2>
11
+
12
+ {% if message %}
13
+ <div class="successnote">
14
+ <p>{{ message }}</p>
15
+ </div>
16
+ {% endif %}
17
+
5
18
  {% if errors %}
6
19
  <div class="errornote">
7
20
  <p>Errors occurred while importing the data:</p>
@@ -12,16 +25,21 @@
12
25
  </ul>
13
26
  </div>
14
27
  {% endif %}
15
- <form method="post" enctype="multipart/form-data">
28
+
29
+ <form id="importForm" method="post" enctype="multipart/form-data" style="margin: 20px 0;">
16
30
  {% csrf_token %}
17
- <div>
18
- <label for="file">Choose file:</label>
19
- <input type="file" name="file" id="file" required>
20
- </div>
21
- <div class="form-group" style="margin-top: 35px;">
22
- <button type="submit" class="button">Import</button>
23
- <a href=".." class="button cancel">Cancel</a>
31
+ <fieldset class="module aligned">
32
+ <div class="form-row field-tn_parent">
33
+ <label for="file">Choose file:</label>
34
+ <input type="file" name="file" id="file" required>
35
+ </div>
36
+ </fieldset>
37
+
38
+ <div class="submit-row" style="margin-top: 35px;">
39
+ <input id="importBtn" type="submit" value="Import" name="_save">
40
+ <input type="button" value="Cancel" class="button cancel" onclick="window.location.href='..';">
24
41
  </div>
25
42
  </form>
43
+
26
44
  </div>
27
- {% endblock %}
45
+ {% endblock %}
@@ -0,0 +1,32 @@
1
+ {% extends "admin/base_site.html" %}
2
+ {% block content %}
3
+ <div class="module">
4
+ <h2>Import Results</h2>
5
+
6
+ <ul class="messagelist">
7
+ <li class="success"><strong>Created:</strong> {{ created_count }} records</li>
8
+ <li class="success"><strong>Updated:</strong> {{ updated_count }} records</li>
9
+ </ul>
10
+
11
+ {% if errors %}
12
+ <div class="errornote">
13
+ <p>Errors occurred while importing the data:</p>
14
+ <ul>
15
+ {% for error in errors %}
16
+ <li>{{ error }}</li>
17
+ {% endfor %}
18
+ </ul>
19
+ </div>
20
+ {% endif %}
21
+
22
+ <div style="margin-top: 20px;">
23
+ <button type="button" class="button" onclick="redirectAfterImport()">Finish</button>
24
+ </div>
25
+ </div>
26
+
27
+ <script>
28
+ function redirectAfterImport() {
29
+ window.location.href = window.location.pathname.replace("import/", "") + "?import_done=1";
30
+ }
31
+ </script>
32
+ {% endblock %}
@@ -0,0 +1,7 @@
1
+ {% for row in rows %}
2
+ <tr data-node-id="{{ row.node_id }}" data-parent-of="{{ row.parent_id }}">
3
+ {% for cell, class in row.cells %}
4
+ <td class="{{ class }}">{{ cell|safe }}</td>
5
+ {% endfor %}
6
+ </tr>
7
+ {% endfor %}