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.
Files changed (107) hide show
  1. django_fast_treenode-3.0.1.dist-info/METADATA +203 -0
  2. django_fast_treenode-3.0.1.dist-info/RECORD +90 -0
  3. {django_fast_treenode-2.1.4.dist-info → django_fast_treenode-3.0.1.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 +41 -19
  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.1.dist-info/licenses}/LICENSE +0 -0
  105. {django_fast_treenode-2.1.4.dist-info → django_fast_treenode-3.0.1.dist-info}/top_level.txt +0 -0
  106. /treenode/static/{treenode → css}/.gitkeep +0 -0
  107. /treenode/static/{treenode/css → js}/.gitkeep +0 -0
@@ -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">&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);
@@ -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