django-fast-treenode 2.1.4__py3-none-any.whl → 3.0.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.
- django_fast_treenode-3.0.1.dist-info/METADATA +203 -0
- django_fast_treenode-3.0.1.dist-info/RECORD +90 -0
- {django_fast_treenode-2.1.4.dist-info → django_fast_treenode-3.0.1.dist-info}/WHEEL +1 -1
- treenode/admin/__init__.py +2 -7
- treenode/admin/admin.py +138 -209
- treenode/admin/changelist.py +21 -39
- treenode/admin/exporter.py +170 -0
- treenode/admin/importer.py +171 -0
- treenode/admin/mixin.py +291 -0
- treenode/apps.py +41 -19
- treenode/cache.py +192 -303
- treenode/forms.py +45 -65
- treenode/managers/__init__.py +4 -20
- treenode/managers/managers.py +216 -0
- treenode/managers/queries.py +233 -0
- treenode/managers/tasks.py +167 -0
- treenode/models/__init__.py +8 -5
- treenode/models/decorators.py +54 -0
- treenode/models/factory.py +44 -68
- treenode/models/mixins/__init__.py +2 -1
- treenode/models/mixins/ancestors.py +44 -20
- treenode/models/mixins/children.py +33 -26
- treenode/models/mixins/descendants.py +33 -22
- treenode/models/mixins/family.py +25 -15
- treenode/models/mixins/logical.py +23 -21
- treenode/models/mixins/node.py +162 -104
- treenode/models/mixins/properties.py +22 -16
- treenode/models/mixins/roots.py +59 -15
- treenode/models/mixins/siblings.py +46 -43
- treenode/models/mixins/tree.py +212 -153
- treenode/models/mixins/update.py +154 -0
- treenode/models/models.py +365 -0
- treenode/settings.py +28 -0
- treenode/static/{treenode/css → css}/tree_widget.css +1 -1
- treenode/static/{treenode/css → css}/treenode_admin.css +43 -2
- treenode/static/css/treenode_tabs.css +51 -0
- treenode/static/js/lz-string.min.js +1 -0
- treenode/static/{treenode/js → js}/tree_widget.js +9 -23
- treenode/static/js/treenode_admin.js +531 -0
- treenode/static/vendors/jquery-ui/AUTHORS.txt +384 -0
- treenode/static/vendors/jquery-ui/LICENSE.txt +43 -0
- treenode/static/vendors/jquery-ui/external/jquery/jquery.js +10716 -0
- treenode/static/vendors/jquery-ui/images/ui-icons_444444_256x240.png +0 -0
- treenode/static/vendors/jquery-ui/images/ui-icons_555555_256x240.png +0 -0
- treenode/static/vendors/jquery-ui/images/ui-icons_777620_256x240.png +0 -0
- treenode/static/vendors/jquery-ui/images/ui-icons_777777_256x240.png +0 -0
- treenode/static/vendors/jquery-ui/images/ui-icons_cc0000_256x240.png +0 -0
- treenode/static/vendors/jquery-ui/images/ui-icons_ffffff_256x240.png +0 -0
- treenode/static/vendors/jquery-ui/index.html +297 -0
- treenode/static/vendors/jquery-ui/jquery-ui.css +438 -0
- treenode/static/vendors/jquery-ui/jquery-ui.js +5223 -0
- treenode/static/vendors/jquery-ui/jquery-ui.min.css +7 -0
- treenode/static/vendors/jquery-ui/jquery-ui.min.js +6 -0
- treenode/static/vendors/jquery-ui/jquery-ui.structure.css +16 -0
- treenode/static/vendors/jquery-ui/jquery-ui.structure.min.css +5 -0
- treenode/static/vendors/jquery-ui/jquery-ui.theme.css +439 -0
- treenode/static/vendors/jquery-ui/jquery-ui.theme.min.css +5 -0
- treenode/static/vendors/jquery-ui/package.json +82 -0
- treenode/templates/admin/treenode_changelist.html +25 -0
- treenode/templates/admin/treenode_import_export.html +85 -0
- treenode/templates/admin/treenode_rows.html +57 -0
- treenode/tests.py +3 -0
- treenode/urls.py +6 -27
- treenode/utils/__init__.py +0 -15
- treenode/utils/db/__init__.py +7 -0
- treenode/utils/db/compiler.py +114 -0
- treenode/utils/db/db_vendor.py +50 -0
- treenode/utils/db/service.py +84 -0
- treenode/utils/db/sqlcompat.py +60 -0
- treenode/utils/db/sqlquery.py +70 -0
- treenode/version.py +2 -2
- treenode/views/__init__.py +5 -0
- treenode/views/autoapi.py +91 -0
- treenode/views/autocomplete.py +52 -0
- treenode/views/children.py +41 -0
- treenode/views/common.py +23 -0
- treenode/views/crud.py +209 -0
- treenode/views/search.py +48 -0
- treenode/widgets.py +27 -44
- django_fast_treenode-2.1.4.dist-info/METADATA +0 -166
- django_fast_treenode-2.1.4.dist-info/RECORD +0 -63
- treenode/admin/mixins.py +0 -302
- treenode/managers/adjacency.py +0 -205
- treenode/managers/closure.py +0 -278
- treenode/models/adjacency.py +0 -342
- treenode/models/classproperty.py +0 -27
- treenode/models/closure.py +0 -122
- treenode/static/treenode/js/.gitkeep +0 -1
- treenode/static/treenode/js/treenode_admin.js +0 -131
- treenode/templates/admin/export_success.html +0 -26
- treenode/templates/admin/tree_node_changelist.html +0 -19
- treenode/templates/admin/tree_node_export.html +0 -27
- treenode/templates/admin/tree_node_import.html +0 -45
- treenode/templates/admin/tree_node_import_report.html +0 -32
- treenode/templates/widgets/tree_widget.css +0 -23
- treenode/utils/aid.py +0 -46
- treenode/utils/base16.py +0 -38
- treenode/utils/base36.py +0 -37
- treenode/utils/db.py +0 -116
- treenode/utils/exporter.py +0 -196
- treenode/utils/importer.py +0 -328
- treenode/utils/radix.py +0 -61
- treenode/views.py +0 -184
- {django_fast_treenode-2.1.4.dist-info → django_fast_treenode-3.0.1.dist-info/licenses}/LICENSE +0 -0
- {django_fast_treenode-2.1.4.dist-info → django_fast_treenode-3.0.1.dist-info}/top_level.txt +0 -0
- /treenode/static/{treenode → css}/.gitkeep +0 -0
- /treenode/static/{treenode/css → js}/.gitkeep +0 -0
treenode/models/closure.py
DELETED
@@ -1,122 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
"""
|
3
|
-
TreeNode Closure Model
|
4
|
-
|
5
|
-
This module defines the Closure Table implementation for hierarchical
|
6
|
-
data storage in the TreeNode model. It supports efficient queries for
|
7
|
-
retrieving ancestors, descendants, breadcrumbs, and tree depth.
|
8
|
-
|
9
|
-
Features:
|
10
|
-
- Uses a Closure Table for efficient tree operations.
|
11
|
-
- Implements cached queries for improved performance.
|
12
|
-
- Provides bulk operations for inserting, moving, and deleting nodes.
|
13
|
-
|
14
|
-
Version: 2.1.0
|
15
|
-
Author: Timur Kady
|
16
|
-
Email: timurkady@yandex.com
|
17
|
-
"""
|
18
|
-
|
19
|
-
|
20
|
-
from django.db import models, transaction
|
21
|
-
from django.db.models.signals import pre_save, post_save
|
22
|
-
|
23
|
-
from ..managers import ClosureModelManager
|
24
|
-
from ..signals import disable_signals
|
25
|
-
|
26
|
-
|
27
|
-
class ClosureModel(models.Model):
|
28
|
-
"""
|
29
|
-
Model for Closure Table.
|
30
|
-
|
31
|
-
Implements hierarchy storage using the Closure Table method.
|
32
|
-
"""
|
33
|
-
|
34
|
-
parent = models.ForeignKey(
|
35
|
-
'TreeNodeModel',
|
36
|
-
related_name='children_set',
|
37
|
-
on_delete=models.CASCADE,
|
38
|
-
)
|
39
|
-
|
40
|
-
child = models.ForeignKey(
|
41
|
-
'TreeNodeModel',
|
42
|
-
related_name='parents_set',
|
43
|
-
on_delete=models.CASCADE,
|
44
|
-
)
|
45
|
-
|
46
|
-
depth = models.PositiveIntegerField()
|
47
|
-
|
48
|
-
node = models.OneToOneField(
|
49
|
-
'TreeNodeModel',
|
50
|
-
related_name="tn_closure",
|
51
|
-
on_delete=models.CASCADE,
|
52
|
-
null=True,
|
53
|
-
blank=True,
|
54
|
-
)
|
55
|
-
|
56
|
-
objects = ClosureModelManager()
|
57
|
-
|
58
|
-
class Meta:
|
59
|
-
"""Meta Class."""
|
60
|
-
|
61
|
-
abstract = True
|
62
|
-
unique_together = (("parent", "child"),)
|
63
|
-
indexes = [
|
64
|
-
models.Index(fields=["parent", "child"]),
|
65
|
-
models.Index(fields=["parent", "depth"]),
|
66
|
-
models.Index(fields=["child", "depth"]),
|
67
|
-
models.Index(fields=["parent", "child", "depth"]),
|
68
|
-
]
|
69
|
-
|
70
|
-
def __str__(self):
|
71
|
-
"""Display information about a class object."""
|
72
|
-
return f"{self.parent} — {self.child} — {self.depth}"
|
73
|
-
|
74
|
-
# ----------- Methods of working with tree structure ----------- #
|
75
|
-
|
76
|
-
@classmethod
|
77
|
-
def get_root(cls, node):
|
78
|
-
"""Get the root node pk for the current node."""
|
79
|
-
queryset = cls.objects.filter(child=node).order_by('-depth')
|
80
|
-
return queryset.first().parent if queryset.count() > 0 else None
|
81
|
-
|
82
|
-
@classmethod
|
83
|
-
def get_depth(cls, node):
|
84
|
-
"""Get the node depth (how deep the node is in the tree)."""
|
85
|
-
result = cls.objects.filter(child__pk=node.pk).aggregate(
|
86
|
-
models.Max("depth")
|
87
|
-
)["depth__max"]
|
88
|
-
return result if result is not None else 0
|
89
|
-
|
90
|
-
@classmethod
|
91
|
-
def get_level(cls, node):
|
92
|
-
"""Get the node level (starting from 1)."""
|
93
|
-
return cls.objects.filter(child__pk=node.pk).aggregate(
|
94
|
-
models.Max("depth"))["depth__max"] + 1
|
95
|
-
|
96
|
-
@classmethod
|
97
|
-
@transaction.atomic
|
98
|
-
def insert_node(cls, node):
|
99
|
-
"""Add a node to a Closure table."""
|
100
|
-
# Call bulk_create passing a single object
|
101
|
-
cls.objects.bulk_create([node], batch_size=1000)
|
102
|
-
|
103
|
-
@classmethod
|
104
|
-
@transaction.atomic
|
105
|
-
def move_node(cls, nodes):
|
106
|
-
"""Move a nodes (node and its subtree) to a new parent."""
|
107
|
-
# Call bulk_update passing a single object
|
108
|
-
cls.objects.bulk_update(nodes, batch_size=1000)
|
109
|
-
|
110
|
-
@classmethod
|
111
|
-
@transaction.atomic
|
112
|
-
def delete_all(cls):
|
113
|
-
"""Clear the Closure Table."""
|
114
|
-
cls.objects.all().delete()
|
115
|
-
|
116
|
-
def save(self, force_insert=False, *args, **kwargs):
|
117
|
-
"""Save method."""
|
118
|
-
with (disable_signals(pre_save, self._meta.model),
|
119
|
-
disable_signals(post_save, self._meta.model)):
|
120
|
-
super().save(force_insert, *args, **kwargs)
|
121
|
-
|
122
|
-
# The End
|
@@ -1 +0,0 @@
|
|
1
|
-
|
@@ -1,131 +0,0 @@
|
|
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"> </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);
|
@@ -1,26 +0,0 @@
|
|
1
|
-
{% extends "admin/base_site.html" %}
|
2
|
-
{% load i18n %}
|
3
|
-
{% block title %}{% trans "Export Successful" %}{% endblock %}
|
4
|
-
|
5
|
-
{% block content %}
|
6
|
-
<div class="module" style="margin-top: 20px;">
|
7
|
-
<h2>{{ message }}</h2>
|
8
|
-
<div class="module ">
|
9
|
-
<p>{{ manual_download_label }}</p>
|
10
|
-
<p><a id="dl" href="{{ download_url }}" class="button">Download</a></p>
|
11
|
-
</div>
|
12
|
-
<p>
|
13
|
-
<input type="button" class="button" value="{{ button_text }}" onclick="window.location.href='{{ redirect_url }}';">
|
14
|
-
</p>
|
15
|
-
|
16
|
-
</div>
|
17
|
-
|
18
|
-
<script type="text/javascript">
|
19
|
-
// When the page loads, create a hidden iframe to initiate the download
|
20
|
-
|
21
|
-
window.onload = function() {
|
22
|
-
document.getElementById("dl").click();
|
23
|
-
};
|
24
|
-
</script>
|
25
|
-
{% endblock %}
|
26
|
-
|
@@ -1,19 +0,0 @@
|
|
1
|
-
{% extends "admin/change_list.html" %}
|
2
|
-
|
3
|
-
{% block object-tools-items %}
|
4
|
-
{{ block.super }}
|
5
|
-
{% if import_export_enabled %}
|
6
|
-
<li>
|
7
|
-
<a href="import/" class="button">Import</a>
|
8
|
-
</li>
|
9
|
-
<li>
|
10
|
-
<a href="export/" class="button">Export</a>
|
11
|
-
</li>
|
12
|
-
{% endif %}
|
13
|
-
{% endblock %}
|
14
|
-
|
15
|
-
{% block extrahead %}
|
16
|
-
|
17
|
-
{{ block.super }}
|
18
|
-
|
19
|
-
{% endblock %}
|
@@ -1,27 +0,0 @@
|
|
1
|
-
{% extends "admin/base_site.html" %}
|
2
|
-
{% load i18n %}
|
3
|
-
{% block content %}
|
4
|
-
<div class="module">
|
5
|
-
<h2>{% trans "Export Tree Node Data" %}</h2>
|
6
|
-
<form method="get" class="form-horizontal" style="margin-top: 20px;">
|
7
|
-
<div class="form-group">
|
8
|
-
<label for="format" class="col-sm-2 control-label">{% trans "Select format:" %}</label>
|
9
|
-
<div class="col-sm-10">
|
10
|
-
<select name="format" id="format" class="form-control">
|
11
|
-
<option value="csv">CSV</option>
|
12
|
-
<option value="json">JSON</option>
|
13
|
-
<option value="xlsx">{% trans "Excel (XLSX)" %}</option>
|
14
|
-
<option value="yaml">YAML</option>
|
15
|
-
<option value="tsv">TSV</option>
|
16
|
-
</select>
|
17
|
-
</div>
|
18
|
-
</div>
|
19
|
-
<div class="form-group" style="margin-top: 35px;">
|
20
|
-
<div class="col-sm-offset-2 col-sm-10">
|
21
|
-
<button type="submit" class="button">{% trans "Export" %}</button>
|
22
|
-
<a href=".." class="button default">{% trans "Cancel" %}</a>
|
23
|
-
</div>
|
24
|
-
</div>
|
25
|
-
</form>
|
26
|
-
</div>
|
27
|
-
{% endblock %}
|
@@ -1,45 +0,0 @@
|
|
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
|
-
|
8
|
-
{% block content %}
|
9
|
-
<div class="module">
|
10
|
-
<h2>Import Data</h2>
|
11
|
-
|
12
|
-
{% if message %}
|
13
|
-
<div class="successnote">
|
14
|
-
<p>{{ message }}</p>
|
15
|
-
</div>
|
16
|
-
{% endif %}
|
17
|
-
|
18
|
-
{% if errors %}
|
19
|
-
<div class="errornote">
|
20
|
-
<p>Errors occurred while importing the data:</p>
|
21
|
-
<ul>
|
22
|
-
{% for error in errors %}
|
23
|
-
<li>{{ error }}</li>
|
24
|
-
{% endfor %}
|
25
|
-
</ul>
|
26
|
-
</div>
|
27
|
-
{% endif %}
|
28
|
-
|
29
|
-
<form id="importForm" method="post" enctype="multipart/form-data" style="margin: 20px 0;">
|
30
|
-
{% csrf_token %}
|
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='..';">
|
41
|
-
</div>
|
42
|
-
</form>
|
43
|
-
|
44
|
-
</div>
|
45
|
-
{% endblock %}
|
@@ -1,32 +0,0 @@
|
|
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 %}
|
@@ -1,23 +0,0 @@
|
|
1
|
-
/* Light mode */
|
2
|
-
.select2-container--default.select2-light .select2-selection {
|
3
|
-
background-color: #fff;
|
4
|
-
color: #333;
|
5
|
-
border: 1px solid #ccc;
|
6
|
-
}
|
7
|
-
|
8
|
-
.select2-container--default.select2-light .select2-dropdown {
|
9
|
-
background-color: #fff;
|
10
|
-
color: #333;
|
11
|
-
}
|
12
|
-
|
13
|
-
/* Dark mode */
|
14
|
-
.select2-container--default.select2-dark .select2-selection {
|
15
|
-
background-color: #222;
|
16
|
-
color: #ddd;
|
17
|
-
border: 1px solid #444;
|
18
|
-
}
|
19
|
-
|
20
|
-
.select2-container--default.select2-dark .select2-dropdown {
|
21
|
-
background-color: #222;
|
22
|
-
color: #ddd;
|
23
|
-
}
|
treenode/utils/aid.py
DELETED
@@ -1,46 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
"""
|
3
|
-
Aid Utility Module
|
4
|
-
|
5
|
-
This module provides various helper functions.
|
6
|
-
|
7
|
-
Version: 2.1.0
|
8
|
-
Author: Timur Kady
|
9
|
-
Email: timurkady@yandex.com
|
10
|
-
"""
|
11
|
-
|
12
|
-
|
13
|
-
from django.utils.safestring import mark_safe
|
14
|
-
|
15
|
-
|
16
|
-
def object_to_content(obj):
|
17
|
-
"""Convert object data to widget options string."""
|
18
|
-
level = obj.get_depth()
|
19
|
-
icon = "📄 " if obj.is_leaf() else "📁 "
|
20
|
-
obj_str = str(obj)
|
21
|
-
content = (
|
22
|
-
f'<span class="treenode-option" style="padding-left: {level * 1.5}em;">'
|
23
|
-
f'{icon}{obj_str}</span>'
|
24
|
-
)
|
25
|
-
return mark_safe(content)
|
26
|
-
|
27
|
-
|
28
|
-
def to_base16(num):
|
29
|
-
"""
|
30
|
-
Convert an integer to a base16 string.
|
31
|
-
|
32
|
-
For example: 10 -> 'A', 11 -> 'B', etc.
|
33
|
-
"""
|
34
|
-
digits = "0123456789ABCDEF"
|
35
|
-
|
36
|
-
if num == 0:
|
37
|
-
return '0'
|
38
|
-
sign = '-' if num < 0 else ''
|
39
|
-
num = abs(num)
|
40
|
-
result = []
|
41
|
-
while num:
|
42
|
-
num, rem = divmod(num, 16)
|
43
|
-
result.append(digits[rem])
|
44
|
-
return sign + ''.join(reversed(result))
|
45
|
-
|
46
|
-
# The End
|
treenode/utils/base16.py
DELETED
@@ -1,38 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
"""
|
3
|
-
Base16 Utility Module
|
4
|
-
|
5
|
-
This module provides a utility function for converting integers
|
6
|
-
to Base16 string representation.
|
7
|
-
|
8
|
-
Features:
|
9
|
-
- Converts integers into a more compact Base36 format.
|
10
|
-
- Maintains lexicographic order when padded with leading zeros.
|
11
|
-
- Supports negative numbers.
|
12
|
-
|
13
|
-
Version: 2.1.0
|
14
|
-
Author: Timur Kady
|
15
|
-
Email: timurkady@yandex.com
|
16
|
-
"""
|
17
|
-
|
18
|
-
|
19
|
-
def to_base16(num):
|
20
|
-
"""
|
21
|
-
Convert an integer to a base16 string.
|
22
|
-
|
23
|
-
For example: 10 -> 'A', 11 -> 'B', etc.
|
24
|
-
"""
|
25
|
-
digits = "0123456789ABCDEF"
|
26
|
-
|
27
|
-
if num == 0:
|
28
|
-
return '0'
|
29
|
-
sign = '-' if num < 0 else ''
|
30
|
-
num = abs(num)
|
31
|
-
result = []
|
32
|
-
while num:
|
33
|
-
num, rem = divmod(num, 16)
|
34
|
-
result.append(digits[rem])
|
35
|
-
return sign + ''.join(reversed(result))
|
36
|
-
|
37
|
-
# The End
|
38
|
-
|
treenode/utils/base36.py
DELETED
@@ -1,37 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
"""
|
3
|
-
Base36 Utility Module
|
4
|
-
|
5
|
-
This module provides a utility function for converting integers
|
6
|
-
to Base36 string representation.
|
7
|
-
|
8
|
-
Features:
|
9
|
-
- Converts integers into a more compact Base36 format.
|
10
|
-
- Maintains lexicographic order when padded with leading zeros.
|
11
|
-
- Supports negative numbers.
|
12
|
-
|
13
|
-
Version: 2.1.0
|
14
|
-
Author: Timur Kady
|
15
|
-
Email: timurkady@yandex.com
|
16
|
-
"""
|
17
|
-
|
18
|
-
|
19
|
-
def to_base36(num):
|
20
|
-
"""
|
21
|
-
Convert an integer to a base36 string.
|
22
|
-
|
23
|
-
For example: 10 -> 'A', 35 -> 'Z', 36 -> '10', etc.
|
24
|
-
"""
|
25
|
-
digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
26
|
-
|
27
|
-
if num == 0:
|
28
|
-
return '0'
|
29
|
-
sign = '-' if num < 0 else ''
|
30
|
-
num = abs(num)
|
31
|
-
result = []
|
32
|
-
while num:
|
33
|
-
num, rem = divmod(num, 36)
|
34
|
-
result.append(digits[rem])
|
35
|
-
return sign + ''.join(reversed(result))
|
36
|
-
|
37
|
-
# The End
|
treenode/utils/db.py
DELETED
@@ -1,116 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
"""
|
3
|
-
DB Vendor Utility Module
|
4
|
-
|
5
|
-
This module provides a utility function for converting integers
|
6
|
-
to Base36 string representation.
|
7
|
-
|
8
|
-
Features:
|
9
|
-
- Converts integers into a more compact Base36 format.
|
10
|
-
- Maintains lexicographic order when padded with leading zeros.
|
11
|
-
- Supports negative numbers.
|
12
|
-
|
13
|
-
Version: 2.1.0
|
14
|
-
Author: Timur Kady
|
15
|
-
Email: timurkady@yandex.com
|
16
|
-
"""
|
17
|
-
|
18
|
-
import logging
|
19
|
-
from django.apps import apps
|
20
|
-
from django.db import connection
|
21
|
-
|
22
|
-
from ..models import TreeNodeModel
|
23
|
-
|
24
|
-
logger = logging.getLogger(__name__)
|
25
|
-
|
26
|
-
|
27
|
-
def create_indexes(model):
|
28
|
-
"""Create indexes for the descendants of TreeNodeModel."""
|
29
|
-
vendor = connection.vendor
|
30
|
-
sender = "Django Fast TeeNode"
|
31
|
-
table = model._meta.db_table
|
32
|
-
|
33
|
-
with connection.cursor() as cursor:
|
34
|
-
if vendor == "postgresql":
|
35
|
-
cursor.execute(
|
36
|
-
"SELECT indexname FROM pg_indexes WHERE tablename = %s AND indexname = %s;",
|
37
|
-
[table, f"idx_{table}_btree"]
|
38
|
-
)
|
39
|
-
if not cursor.fetchone():
|
40
|
-
cursor.execute(
|
41
|
-
f"CREATE INDEX idx_{table}_btree ON {table} USING BTREE (id);"
|
42
|
-
)
|
43
|
-
logger.info(f"{sender}: GIN index for table {table} created.")
|
44
|
-
|
45
|
-
# Если существует первичный ключ, выполняем кластеризацию
|
46
|
-
cursor.execute(
|
47
|
-
"SELECT relname FROM pg_class WHERE relname = %s;",
|
48
|
-
[f"{table}_pkey"]
|
49
|
-
)
|
50
|
-
if cursor.fetchone():
|
51
|
-
cursor.execute(f"CLUSTER {table} USING {table}_pkey;")
|
52
|
-
logger.info(f"{sender}: Table {table} is clustered.")
|
53
|
-
|
54
|
-
elif vendor == "mysql":
|
55
|
-
cursor.execute("SHOW TABLE STATUS WHERE Name = %s;", [table])
|
56
|
-
columns = [col[0] for col in cursor.description]
|
57
|
-
row = cursor.fetchone()
|
58
|
-
if row:
|
59
|
-
table_status = dict(zip(columns, row))
|
60
|
-
engine = table_status.get("Engine", "").lower()
|
61
|
-
if engine != "innodb":
|
62
|
-
cursor.execute(f"ALTER TABLE {table} ENGINE = InnoDB;")
|
63
|
-
logger.info(
|
64
|
-
f"{sender}: Table {table} has been converted to InnoDB."
|
65
|
-
)
|
66
|
-
|
67
|
-
elif vendor in ["microsoft", "oracle"]:
|
68
|
-
if vendor == "microsoft":
|
69
|
-
cursor.execute(
|
70
|
-
"SELECT name FROM sys.indexes WHERE name = %s AND object_id = OBJECT_ID(%s);",
|
71
|
-
[f"idx_{table}_cluster", table]
|
72
|
-
)
|
73
|
-
else:
|
74
|
-
cursor.execute(
|
75
|
-
"SELECT index_name FROM user_indexes WHERE index_name = %s;",
|
76
|
-
[f"IDX_{table.upper()}_CLUSTER"]
|
77
|
-
)
|
78
|
-
if not cursor.fetchone():
|
79
|
-
cursor.execute(
|
80
|
-
f"CREATE CLUSTERED INDEX idx_{table}_cluster ON {table} (id);")
|
81
|
-
logger.info(
|
82
|
-
f"{sender}: CLUSTERED index for table {table} created."
|
83
|
-
)
|
84
|
-
|
85
|
-
elif vendor == "sqlite":
|
86
|
-
# Kick those on SQLite
|
87
|
-
logger.warning(
|
88
|
-
f"{sender} Unable to create GIN and CLUSTER indexes for SQLite."
|
89
|
-
)
|
90
|
-
else:
|
91
|
-
logger.warning(
|
92
|
-
f"{sender}: Unknown vendor. Index creation cancelled."
|
93
|
-
)
|
94
|
-
|
95
|
-
|
96
|
-
def post_migrate_update(sender, **kwargs):
|
97
|
-
"""Update indexes and tn_closure field only when necessary."""
|
98
|
-
# Перебираем все зарегистрированные модели
|
99
|
-
for model in apps.get_models():
|
100
|
-
# Check that the model inherits from TreeNodeModel and
|
101
|
-
# is not abstract
|
102
|
-
if issubclass(model, TreeNodeModel) and not model._meta.abstract:
|
103
|
-
# Create GIN and CLUSTER indexrs
|
104
|
-
create_indexes(model)
|
105
|
-
# Get ClosureModel
|
106
|
-
closure_model = model.closure_model
|
107
|
-
# Check node counts
|
108
|
-
al_count = model.objects.exists()
|
109
|
-
cl_counts = closure_model.objects.exclude(node=None).exists()
|
110
|
-
|
111
|
-
if al_count and not cl_counts:
|
112
|
-
# Call update_tree()
|
113
|
-
model.update_tree()
|
114
|
-
|
115
|
-
|
116
|
-
# The End
|