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.
- {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/LICENSE +2 -2
- django_fast_treenode-2.1.0.dist-info/METADATA +161 -0
- django_fast_treenode-2.1.0.dist-info/RECORD +75 -0
- {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/WHEEL +1 -1
- treenode/admin/__init__.py +9 -0
- treenode/admin/admin.py +295 -0
- treenode/admin/changelist.py +65 -0
- treenode/admin/mixins.py +302 -0
- treenode/apps.py +12 -1
- treenode/cache.py +2 -2
- treenode/docs/.gitignore +0 -0
- treenode/docs/about.md +36 -0
- treenode/docs/admin.md +104 -0
- treenode/docs/api.md +739 -0
- treenode/docs/cache.md +187 -0
- treenode/docs/import_export.md +35 -0
- treenode/docs/index.md +30 -0
- treenode/docs/installation.md +74 -0
- treenode/docs/migration.md +145 -0
- treenode/docs/models.md +128 -0
- treenode/docs/roadmap.md +45 -0
- treenode/forms.py +33 -22
- treenode/managers/__init__.py +21 -0
- treenode/managers/adjacency.py +203 -0
- treenode/managers/closure.py +278 -0
- treenode/models/__init__.py +2 -1
- treenode/models/adjacency.py +343 -0
- treenode/models/classproperty.py +3 -0
- treenode/models/closure.py +39 -65
- treenode/models/factory.py +12 -2
- treenode/models/mixins/__init__.py +23 -0
- treenode/models/mixins/ancestors.py +65 -0
- treenode/models/mixins/children.py +81 -0
- treenode/models/mixins/descendants.py +66 -0
- treenode/models/mixins/family.py +63 -0
- treenode/models/mixins/logical.py +68 -0
- treenode/models/mixins/node.py +210 -0
- treenode/models/mixins/properties.py +156 -0
- treenode/models/mixins/roots.py +96 -0
- treenode/models/mixins/siblings.py +99 -0
- treenode/models/mixins/tree.py +344 -0
- treenode/signals.py +26 -0
- treenode/static/treenode/css/tree_widget.css +201 -31
- treenode/static/treenode/css/treenode_admin.css +48 -41
- treenode/static/treenode/js/tree_widget.js +269 -131
- treenode/static/treenode/js/treenode_admin.js +131 -171
- treenode/templates/admin/tree_node_changelist.html +6 -0
- treenode/templates/admin/tree_node_import.html +27 -9
- treenode/templates/admin/tree_node_import_report.html +32 -0
- treenode/templates/admin/treenode_ajax_rows.html +7 -0
- treenode/tests/tests.py +488 -0
- treenode/urls.py +10 -6
- treenode/utils/__init__.py +2 -0
- treenode/utils/aid.py +46 -0
- treenode/utils/base16.py +38 -0
- treenode/utils/base36.py +3 -1
- treenode/utils/db.py +116 -0
- treenode/utils/exporter.py +63 -36
- treenode/utils/importer.py +168 -161
- treenode/utils/radix.py +61 -0
- treenode/version.py +2 -2
- treenode/views.py +119 -38
- treenode/widgets.py +104 -40
- django_fast_treenode-2.0.10.dist-info/METADATA +0 -698
- django_fast_treenode-2.0.10.dist-info/RECORD +0 -41
- treenode/admin.py +0 -396
- treenode/docs/Documentation +0 -664
- treenode/managers.py +0 -281
- treenode/models/proxy.py +0 -650
- {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/top_level.txt +0 -0
treenode/utils/db.py
ADDED
@@ -0,0 +1,116 @@
|
|
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
|
treenode/utils/exporter.py
CHANGED
@@ -12,7 +12,7 @@ Features:
|
|
12
12
|
- Provides optimized data extraction for QuerySets.
|
13
13
|
- Generates downloadable files with appropriate HTTP responses.
|
14
14
|
|
15
|
-
Version: 2.0.
|
15
|
+
Version: 2.0.11
|
16
16
|
Author: Timur Kady
|
17
17
|
Email: timurkady@yandex.com
|
18
18
|
"""
|
@@ -23,6 +23,7 @@ import json
|
|
23
23
|
import yaml
|
24
24
|
import xlsxwriter
|
25
25
|
import numpy as np
|
26
|
+
import uuid
|
26
27
|
from io import BytesIO
|
27
28
|
from django.http import HttpResponse
|
28
29
|
import logging
|
@@ -43,6 +44,7 @@ class TreeNodeExporter:
|
|
43
44
|
self.queryset = queryset
|
44
45
|
self.filename = filename
|
45
46
|
self.fields = [field.name for field in queryset.model._meta.fields]
|
47
|
+
self.fields = self.get_ordered_fields()
|
46
48
|
|
47
49
|
def export(self, format):
|
48
50
|
"""Determine the export format and calls the corresponding method."""
|
@@ -60,7 +62,9 @@ class TreeNodeExporter:
|
|
60
62
|
def process_complex_fields(self, record):
|
61
63
|
"""Convert complex fields (lists, dictionaries) into JSON strings."""
|
62
64
|
for key, value in record.items():
|
63
|
-
if isinstance(value,
|
65
|
+
if isinstance(value, uuid.UUID):
|
66
|
+
record[key] = str(value)
|
67
|
+
elif isinstance(value, (list, dict)):
|
64
68
|
try:
|
65
69
|
record[key] = json.dumps(value, ensure_ascii=False)
|
66
70
|
except Exception as e:
|
@@ -68,11 +72,24 @@ class TreeNodeExporter:
|
|
68
72
|
record[key] = None
|
69
73
|
return record
|
70
74
|
|
75
|
+
def get_ordered_fields(self):
|
76
|
+
"""Return fields in the desired order.
|
77
|
+
|
78
|
+
Order: id, tn_parent, tn_priority, then the rest.
|
79
|
+
"""
|
80
|
+
required_fields = ["id", "tn_parent", "tn_priority"]
|
81
|
+
other_fields = [
|
82
|
+
field for field in self.fields if field not in required_fields]
|
83
|
+
return required_fields + other_fields
|
84
|
+
|
71
85
|
def get_sorted_queryset(self):
|
72
|
-
"""
|
73
|
-
|
74
|
-
tn_orders = np.array([obj.tn_order for obj in
|
75
|
-
|
86
|
+
"""Quick sort queryset by tn_order."""
|
87
|
+
queryset = self.queryset
|
88
|
+
tn_orders = np.array([obj.tn_order for obj in queryset])
|
89
|
+
sorted_indices = np.argsort(tn_orders)
|
90
|
+
queryset_list = list(queryset.iterator())
|
91
|
+
result = [queryset_list[int(idx)] for idx in sorted_indices]
|
92
|
+
return result
|
76
93
|
|
77
94
|
def get_data(self):
|
78
95
|
"""Return a list of data from QuerySet as dictionaries."""
|
@@ -100,70 +117,80 @@ class TreeNodeExporter:
|
|
100
117
|
return data
|
101
118
|
|
102
119
|
def to_csv(self):
|
103
|
-
"""Export to CSV with proper
|
104
|
-
response = HttpResponse(content_type="text/csv")
|
120
|
+
"""Export to CSV with proper UTF-8 encoding."""
|
121
|
+
response = HttpResponse(content_type="text/csv; charset=utf-8")
|
105
122
|
response["Content-Disposition"] = f'attachment; filename="{self.filename}.csv"'
|
123
|
+
response.write("\ufeff") # Добавляем BOM для Excel
|
124
|
+
|
106
125
|
writer = csv.DictWriter(response, fieldnames=self.fields)
|
107
126
|
writer.writeheader()
|
108
|
-
|
127
|
+
for row in self.get_data():
|
128
|
+
writer.writerow({key: str(value)
|
129
|
+
for key, value in row.items()}) # Приводим к строкам
|
130
|
+
|
109
131
|
return response
|
110
132
|
|
111
133
|
def to_json(self):
|
112
|
-
"""Export to JSON with
|
113
|
-
response = HttpResponse(content_type="application/
|
134
|
+
"""Export to JSON with proper UTF-8 encoding."""
|
135
|
+
response = HttpResponse(content_type="application/json; charset=utf-8")
|
114
136
|
response["Content-Disposition"] = f'attachment; filename="{self.filename}.json"'
|
115
|
-
json.
|
116
|
-
|
117
|
-
response,
|
118
|
-
ensure_ascii=False,
|
119
|
-
indent=4,
|
120
|
-
default=str
|
121
|
-
)
|
137
|
+
json_str = json.dumps(self.get_data(), ensure_ascii=False, indent=4)
|
138
|
+
response.write(json_str)
|
122
139
|
return response
|
123
140
|
|
124
141
|
def to_xlsx(self):
|
125
|
-
"""Export to XLSX."""
|
142
|
+
"""Export to XLSX with UTF-8 encoding."""
|
126
143
|
response = HttpResponse(
|
127
144
|
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
128
145
|
)
|
129
146
|
response["Content-Disposition"] = f'attachment; filename="{self.filename}.xlsx"'
|
130
147
|
|
131
|
-
data = self.get_data()
|
132
148
|
output = BytesIO()
|
133
149
|
workbook = xlsxwriter.Workbook(output)
|
134
150
|
worksheet = workbook.add_worksheet()
|
135
151
|
|
136
|
-
#
|
137
|
-
headers = list(
|
152
|
+
# Заголовки
|
153
|
+
headers = list(self.fields)
|
138
154
|
for col_num, header in enumerate(headers):
|
139
155
|
worksheet.write(0, col_num, header)
|
140
156
|
|
141
|
-
#
|
142
|
-
for row_num, row in enumerate(
|
157
|
+
# Данные
|
158
|
+
for row_num, row in enumerate(self.get_data(), start=1):
|
143
159
|
for col_num, key in enumerate(headers):
|
144
|
-
worksheet.write(
|
160
|
+
worksheet.write(
|
161
|
+
row_num,
|
162
|
+
col_num,
|
163
|
+
str(row[key]) if row[key] is not None else ""
|
164
|
+
)
|
145
165
|
|
146
166
|
workbook.close()
|
147
167
|
output.seek(0)
|
148
|
-
|
168
|
+
response.write(output.read())
|
169
|
+
return response
|
149
170
|
|
150
171
|
def to_yaml(self):
|
151
|
-
"""Export to YAML with proper
|
152
|
-
response = HttpResponse(
|
172
|
+
"""Export to YAML with proper UTF-8 encoding."""
|
173
|
+
response = HttpResponse(
|
174
|
+
content_type="application/x-yaml; charset=utf-8")
|
153
175
|
response["Content-Disposition"] = f'attachment; filename="{self.filename}.yaml"'
|
154
|
-
yaml_str = yaml.dump(
|
176
|
+
yaml_str = yaml.dump(
|
177
|
+
self.get_data(), allow_unicode=True, default_flow_style=False)
|
155
178
|
response.write(yaml_str)
|
156
179
|
return response
|
157
180
|
|
158
181
|
def to_tsv(self):
|
159
|
-
"""Export to TSV with
|
160
|
-
response = HttpResponse(
|
182
|
+
"""Export to TSV with UTF-8 encoding."""
|
183
|
+
response = HttpResponse(
|
184
|
+
content_type="text/tab-separated-values; charset=utf-8")
|
161
185
|
response["Content-Disposition"] = f'attachment; filename="{self.filename}.tsv"'
|
186
|
+
response.write("\ufeff") # Добавляем BOM
|
187
|
+
|
162
188
|
writer = csv.DictWriter(
|
163
|
-
response,
|
164
|
-
fieldnames=self.fields,
|
165
|
-
delimiter=" "
|
166
|
-
)
|
189
|
+
response, fieldnames=self.fields, delimiter="\t")
|
167
190
|
writer.writeheader()
|
168
|
-
|
191
|
+
for row in self.get_data():
|
192
|
+
writer.writerow({key: str(value) for key, value in row.items()})
|
193
|
+
|
169
194
|
return response
|
195
|
+
|
196
|
+
# The End
|