django-fast-treenode 2.1.4__py3-none-any.whl → 3.0.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-3.0.0.dist-info/METADATA +203 -0
- django_fast_treenode-3.0.0.dist-info/RECORD +90 -0
- {django_fast_treenode-2.1.4.dist-info → django_fast_treenode-3.0.0.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 +42 -20
- 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.0.dist-info/licenses}/LICENSE +0 -0
- {django_fast_treenode-2.1.4.dist-info → django_fast_treenode-3.0.0.dist-info}/top_level.txt +0 -0
- /treenode/static/{treenode → css}/.gitkeep +0 -0
- /treenode/static/{treenode/css → js}/.gitkeep +0 -0
treenode/utils/exporter.py
DELETED
@@ -1,196 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
"""
|
3
|
-
TreeNode Exporter Module
|
4
|
-
|
5
|
-
This module provides functionality for exporting tree-structured data
|
6
|
-
to various formats, including CSV, JSON, XLSX, YAML, and TSV.
|
7
|
-
|
8
|
-
Features:
|
9
|
-
- Supports exporting ForeignKey fields as IDs and ManyToMany fields as JSON
|
10
|
-
lists.
|
11
|
-
- Handles complex field types (lists, dictionaries) with proper serialization.
|
12
|
-
- Provides optimized data extraction for QuerySets.
|
13
|
-
- Generates downloadable files with appropriate HTTP responses.
|
14
|
-
|
15
|
-
Version: 2.0.11
|
16
|
-
Author: Timur Kady
|
17
|
-
Email: timurkady@yandex.com
|
18
|
-
"""
|
19
|
-
|
20
|
-
|
21
|
-
import csv
|
22
|
-
import json
|
23
|
-
import yaml
|
24
|
-
import xlsxwriter
|
25
|
-
import numpy as np
|
26
|
-
import uuid
|
27
|
-
from io import BytesIO
|
28
|
-
from django.http import HttpResponse
|
29
|
-
import logging
|
30
|
-
|
31
|
-
logger = logging.getLogger(__name__)
|
32
|
-
|
33
|
-
|
34
|
-
class TreeNodeExporter:
|
35
|
-
"""Exporter for tree-structured data to various formats."""
|
36
|
-
|
37
|
-
def __init__(self, queryset, filename="tree_nodes"):
|
38
|
-
"""
|
39
|
-
Init.
|
40
|
-
|
41
|
-
:param queryset: QuerySet of objects to export.
|
42
|
-
:param filename: Filename without extension.
|
43
|
-
"""
|
44
|
-
self.queryset = queryset
|
45
|
-
self.filename = filename
|
46
|
-
self.fields = [field.name for field in queryset.model._meta.fields]
|
47
|
-
self.fields = self.get_ordered_fields()
|
48
|
-
|
49
|
-
def export(self, format):
|
50
|
-
"""Determine the export format and calls the corresponding method."""
|
51
|
-
exporters = {
|
52
|
-
"csv": self.to_csv,
|
53
|
-
"json": self.to_json,
|
54
|
-
"xlsx": self.to_xlsx,
|
55
|
-
"yaml": self.to_yaml,
|
56
|
-
"tsv": self.to_tsv,
|
57
|
-
}
|
58
|
-
if format not in exporters:
|
59
|
-
raise ValueError("Unsupported export format")
|
60
|
-
return exporters[format]()
|
61
|
-
|
62
|
-
def process_complex_fields(self, record):
|
63
|
-
"""Convert complex fields (lists, dictionaries) into JSON strings."""
|
64
|
-
for key, value in record.items():
|
65
|
-
if isinstance(value, uuid.UUID):
|
66
|
-
record[key] = str(value)
|
67
|
-
elif isinstance(value, (list, dict)):
|
68
|
-
try:
|
69
|
-
record[key] = json.dumps(value, ensure_ascii=False)
|
70
|
-
except Exception as e:
|
71
|
-
logger.warning("Error serializing field %s: %s", key, e)
|
72
|
-
record[key] = None
|
73
|
-
return record
|
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
|
-
|
85
|
-
def get_sorted_queryset(self):
|
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
|
93
|
-
|
94
|
-
def get_data(self):
|
95
|
-
"""Return a list of data from QuerySet as dictionaries."""
|
96
|
-
data = []
|
97
|
-
for obj in self.get_sorted_queryset():
|
98
|
-
record = {}
|
99
|
-
for field in self.fields:
|
100
|
-
value = getattr(obj, field, None)
|
101
|
-
field_object = obj._meta.get_field(field)
|
102
|
-
if field_object.is_relation:
|
103
|
-
if field_object.many_to_many:
|
104
|
-
# ManyToMany - save as a JSON string
|
105
|
-
record[field] = json.dumps(
|
106
|
-
list(value.values_list('id', flat=True)),
|
107
|
-
ensure_ascii=False)
|
108
|
-
elif field_object.many_to_one:
|
109
|
-
# ForeignKey - save as ID
|
110
|
-
record[field] = getattr(value, "id", None)
|
111
|
-
else:
|
112
|
-
record[field] = value
|
113
|
-
else:
|
114
|
-
record[field] = value
|
115
|
-
record = self.process_complex_fields(record)
|
116
|
-
data.append(record)
|
117
|
-
return data
|
118
|
-
|
119
|
-
def to_csv(self):
|
120
|
-
"""Export to CSV with proper UTF-8 encoding."""
|
121
|
-
response = HttpResponse(content_type="text/csv; charset=utf-8")
|
122
|
-
response["Content-Disposition"] = f'attachment; filename="{self.filename}.csv"'
|
123
|
-
response.write("\ufeff") # Добавляем BOM для Excel
|
124
|
-
|
125
|
-
writer = csv.DictWriter(response, fieldnames=self.fields)
|
126
|
-
writer.writeheader()
|
127
|
-
for row in self.get_data():
|
128
|
-
writer.writerow({key: str(value)
|
129
|
-
for key, value in row.items()}) # Приводим к строкам
|
130
|
-
|
131
|
-
return response
|
132
|
-
|
133
|
-
def to_json(self):
|
134
|
-
"""Export to JSON with proper UTF-8 encoding."""
|
135
|
-
response = HttpResponse(content_type="application/json; charset=utf-8")
|
136
|
-
response["Content-Disposition"] = f'attachment; filename="{self.filename}.json"'
|
137
|
-
json_str = json.dumps(self.get_data(), ensure_ascii=False, indent=4)
|
138
|
-
response.write(json_str)
|
139
|
-
return response
|
140
|
-
|
141
|
-
def to_xlsx(self):
|
142
|
-
"""Export to XLSX with UTF-8 encoding."""
|
143
|
-
response = HttpResponse(
|
144
|
-
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
145
|
-
)
|
146
|
-
response["Content-Disposition"] = f'attachment; filename="{self.filename}.xlsx"'
|
147
|
-
|
148
|
-
output = BytesIO()
|
149
|
-
workbook = xlsxwriter.Workbook(output)
|
150
|
-
worksheet = workbook.add_worksheet()
|
151
|
-
|
152
|
-
# Заголовки
|
153
|
-
headers = list(self.fields)
|
154
|
-
for col_num, header in enumerate(headers):
|
155
|
-
worksheet.write(0, col_num, header)
|
156
|
-
|
157
|
-
# Данные
|
158
|
-
for row_num, row in enumerate(self.get_data(), start=1):
|
159
|
-
for col_num, key in enumerate(headers):
|
160
|
-
worksheet.write(
|
161
|
-
row_num,
|
162
|
-
col_num,
|
163
|
-
str(row[key]) if row[key] is not None else ""
|
164
|
-
)
|
165
|
-
|
166
|
-
workbook.close()
|
167
|
-
output.seek(0)
|
168
|
-
response.write(output.read())
|
169
|
-
return response
|
170
|
-
|
171
|
-
def to_yaml(self):
|
172
|
-
"""Export to YAML with proper UTF-8 encoding."""
|
173
|
-
response = HttpResponse(
|
174
|
-
content_type="application/x-yaml; charset=utf-8")
|
175
|
-
response["Content-Disposition"] = f'attachment; filename="{self.filename}.yaml"'
|
176
|
-
yaml_str = yaml.dump(
|
177
|
-
self.get_data(), allow_unicode=True, default_flow_style=False)
|
178
|
-
response.write(yaml_str)
|
179
|
-
return response
|
180
|
-
|
181
|
-
def to_tsv(self):
|
182
|
-
"""Export to TSV with UTF-8 encoding."""
|
183
|
-
response = HttpResponse(
|
184
|
-
content_type="text/tab-separated-values; charset=utf-8")
|
185
|
-
response["Content-Disposition"] = f'attachment; filename="{self.filename}.tsv"'
|
186
|
-
response.write("\ufeff") # Добавляем BOM
|
187
|
-
|
188
|
-
writer = csv.DictWriter(
|
189
|
-
response, fieldnames=self.fields, delimiter="\t")
|
190
|
-
writer.writeheader()
|
191
|
-
for row in self.get_data():
|
192
|
-
writer.writerow({key: str(value) for key, value in row.items()})
|
193
|
-
|
194
|
-
return response
|
195
|
-
|
196
|
-
# The End
|
treenode/utils/importer.py
DELETED
@@ -1,328 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
"""
|
3
|
-
TreeNode Importer Module
|
4
|
-
|
5
|
-
This module provides functionality for importing tree-structured data
|
6
|
-
from various formats, including CSV, JSON, XLSX, YAML, and TSV.
|
7
|
-
|
8
|
-
Features:
|
9
|
-
- Supports field mapping and data type conversion for model compatibility.
|
10
|
-
- Handles ForeignKey relationships and ManyToMany fields.
|
11
|
-
- Validates and processes raw data before saving to the database.
|
12
|
-
- Uses bulk operations for efficient data insertion and updates.
|
13
|
-
- Supports transactional imports to maintain data integrity.
|
14
|
-
|
15
|
-
Version: 2.0.11
|
16
|
-
Author: Timur Kady
|
17
|
-
Email: timurkady@yandex.com
|
18
|
-
"""
|
19
|
-
|
20
|
-
|
21
|
-
import csv
|
22
|
-
import json
|
23
|
-
import yaml
|
24
|
-
import openpyxl
|
25
|
-
import math
|
26
|
-
import uuid
|
27
|
-
from io import BytesIO, StringIO
|
28
|
-
|
29
|
-
import logging
|
30
|
-
|
31
|
-
logger = logging.getLogger(__name__)
|
32
|
-
|
33
|
-
|
34
|
-
class TreeNodeImporter:
|
35
|
-
"""Импортер древовидных данных из различных форматов."""
|
36
|
-
|
37
|
-
def __init__(self, model, file, format, fields=None, mapping=None):
|
38
|
-
"""
|
39
|
-
Init method.
|
40
|
-
|
41
|
-
:param model: Django model where the data will be imported.
|
42
|
-
:param file: File object.
|
43
|
-
:param format: File format ('csv', 'json', 'xlsx', 'yaml', 'tsv').
|
44
|
-
:param fields: List of model fields to import.
|
45
|
-
:param mapping: Dictionary for mapping keys from file to model
|
46
|
-
field names.
|
47
|
-
For example: {"Name": "title", "Description": "desc"}
|
48
|
-
"""
|
49
|
-
self.model = model
|
50
|
-
self.format = format
|
51
|
-
# Если поля не заданы, используем все поля модели
|
52
|
-
self.fields = fields or [field.name for field in model._meta.fields]
|
53
|
-
# По умолчанию маппинг идентичен: ключи совпадают с именами полей
|
54
|
-
self.mapping = mapping or {field: field for field in self.fields}
|
55
|
-
# Считываем содержимое файла один раз, чтобы избежать проблем с курсором
|
56
|
-
self.file_content = file.read()
|
57
|
-
|
58
|
-
def get_text_content(self):
|
59
|
-
"""Return the contents of a file as a string."""
|
60
|
-
if isinstance(self.file_content, bytes):
|
61
|
-
return self.file_content.decode("utf-8")
|
62
|
-
return self.file_content
|
63
|
-
|
64
|
-
def import_data(self):
|
65
|
-
"""Import data and returns a list of dictionaries."""
|
66
|
-
importers = {
|
67
|
-
"csv": self.from_csv,
|
68
|
-
"json": self.from_json,
|
69
|
-
"xlsx": self.from_xlsx,
|
70
|
-
"yaml": self.from_yaml,
|
71
|
-
"tsv": self.from_tsv,
|
72
|
-
}
|
73
|
-
if self.format not in importers:
|
74
|
-
raise ValueError("Unsupported import format")
|
75
|
-
|
76
|
-
raw_data = importers[self.format]()
|
77
|
-
|
78
|
-
# Processing: field filtering, complex value packing and type casting
|
79
|
-
processed = []
|
80
|
-
for record in raw_data:
|
81
|
-
filtered = self.filter_fields(record)
|
82
|
-
filtered = self.process_complex_fields(filtered)
|
83
|
-
filtered = self.cast_record_types(filtered)
|
84
|
-
processed.append(filtered)
|
85
|
-
|
86
|
-
return processed
|
87
|
-
|
88
|
-
def get_tn_orders(self, rows):
|
89
|
-
"""Calculate the materialized path without including None parents."""
|
90
|
-
# Build a mapping from id to record for quick lookup.
|
91
|
-
row_dict = {row["id"]: row for row in rows}
|
92
|
-
|
93
|
-
def get_ancestor_path(row):
|
94
|
-
parent_field = 'tn_parent' if 'tn_parent' in row else 'tn_parent_id'
|
95
|
-
return get_ancestor_path(row_dict[row[parent_field]]) + [row["id"]] if row[parent_field] else [row["id"]]
|
96
|
-
|
97
|
-
return [
|
98
|
-
{"id": row["id"], "path": get_ancestor_path(row)}
|
99
|
-
for row in rows
|
100
|
-
]
|
101
|
-
|
102
|
-
def filter_fields(self, record):
|
103
|
-
"""
|
104
|
-
Filter the record according to the mapping.
|
105
|
-
|
106
|
-
Only the necessary keys remain, while the names are renamed.
|
107
|
-
"""
|
108
|
-
new_record = {}
|
109
|
-
for file_key, model_field in self.mapping.items():
|
110
|
-
new_record[model_field] = record.get(file_key)
|
111
|
-
return new_record
|
112
|
-
|
113
|
-
def process_complex_fields(self, record):
|
114
|
-
"""
|
115
|
-
Pack it into a JSON string.
|
116
|
-
|
117
|
-
If the field value is a dictionary or list.
|
118
|
-
"""
|
119
|
-
for key, value in record.items():
|
120
|
-
if isinstance(value, uuid.UUID):
|
121
|
-
record[key] = str(value)
|
122
|
-
if isinstance(value, (list, dict)):
|
123
|
-
try:
|
124
|
-
record[key] = json.dumps(value, ensure_ascii=False)
|
125
|
-
except Exception as e:
|
126
|
-
logger.warning("Error serializing field %s: %s", key, e)
|
127
|
-
record[key] = None
|
128
|
-
return record
|
129
|
-
|
130
|
-
def cast_record_types(self, record):
|
131
|
-
"""
|
132
|
-
Cast the values of the record fields to the types defined in the model.
|
133
|
-
|
134
|
-
For each field, its to_python() method is called. If the value is nan,
|
135
|
-
it is replaced with None.
|
136
|
-
For ForeignKey fields (many-to-one), the value is written to
|
137
|
-
the <field>_id attribute, and the original key is removed.
|
138
|
-
"""
|
139
|
-
for field in self.model._meta.fields:
|
140
|
-
field_name = field.name
|
141
|
-
if field.is_relation and field.many_to_one:
|
142
|
-
if field_name in record:
|
143
|
-
value = record[field_name]
|
144
|
-
if isinstance(value, float) and math.isnan(value):
|
145
|
-
value = None
|
146
|
-
try:
|
147
|
-
converted = None if value is None else int(value)
|
148
|
-
# Записываем в атрибут, например, tn_parent_id
|
149
|
-
record[field.attname] = converted
|
150
|
-
except Exception as e:
|
151
|
-
logger.warning(
|
152
|
-
"Error converting FK field %s with value %r: %s",
|
153
|
-
field_name,
|
154
|
-
value,
|
155
|
-
e
|
156
|
-
)
|
157
|
-
record[field.attname] = None
|
158
|
-
# Удаляем оригинальное значение, чтобы Django не пыталась
|
159
|
-
# его обработать
|
160
|
-
del record[field_name]
|
161
|
-
else:
|
162
|
-
if field_name in record:
|
163
|
-
value = record[field_name]
|
164
|
-
if isinstance(value, float) and math.isnan(value):
|
165
|
-
record[field_name] = None
|
166
|
-
else:
|
167
|
-
try:
|
168
|
-
record[field_name] = field.to_python(value)
|
169
|
-
except Exception as e:
|
170
|
-
logger.warning(
|
171
|
-
"Error converting field %s with value %r: %s",
|
172
|
-
field_name,
|
173
|
-
value,
|
174
|
-
e
|
175
|
-
)
|
176
|
-
record[field_name] = None
|
177
|
-
return record
|
178
|
-
|
179
|
-
# ------------------------------------------------------------------------
|
180
|
-
|
181
|
-
def finalize(self, raw_data):
|
182
|
-
"""
|
183
|
-
Finalize import.
|
184
|
-
|
185
|
-
Processes raw_data, creating and updating objects by levels
|
186
|
-
(from roots to leaves) using the materialized path to calculate
|
187
|
-
the level.
|
188
|
-
|
189
|
-
Algorithm:
|
190
|
-
1. Build a raw_by_id dictionary for quick access to records by id.
|
191
|
-
2. For each record, calculate the materialized path:
|
192
|
-
- If tn_parent is specified and exists in raw_data, recursively get
|
193
|
-
the parent's path and add its id.
|
194
|
-
- If tn_parent is missing from raw_data, check if the parent is in
|
195
|
-
the database.
|
196
|
-
If not, generate an error.
|
197
|
-
3. Record level = length of its materialized path.
|
198
|
-
4. Split records into those that need to be created (if the object
|
199
|
-
with the given id is not yet in the database), and those that need
|
200
|
-
to be updated.
|
201
|
-
5. To create, process groups by levels (sort by increasing level):
|
202
|
-
- Validate each record, if there are no errors, add the instance to
|
203
|
-
the list.
|
204
|
-
- After each level, we perform bulk_create.
|
205
|
-
6. For updates, we collect instances, fixing fields (without id)
|
206
|
-
and perform bulk_update.
|
207
|
-
|
208
|
-
Returns a dictionary:
|
209
|
-
{
|
210
|
-
"create": [созданные объекты],
|
211
|
-
"update": [обновлённые объекты],
|
212
|
-
"errors": [список ошибок]
|
213
|
-
}
|
214
|
-
"""
|
215
|
-
result = {
|
216
|
-
"create": [],
|
217
|
-
"update": [],
|
218
|
-
"errors": []
|
219
|
-
}
|
220
|
-
|
221
|
-
# 1. Calculate the materialized path and level for each entry.
|
222
|
-
paths = self.get_tn_orders(raw_data)
|
223
|
-
# key: record id, value: уровень (int)
|
224
|
-
levels_by_record = {rec["id"]: len(rec["path"])-1 for rec in paths}
|
225
|
-
|
226
|
-
# 2. Разбиваем записи по уровням
|
227
|
-
levels = {}
|
228
|
-
for record in raw_data:
|
229
|
-
level = levels_by_record.get(record["id"], 0)
|
230
|
-
if level not in levels:
|
231
|
-
levels[level] = []
|
232
|
-
levels[level].append(record)
|
233
|
-
|
234
|
-
records_by_level = [
|
235
|
-
sorted(
|
236
|
-
levels[key],
|
237
|
-
key=lambda x: (x.get(
|
238
|
-
"tn_parent",
|
239
|
-
x.get("tn_parent_id", 0)) or -1)
|
240
|
-
)
|
241
|
-
for key in sorted(levels.keys())
|
242
|
-
]
|
243
|
-
|
244
|
-
# 4. We split the records into those to create and those to update.
|
245
|
-
# The list of records to update
|
246
|
-
to_update = []
|
247
|
-
|
248
|
-
for level in range(len(records_by_level)):
|
249
|
-
instances_to_create = []
|
250
|
-
for record in records_by_level[level]:
|
251
|
-
rec_id = record["id"]
|
252
|
-
if self.model.objects.filter(pk=rec_id).exists():
|
253
|
-
to_update.append(record)
|
254
|
-
else:
|
255
|
-
instance = self.model(**record)
|
256
|
-
try:
|
257
|
-
instance.full_clean()
|
258
|
-
instances_to_create.append(instance)
|
259
|
-
except Exception as e:
|
260
|
-
result["errors"].append(f"Validation error for record \
|
261
|
-
{record['id']} on level {level}: {e}")
|
262
|
-
try:
|
263
|
-
created = self.model.objects.bulk_create(instances_to_create)
|
264
|
-
result["create"].extend(created)
|
265
|
-
except Exception as e:
|
266
|
-
result["errors"].append(f"Create error on level {level}: {e}")
|
267
|
-
|
268
|
-
# 6. Processing updates: collecting instances and a list of fields
|
269
|
-
# for bulk_update
|
270
|
-
updated_instances = []
|
271
|
-
update_fields_set = set()
|
272
|
-
for record in to_update:
|
273
|
-
rec_id = record["id"]
|
274
|
-
try:
|
275
|
-
instance = self.model.objects.get(pk=rec_id)
|
276
|
-
for field, value in record.items():
|
277
|
-
if field != "id":
|
278
|
-
setattr(instance, field, value)
|
279
|
-
update_fields_set.add(field)
|
280
|
-
instance.full_clean()
|
281
|
-
updated_instances.append(instance)
|
282
|
-
except Exception as e:
|
283
|
-
result["errors"].append(
|
284
|
-
f"Validation error updating record {rec_id}: {e}")
|
285
|
-
update_fields = list(update_fields_set)
|
286
|
-
if updated_instances:
|
287
|
-
try:
|
288
|
-
self.model.objects.bulk_update(updated_instances, update_fields)
|
289
|
-
result["update"].extend(updated_instances)
|
290
|
-
except Exception as e:
|
291
|
-
result["errors"].append(f"Bulk update error: {e}")
|
292
|
-
|
293
|
-
return result
|
294
|
-
|
295
|
-
# ------------------------------------------------------------------------
|
296
|
-
|
297
|
-
def from_csv(self):
|
298
|
-
"""Import from CSV."""
|
299
|
-
text = self.get_text_content()
|
300
|
-
return list(csv.DictReader(StringIO(text)))
|
301
|
-
|
302
|
-
def from_json(self):
|
303
|
-
"""Import from JSON."""
|
304
|
-
return json.loads(self.get_text_content())
|
305
|
-
|
306
|
-
def from_xlsx(self):
|
307
|
-
"""Import from XLSX (Excel)."""
|
308
|
-
file_stream = BytesIO(self.file_content)
|
309
|
-
rows = []
|
310
|
-
wb = openpyxl.load_workbook(file_stream, read_only=True)
|
311
|
-
ws = wb.active
|
312
|
-
headers = [
|
313
|
-
cell.value for cell in next(ws.iter_rows(min_row=1, max_row=1))
|
314
|
-
]
|
315
|
-
for row in ws.iter_rows(min_row=2, values_only=True):
|
316
|
-
rows.append(dict(zip(headers, row)))
|
317
|
-
return rows
|
318
|
-
|
319
|
-
def from_yaml(self):
|
320
|
-
"""Import from YAML."""
|
321
|
-
return yaml.safe_load(self.get_text_content())
|
322
|
-
|
323
|
-
def from_tsv(self):
|
324
|
-
"""Import from TSV."""
|
325
|
-
text = self.get_text_content()
|
326
|
-
return list(csv.DictReader(StringIO(text), delimiter="\t"))
|
327
|
-
|
328
|
-
# The End
|
treenode/utils/radix.py
DELETED
@@ -1,61 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
"""
|
3
|
-
Implementation of the Radix Sort algorithm.
|
4
|
-
|
5
|
-
Radix Sort is a non-comparative sorting algorithm. It avoids comparisons by
|
6
|
-
creating and distributing elements into buckets according to their radix.
|
7
|
-
|
8
|
-
It is used as a replacement for numpy when sorting materialized paths and
|
9
|
-
tree node indices.
|
10
|
-
|
11
|
-
Version: 2.1.0
|
12
|
-
Author: Timur Kady
|
13
|
-
Email: timurkady@yandex.com
|
14
|
-
"""
|
15
|
-
|
16
|
-
from collections import defaultdict
|
17
|
-
|
18
|
-
|
19
|
-
def counting_sort(pairs, index):
|
20
|
-
"""Sort pairs (key, string) by character at position index."""
|
21
|
-
count = defaultdict(list)
|
22
|
-
|
23
|
-
# Distribution of pairs into baskets
|
24
|
-
for key, s in pairs:
|
25
|
-
key_char = s[index] if index < len(s) else ''
|
26
|
-
count[key_char].append((key, s))
|
27
|
-
|
28
|
-
# Collect sorted pairs
|
29
|
-
sorted_pairs = []
|
30
|
-
for key_char in sorted(count.keys()):
|
31
|
-
sorted_pairs.extend(count[key_char])
|
32
|
-
|
33
|
-
return sorted_pairs
|
34
|
-
|
35
|
-
|
36
|
-
def radix_sort_pairs(pairs, max_length):
|
37
|
-
"""Radical sorting of pairs (key, string) by string."""
|
38
|
-
for i in range(max_length - 1, -1, -1):
|
39
|
-
pairs = counting_sort(pairs, i)
|
40
|
-
return pairs
|
41
|
-
|
42
|
-
|
43
|
-
def quick_sort(pairs):
|
44
|
-
"""
|
45
|
-
Sort tree objects by materialized path.
|
46
|
-
|
47
|
-
pairs = [{obj.id: obj.path} for obj in objs]
|
48
|
-
Returns a list of id (pk) objects sorted by their materialized path.
|
49
|
-
"""
|
50
|
-
# Get the maximum length of the string
|
51
|
-
max_length = max(len(s) for _, s in pairs)
|
52
|
-
|
53
|
-
# Sort pairs by rows
|
54
|
-
sorted_pairs = radix_sort_pairs(pairs, max_length)
|
55
|
-
|
56
|
-
# Access keys in sorted order
|
57
|
-
sorted_keys = [key for key, _ in sorted_pairs]
|
58
|
-
return sorted_keys
|
59
|
-
|
60
|
-
|
61
|
-
# The End
|