django-fast-treenode 2.0.11__py3-none-any.whl → 2.1.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 (57) hide show
  1. {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.dist-info}/LICENSE +2 -2
  2. django_fast_treenode-2.1.1.dist-info/METADATA +158 -0
  3. django_fast_treenode-2.1.1.dist-info/RECORD +64 -0
  4. {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.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/forms.py +8 -10
  12. treenode/managers/__init__.py +21 -0
  13. treenode/managers/adjacency.py +203 -0
  14. treenode/managers/closure.py +278 -0
  15. treenode/models/__init__.py +2 -1
  16. treenode/models/adjacency.py +343 -0
  17. treenode/models/classproperty.py +3 -0
  18. treenode/models/closure.py +23 -24
  19. treenode/models/factory.py +12 -2
  20. treenode/models/mixins/__init__.py +23 -0
  21. treenode/models/mixins/ancestors.py +65 -0
  22. treenode/models/mixins/children.py +81 -0
  23. treenode/models/mixins/descendants.py +66 -0
  24. treenode/models/mixins/family.py +63 -0
  25. treenode/models/mixins/logical.py +68 -0
  26. treenode/models/mixins/node.py +210 -0
  27. treenode/models/mixins/properties.py +156 -0
  28. treenode/models/mixins/roots.py +96 -0
  29. treenode/models/mixins/siblings.py +99 -0
  30. treenode/models/mixins/tree.py +344 -0
  31. treenode/signals.py +26 -0
  32. treenode/static/treenode/css/tree_widget.css +201 -31
  33. treenode/static/treenode/css/treenode_admin.css +48 -41
  34. treenode/static/treenode/js/tree_widget.js +269 -131
  35. treenode/static/treenode/js/treenode_admin.js +131 -171
  36. treenode/templates/admin/tree_node_changelist.html +6 -0
  37. treenode/templates/admin/treenode_ajax_rows.html +7 -0
  38. treenode/tests/tests.py +488 -0
  39. treenode/urls.py +10 -6
  40. treenode/utils/__init__.py +2 -0
  41. treenode/utils/aid.py +46 -0
  42. treenode/utils/base16.py +38 -0
  43. treenode/utils/base36.py +3 -1
  44. treenode/utils/db.py +116 -0
  45. treenode/utils/exporter.py +2 -0
  46. treenode/utils/importer.py +0 -1
  47. treenode/utils/radix.py +61 -0
  48. treenode/version.py +2 -2
  49. treenode/views.py +118 -43
  50. treenode/widgets.py +91 -43
  51. django_fast_treenode-2.0.11.dist-info/METADATA +0 -698
  52. django_fast_treenode-2.0.11.dist-info/RECORD +0 -42
  53. treenode/admin.py +0 -439
  54. treenode/docs/Documentation +0 -636
  55. treenode/managers.py +0 -419
  56. treenode/models/proxy.py +0 -669
  57. {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.dist-info}/top_level.txt +0 -0
@@ -11,9 +11,10 @@ Features:
11
11
  - Smooth hover effects and animations.
12
12
  - Consistent layout adjustments for better UI interaction.
13
13
 
14
- Version: 2.0.0
14
+ Version: 2.1.0
15
15
  Author: Timur Kady
16
16
  Email: timurkady@yandex.com
17
+
17
18
  */
18
19
 
19
20
 
@@ -22,13 +23,17 @@ Email: timurkady@yandex.com
22
23
  100% {width: 100px; height: 100px;}
23
24
  }
24
25
 
26
+ .field-drag, .field-toggle {
27
+ width: 18px !important;
28
+ text-align: center !important;
29
+ padding: 8px 0px !important;
30
+ }
31
+
25
32
  .treenode-space {
26
33
  display: inline-block;
27
- width: 10px;
28
- height: 10px;
29
- margin-top: 0px;
30
- margin-bottom: 0px;
31
- margin-right: 7px !important;
34
+ width: 18px;
35
+ height: 18px;
36
+ margin: 0px 3px !important;
32
37
  background-color: transparent;
33
38
  border: 1px solid transparent;
34
39
  padding: 1px;
@@ -36,25 +41,28 @@ Email: timurkady@yandex.com
36
41
 
37
42
  .treenode-toggle {
38
43
  display: inline-block;
39
- text-align: center;
40
- font-weight: bold;
41
- font-size: 10px;
42
- width: 10px;
43
- height: 10px;
44
- line-height: 10px;
45
- padding: 1px;
44
+ width: 18px;
45
+ height: 18px;
46
+ background: var(--button-bg);
47
+ color: var(--button-fg);
48
+ border-radius: 3px;
49
+ border: none;
50
+ margin: 0px 5px;
46
51
  cursor: pointer;
47
- margin-top: 0px;
48
- margin-bottom: 0px;
49
- margin-right: 7px !important;
50
- border: 1px solid #ddd;
51
- border-radius: 4px;
52
- transition: background-color 0.2s ease, color 0.2s ease;
52
+ font-size: 12px;
53
+ line-height: 18px;
54
+ padding: 0px;
55
+ opacity: 0.8;
56
+ transition: opacity 0.2s ease, color 0.2s ease;
57
+ }
58
+
59
+ .treenode-toggle[expanded="true"] {
60
+ color: green;
53
61
  }
54
62
 
63
+
55
64
  .treenode-toggle:hover {
56
- background-color: #e0e0e0;
57
- color: #007bff;
65
+ opacity: 1.0;
58
66
  }
59
67
 
60
68
  .dark-theme .treenode-toggle {
@@ -68,24 +76,6 @@ Email: timurkady@yandex.com
68
76
  color: #fff;
69
77
  }
70
78
 
71
- .treenode-toggle {
72
- font-weight: bold;
73
- display: inline-block;
74
-
75
- text-align: center;
76
- cursor: pointer;
77
- margin-right: 5px;
78
- border-radius: 4px;
79
-
80
- padding: 2px;
81
- transition: background-color 0.2s ease, color 0.2s ease;
82
- }
83
-
84
- .treenode-toggle:hover {
85
- background-color: #e0e0e0;
86
- color: #007bff;
87
- }
88
-
89
79
  .dark-theme .treenode-toggle {
90
80
  color: #ccc;
91
81
  background-color: #444;
@@ -97,10 +87,27 @@ Email: timurkady@yandex.com
97
87
  color: #fff;
98
88
  }
99
89
 
100
- .treenode-wrapper {
90
+ .treenode-drag-handle {
101
91
  display: inline-block;
92
+ text-align: center;
93
+ font-weight: bold;
94
+ font-size: 10px;
95
+ width: 10px;
96
+ height: 10px;
97
+ line-height: 10px;
98
+ padding: 1px;
99
+ cursor: ns-resize;
100
+ opacity: 0.25;
102
101
  }
103
102
 
104
- .dark-theme .treenode-toggle {
103
+ .treenode-drag-handle:hover {
104
+ opacity: 1.0;
105
+ }
106
+
107
+ .dark-theme treenode-drag-handle {
105
108
  color: #ccc;
106
109
  }
110
+
111
+ .treenode-wrapper {
112
+ display: inline-block;
113
+ }
@@ -1,161 +1,299 @@
1
1
  /*
2
2
  TreeNode Select2 Widget
3
3
 
4
- This script enhances the Select2 dropdown widget for hierarchical data
5
- representation in Django admin. It supports AJAX data fetching and ensures
6
- a structured tree-like display.
4
+ This script replaces Django's Select widget for presenting hierarchical data
5
+ in the Django admin panel. The widget is intended for a parent field, but
6
+ can be used to select a tree node with a different purpose. It provides
7
+ structured tree display.
8
+ The widget supports AJAX fetching, which avoids loading the entire tree.
7
9
 
8
10
  Features:
9
- - Dynamically initializes Select2 on elements with the class `tree-widget`.
10
- - Retrieves data via AJAX and displays it with proper indentation.
11
- - Supports dark mode and automatically applies theme styling.
11
+ - Dynamically initializes select-like elements with the `tree-widget` class.
12
+ - Fetches data via AJAX and displays it with the correct indentation.
13
+ - Supports dark mode and automatically applies theme styles.
12
14
  - Handles parent-child relationships and updates node priorities.
13
15
 
14
- Version: 2.0.0
16
+ Version: 2.1.0
15
17
  Author: Timur Kady
16
18
  Email: timurkady@yandex.com
17
19
  */
18
20
 
19
-
20
21
  (function ($) {
21
- "use strict";
22
-
23
- /**
24
- * Initializes Select2 on all elements with the class "tree-widget".
25
- * Ensures proper AJAX data fetching and hierarchical display.
26
- */
27
- function initializeSelect2() {
28
- $(".tree-widget").each(function () {
29
- var $widget = $(this);
30
- var url = $widget.data("url"); // Fetch the data URL for AJAX requests
31
-
32
- if (!url) {
33
- console.error("Error: Missing data-url for", $widget.attr("id"));
34
- return;
35
- }
22
+ "use strict";
23
+
24
+ var TreeWidget = {
25
+ // Initialize each widget by container with class .tree-widget
26
+ init: function (selector) {
27
+ $(selector).each(function () {
28
+ var $widget = $(this);
29
+
30
+ // Find the hidden input, dropdown list and display area inside
31
+ // the container
32
+ var $select = $widget.find('input[type="hidden"]').first();
33
+ var $dropdown = $widget.find('.tree-widget-dropdown').first();
34
+ var $display = $widget.find('.tree-widget-display').first();
36
35
 
37
- // Initialize Select2 with AJAX support
38
- $widget.select2({
39
- ajax: {
40
- url: url,
41
- dataType: "json",
42
- delay: 250, // Introduces a delay to avoid excessive API calls
43
- data: function (params) {
44
- var forwardData = $widget.data("forward") || {}; // Retrieve forwarded model data
45
- return {
46
- q: params.term, // Search query parameter
47
- model: forwardData.model || null, // Pass the model name
48
- };
49
- },
50
- processResults: function (data) {
51
- if (!data.results) {
52
- return { results: [] }; // Return an empty array if no results exist
53
- }
54
- return { results: data.results };
55
- },
56
- },
57
- minimumInputLength: 0, // Allows opening the dropdown without typing
58
- allowClear: true, // Enables the "clear selection" button
59
- width: "100%", // Expands the dropdown to fit the parent container
60
- templateResult: formatTreeResult, // Custom rendering function for hierarchical display
61
- });
62
-
63
- // Immediately apply theme styling after Select2 initialization
64
- var select2Instance = $widget.data("select2");
65
- if (select2Instance && isDarkTheme()) {
66
- select2Instance.$container
67
- .find(".select2-selection--single")
68
- .addClass("dark-theme"); // Apply dark mode styling if enabled
36
+ // Get URL for AJAX and model data from data attributes
37
+ var ajaxUrl = $select.data('url');
38
+ var ajaxUrlChildren = $select.data('url-children');
39
+ var forwardData = $select.attr("data-forward");
40
+ if (typeof forwardData === "string" && forwardData.trim().length > 0) {
41
+ try {
42
+ forwardData = JSON.parse(forwardData.replace(/"/g, '"'));
43
+ } catch (e) {
44
+ console.error("Invalid JSON in data-forward:", forwardData, e);
45
+ forwardData = {};
69
46
  }
47
+ } else {
48
+ forwardData = {};
49
+ }
50
+
51
+ var selectedId = $select.data('selected'); // Получаем значение data-selected
52
+ if (selectedId === undefined) {
53
+ selectedId = "";
54
+ }
55
+
56
+ var widgetData = {
57
+ $widget: $widget,
58
+ $select: $select,
59
+ $dropdown: $dropdown,
60
+ $display: $display,
61
+ ajaxUrl: ajaxUrl,
62
+ urlChildren: ajaxUrlChildren,
63
+ model: forwardData.model || '',
64
+ selectedId: selectedId,
65
+ mode: selectedId ? 'selected' : 'default'
66
+ };
67
+
68
+ $widget.data('widgetData', widgetData);
69
+
70
+ // Load data for the current mode (default, selected or search)
71
+ TreeWidget.loadData(widgetData);
72
+
73
+ // Bind event handlers
74
+ TreeWidget.bindEvents(widgetData);
75
+ });
76
+ },
77
+
78
+ // Method of loading data via AJAX
79
+ loadData: function (widgetData, searchQuery) {
80
+ var params = {model: widgetData.model};
81
+ if (searchQuery) {
82
+ params.q = searchQuery;
83
+ widgetData.mode = 'search';
84
+ } else if (widgetData.selectedId) {
85
+ params.select_id = widgetData.selectedId;
86
+ widgetData.mode = 'selected';
87
+ } else {
88
+ widgetData.mode = 'default';
89
+ }
90
+
91
+ $.ajax({
92
+ url: widgetData.ajaxUrl,
93
+ data: params,
94
+ dataType: 'json',
95
+ success: function (response) {
96
+ var $treeList = widgetData.$dropdown.find('.tree-list');
97
+ $treeList.empty();
98
+ TreeWidget.renderNodes(response.results, $treeList);
99
+ // If needed, you can show a dropdown after loading the data
100
+ // widgetData.$dropdown.show();
101
+ },
102
+ error: function (error) {
103
+ console.error("Error loading data:", params, error);
104
+ }
105
+ });
106
+ },
107
+
108
+ // Method for formatting a node
109
+ formatNode: function(node, levelOverride) {
110
+ // If levelOverride is passed, use it, otherwise take node.level
111
+ var level = (typeof levelOverride !== 'undefined') ? levelOverride : parseInt(node.level, 10);
112
+ var $li = $('<li></li>')
113
+ .addClass('tree-node')
114
+ .attr('data-id', node.id)
115
+ .attr('data-level', level);
116
+ var indent = level * 20;
117
+ $li.css('margin-left', indent + 'px');
118
+
119
+ // If the node is not a leaf node, add a button to expand it,
120
+ // otherwise insert an empty element for alignment
121
+ if (!node.is_leaf) {
122
+ var $expandBtn = $('<button type="button" class="expand-button">⏵</button>');
123
+ $li.append($expandBtn);
124
+ $li.append('<span class="node-icon">📁</span>').css({
125
+ display: 'inline-block'
126
+ })
127
+ } else {
128
+ $li.append($('<span class="no-expand"></span>').css({
129
+ display: 'inline-block'
130
+ }));
131
+ $li.append('<span class="node-icon">📄</span>').css({
132
+ display: 'inline-block'
133
+ })
134
+ }
135
+
136
+ $li.append($('<span class="node-text"></span>').text(node.text));
137
+
138
+ return $li;
139
+ },
140
+
141
+ // Method of drawing a node
142
+ renderNodes: function (nodes, $container) {
143
+ $container.empty();
144
+ $.each(nodes, function (index, node) {
145
+ var $nodeElem = TreeWidget.formatNode(node);
146
+ $container.append($nodeElem);
147
+ });
148
+ },
149
+
150
+ // Handler for clicking on the node's expand button
151
+ expandNode: function ($button, widgetData) {
152
+ var $li = $button.closest('li.tree-node');
153
+ var nodeId = $li.data('id');
154
+ if ($button.data('expanded')) {
155
+ TreeWidget.collapseNode($li);
156
+ $button.text('⏵').data('expanded', false);
157
+ } else {
158
+ $.ajax({
159
+ url: widgetData.urlChildren,
160
+ data: { model: widgetData.model, reference_id: nodeId },
161
+ dataType: 'json',
162
+ success: function (response) {
163
+ var parentLevel = parseInt($li.data('level'), 10);
164
+ var $childrenFragment = $();
165
+ $.each(response.results, function (index, node) {
166
+ var $childLi = TreeWidget.formatNode(node, parentLevel + 1);
167
+ $childrenFragment = $childrenFragment.add($childLi);
168
+ });
169
+ $li.after($childrenFragment);
170
+ $button.text('⏷').data('expanded', true);
171
+ },
172
+ error: function (data, error) {
173
+ console.error("Error loading children: ", data, error);
174
+ }
70
175
  });
71
- }
176
+ }
177
+ },
72
178
 
73
- /**
74
- * Checks whether dark mode is enabled.
75
- * It relies on the presence of the `data-theme="dark"` attribute on the <html> tag.
76
- * @returns {boolean} - True if dark mode is active, false otherwise.
77
- */
78
- function isDarkTheme() {
79
- return document.documentElement.getAttribute("data-theme") === "dark";
80
- }
179
+ // Node collapse method
180
+ // Remove child elements that are higher than the parent
181
+ collapseNode: function ($li) {
182
+ var currentLevel = parseInt($li.data('level'), 10);
183
+ var $next = $li.next();
184
+ while ($next.length && parseInt($next.data('level'), 10) > currentLevel) {
185
+ var $temp = $next.next();
186
+ $next.remove();
187
+ $next = $temp;
188
+ }
189
+ },
81
190
 
82
- /**
83
- * Applies or removes the `.dark-theme` class to the Select2 dropdown and container.
84
- * Ensures the dropdown styling follows the selected theme.
85
- */
86
- function applyTheme() {
87
- var dark = isDarkTheme(); // Check if dark mode is enabled
88
- var $dropdown = $(".select2-container--open .select2-dropdown"); // Get the currently open dropdown
89
- var $container = $(".select2-container--open .select2-selection--single"); // Get the selection box
90
-
91
- if (dark) {
92
- $dropdown.addClass("dark-theme");
93
- $container.addClass("dark-theme");
191
+ // Binding events for widget operation
192
+ bindEvents: function (widgetData) {
193
+ var $dropdown = widgetData.$dropdown;
194
+ var $widget = widgetData.$widget;
195
+ var $select = widgetData.$select;
196
+ var $display = widgetData.$display;
197
+
198
+ // Handle click on node expand button
199
+ $dropdown.on('click', '.expand-button', function (e) {
200
+ e.preventDefault();
201
+ e.stopPropagation();
202
+ TreeWidget.expandNode($(this), widgetData);
203
+ });
204
+
205
+ // Handle node selection (click on node text)
206
+ $dropdown.on('click', 'li.tree-node', function (e) {
207
+ e.preventDefault();
208
+ e.stopPropagation();
209
+ var $li = $(this).closest('li.tree-node');
210
+ var nodeId = $li.data('id');
211
+ $select.val(nodeId);
212
+ $select.data('selected', nodeId);
213
+ // Update the displayed selected value
214
+ $widget.find('.selected-node').text($(this).text());
215
+ $dropdown.hide();
216
+ });
217
+
218
+ // Processing input in the search field
219
+ $dropdown.on('keyup', '.tree-search', function (e) {
220
+ var query = $(this).val();
221
+ if (query.length > 0) {
222
+ TreeWidget.loadData(widgetData, query);
94
223
  } else {
95
- $dropdown.removeClass("dark-theme");
96
- $container.removeClass("dark-theme");
224
+ TreeWidget.loadData(widgetData);
97
225
  }
98
- }
226
+ });
99
227
 
100
- /**
101
- * Formats each result in the Select2 dropdown to visually represent hierarchy.
102
- * Adds indentation based on node depth and assigns folder/file icons.
103
- * @param {Object} result - A single result object from the AJAX response.
104
- * @returns {jQuery} - A formatted span element with the tree structure.
105
- */
106
- function formatTreeResult(result) {
107
- if (!result.id) {
108
- return result.text; // Return plain text for placeholder options
228
+ // Handle clicking on the search field clear button
229
+ $dropdown.on('click', '.tree-search-clear', function (e) {
230
+ e.preventDefault();
231
+ e.stopPropagation();
232
+ var $search = $dropdown.find('.tree-search');
233
+ $search.val('');
234
+ TreeWidget.loadData(widgetData);
235
+ });
236
+
237
+ // Toggle the visibility of the dropdown list when clicking on the
238
+ // display area
239
+ $display.on('click', function (e) {
240
+ e.preventDefault();
241
+ $dropdown.toggle();
242
+ });
243
+
244
+ // Hide the dropdown when clicking outside the widget
245
+ $(document).on('click', function (e) {
246
+ if (!$widget.is(e.target) && $widget.has(e.target).length === 0) {
247
+ $dropdown.hide();
109
248
  }
110
- var level = result.level || 0; // Retrieve node depth (default: 0)
111
- var is_leaf = result.is_leaf || false; // Determine if it's a leaf node
112
- var indent = "&nbsp;&nbsp;".repeat(level); // Create indentation based on depth
113
- var icon = is_leaf ? "📄 " : "📂 "; // Use 📄 for leaves, 📂 for parent nodes
114
- return $("<span>" + indent + icon + result.text + "</span>"); // Return formatted text
249
+ });
115
250
  }
251
+ };
116
252
 
117
- /**
118
- * Binds event listeners and initializes Select2.
119
- * Ensures correct theme application on page load and during interactions.
120
- */
121
- $(document).ready(function () {
122
- initializeSelect2();
123
- applyTheme();
124
-
125
- // When a Select2 dropdown opens, update its theme
126
- $(document).on("select2:open", function () {
127
- applyTheme();
128
- });
129
253
 
130
- // When the theme toggle button is clicked, reapply the theme
131
- $(document).on("click", ".theme-toggle", function () {
132
- applyTheme();
133
- });
254
+ /**
255
+ * Checks whether dark mode is enabled.
256
+ * It relies on the presence of the `data-theme="dark"` attribute on the
257
+ * <html> tag.
258
+ * @returns {boolean} - True if dark mode is active, false otherwise.
259
+ */
260
+ function isDarkTheme() {
261
+ return document.documentElement.getAttribute("data-theme") === "dark";
262
+ }
134
263
 
135
- // When a parent changes, get the number of its children and set tn_priority
136
- $("#id_tn_parent").on("change", function () {
137
- var parentId = $(this).val();
138
- var model = $(this).data("forward") ? $(this).data("forward").model : null;
264
+ /**
265
+ * Applies or removes the `.dark-theme` class to the Select2 dropdown and
266
+ * container. Ensures the dropdown styling follows the selected theme.
267
+ */
268
+ function applyTheme() {
269
+ var dark = isDarkTheme(); // Check if dark mode is enabled
270
+ var $container = $(".tree-widget");
271
+ var $dropdown = $(".tree-widget-dropdown");
139
272
 
140
- if (!parentId || !model) {
141
- console.log("No parent selected or model is missing.");
142
- return;
143
- }
273
+ if (dark) {
274
+ $dropdown.addClass("dark-theme");
275
+ $container.addClass("dark-theme");
276
+ } else {
277
+ $dropdown.removeClass("dark-theme");
278
+ $container.removeClass("dark-theme");
279
+ }
280
+ }
144
281
 
145
- $.ajax({
146
- url: "/treenode/get-children-count/",
147
- data: { parent_id: parentId, model: model },
148
- dataType: "json",
149
- success: function (response) {
150
- if (response.children_count !== undefined) {
151
- $("#id_tn_priority").val(response.children_count); // Set the value
152
- }
153
- },
154
- error: function () {
155
- console.error("Failed to fetch children count.");
156
- }
157
- });
158
- });
282
+ // Initialize the widget when the page loads using the .tree-widget container
283
+ $(document).ready(function () {
284
+ applyTheme();
285
+ TreeWidget.init('.tree-widget');
286
+
287
+ // When a widget dropdown opens, update its theme
288
+ // $(document).on("select2:open", function () {
289
+ // applyTheme();
290
+ // });
291
+
292
+ // When the theme toggle button is clicked, reapply the theme
293
+ $(document).on("click", ".theme-toggle", function () {
294
+ applyTheme();
159
295
  });
296
+
297
+ });
160
298
 
161
299
  })(django.jQuery || window.jQuery);