django-fast-treenode 1.1.3__py3-none-any.whl → 2.0.1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.1.dist-info}/METADATA +127 -46
  2. django_fast_treenode-2.0.1.dist-info/RECORD +41 -0
  3. {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.1.dist-info}/WHEEL +1 -1
  4. treenode/__init__.py +0 -7
  5. treenode/admin.py +327 -82
  6. treenode/apps.py +20 -3
  7. treenode/cache.py +231 -0
  8. treenode/docs/Documentation +101 -54
  9. treenode/forms.py +75 -19
  10. treenode/managers.py +260 -48
  11. treenode/models/__init__.py +7 -0
  12. treenode/models/classproperty.py +24 -0
  13. treenode/models/closure.py +168 -0
  14. treenode/models/factory.py +71 -0
  15. treenode/models/proxy.py +650 -0
  16. treenode/static/treenode/css/tree_widget.css +62 -0
  17. treenode/static/treenode/css/treenode_admin.css +106 -0
  18. treenode/static/treenode/js/tree_widget.js +161 -0
  19. treenode/static/treenode/js/treenode_admin.js +171 -0
  20. treenode/templates/admin/export_success.html +26 -0
  21. treenode/templates/admin/tree_node_changelist.html +11 -0
  22. treenode/templates/admin/tree_node_export.html +27 -0
  23. treenode/templates/admin/tree_node_import.html +27 -0
  24. treenode/templates/widgets/tree_widget.css +23 -0
  25. treenode/templates/widgets/tree_widget.html +21 -0
  26. treenode/urls.py +34 -0
  27. treenode/utils/__init__.py +4 -0
  28. treenode/utils/base36.py +35 -0
  29. treenode/utils/exporter.py +141 -0
  30. treenode/utils/importer.py +296 -0
  31. treenode/version.py +11 -1
  32. treenode/views.py +102 -2
  33. treenode/widgets.py +49 -27
  34. django_fast_treenode-1.1.3.dist-info/RECORD +0 -33
  35. treenode/compat.py +0 -8
  36. treenode/factory.py +0 -68
  37. treenode/models.py +0 -668
  38. treenode/static/select2tree/.gitkeep +0 -1
  39. treenode/static/select2tree/select2tree.css +0 -176
  40. treenode/static/select2tree/select2tree.js +0 -181
  41. treenode/static/treenode/css/treenode.css +0 -85
  42. treenode/static/treenode/js/treenode.js +0 -201
  43. treenode/templates/widgets/.gitkeep +0 -1
  44. treenode/templates/widgets/attrs.html +0 -7
  45. treenode/templates/widgets/options.html +0 -1
  46. treenode/templates/widgets/select2tree.html +0 -22
  47. treenode/tests.py +0 -3
  48. {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.1.dist-info}/LICENSE +0 -0
  49. {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.1.dist-info}/top_level.txt +0 -0
  50. /treenode/{docs → templates/admin}/.gitkeep +0 -0
@@ -0,0 +1,141 @@
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.0
16
+ Author: Timur Kady
17
+ Email: timurkady@yandex.com
18
+ """
19
+
20
+
21
+ import csv
22
+ import json
23
+ import yaml
24
+ import pandas as pd
25
+ from io import BytesIO
26
+ from django.http import HttpResponse
27
+ import logging
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ class TreeNodeExporter:
33
+ """Exporter for tree-structured data to various formats."""
34
+
35
+ def __init__(self, queryset, filename="tree_nodes"):
36
+ """
37
+ Init.
38
+
39
+ :param queryset: QuerySet of objects to export.
40
+ :param filename: Filename without extension.
41
+ """
42
+ self.queryset = queryset
43
+ self.filename = filename
44
+ self.fields = [field.name for field in queryset.model._meta.fields]
45
+
46
+ def export(self, format):
47
+ """Determine the export format and calls the corresponding method."""
48
+ exporters = {
49
+ "csv": self.to_csv,
50
+ "json": self.to_json,
51
+ "xlsx": self.to_xlsx,
52
+ "yaml": self.to_yaml,
53
+ "tsv": self.to_tsv,
54
+ }
55
+ if format not in exporters:
56
+ raise ValueError("Unsupported export format")
57
+ return exporters[format]()
58
+
59
+ def process_complex_fields(self, record):
60
+ """Convert complex fields (lists, dictionaries) into JSON strings."""
61
+ for key, value in record.items():
62
+ if isinstance(value, (list, dict)):
63
+ try:
64
+ record[key] = json.dumps(value, ensure_ascii=False)
65
+ except Exception as e:
66
+ logger.warning("Error serializing field %s: %s", key, e)
67
+ record[key] = None
68
+ return record
69
+
70
+ def get_data(self):
71
+ """Return a list of data from QuerySet as dictionaries."""
72
+ data = []
73
+ for obj in self.queryset:
74
+ record = {}
75
+ for field in self.fields:
76
+ value = getattr(obj, field, None)
77
+ field_object = obj._meta.get_field(field)
78
+ if field_object.is_relation:
79
+ if field_object.many_to_many:
80
+ # ManyToMany - save as a JSON string
81
+ record[field] = json.dumps(
82
+ list(value.values_list('id', flat=True)),
83
+ ensure_ascii=False)
84
+ elif field_object.many_to_one:
85
+ # ForeignKey - save as ID
86
+ record[field] = value.id if value else None
87
+ else:
88
+ record[field] = value
89
+ else:
90
+ record[field] = value
91
+ record = self.process_complex_fields(record)
92
+ data.append(record)
93
+ return data
94
+
95
+ def to_csv(self):
96
+ """Export to CSV with proper attachment handling."""
97
+ response = HttpResponse(content_type="text/csv")
98
+ response["Content-Disposition"] = f'attachment; filename="{self.filename}.csv"'
99
+ writer = csv.DictWriter(response, fieldnames=self.fields)
100
+ writer.writeheader()
101
+ writer.writerows(self.get_data())
102
+ return response
103
+
104
+ def to_json(self):
105
+ """Export to JSON with UUID serialization handling."""
106
+ response = HttpResponse(content_type="application/octet-stream")
107
+ response["Content-Disposition"] = f'attachment; filename="{self.filename}.json"'
108
+ json.dump(self.get_data(), response,
109
+ ensure_ascii=False, indent=4, default=str)
110
+ return response
111
+
112
+ def to_xlsx(self):
113
+ """Export to XLSX."""
114
+ response = HttpResponse(
115
+ content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
116
+ response["Content-Disposition"] = f'attachment; filename="{self.filename}.xlsx"'
117
+ df = pd.DataFrame(self.get_data())
118
+ with BytesIO() as buffer:
119
+ writer = pd.ExcelWriter(buffer, engine="xlsxwriter")
120
+ df.to_excel(writer, index=False)
121
+ writer.close()
122
+ response.write(buffer.getvalue())
123
+ return response
124
+
125
+ def to_yaml(self):
126
+ """Export to YAML with proper attachment handling."""
127
+ response = HttpResponse(content_type="application/octet-stream")
128
+ response["Content-Disposition"] = f'attachment; filename="{self.filename}.yaml"'
129
+ yaml_str = yaml.dump(self.get_data(), allow_unicode=True)
130
+ response.write(yaml_str)
131
+ return response
132
+
133
+ def to_tsv(self):
134
+ """Export to TSV with proper attachment handling."""
135
+ response = HttpResponse(content_type="application/octet-stream")
136
+ response["Content-Disposition"] = f'attachment; filename="{self.filename}.tsv"'
137
+ writer = csv.DictWriter(
138
+ response, fieldnames=self.fields, delimiter=" ")
139
+ writer.writeheader()
140
+ writer.writerows(self.get_data())
141
+ return response
@@ -0,0 +1,296 @@
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.0
16
+ Author: Timur Kady
17
+ Email: timurkady@yandex.com
18
+ """
19
+
20
+
21
+ import csv
22
+ import json
23
+ import yaml
24
+ import math
25
+ import pandas as pd
26
+ from io import BytesIO, StringIO
27
+ from django.db import transaction
28
+ import logging
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ class TreeNodeImporter:
34
+ """Импортер древовидных данных из различных форматов."""
35
+
36
+ def __init__(self, model, file, format, fields=None, mapping=None):
37
+ """
38
+ Init method.
39
+
40
+ :param model: Django model where the data will be imported.
41
+ :param file: File object.
42
+ :param format: File format ('csv', 'json', 'xlsx', 'yaml', 'tsv').
43
+ :param fields: List of model fields to import.
44
+ :param mapping: Dictionary for mapping keys from file to model
45
+ field names.
46
+ For example: {"Name": "title", "Description": "desc"}
47
+ """
48
+ self.model = model
49
+ self.format = format
50
+ # Если поля не заданы, используем все поля модели
51
+ self.fields = fields or [field.name for field in model._meta.fields]
52
+ # По умолчанию маппинг идентичен: ключи совпадают с именами полей
53
+ self.mapping = mapping or {field: field for field in self.fields}
54
+ # Считываем содержимое файла один раз, чтобы избежать проблем с курсором
55
+ self.file_content = file.read()
56
+
57
+ def get_text_content(self):
58
+ """Возвращает содержимое файла в виде строки."""
59
+ if isinstance(self.file_content, bytes):
60
+ return self.file_content.decode("utf-8")
61
+ return self.file_content
62
+
63
+ def import_data(self):
64
+ """Импортирует данные и возвращает список словарей."""
65
+ importers = {
66
+ "csv": self.from_csv,
67
+ "json": self.from_json,
68
+ "xlsx": self.from_xlsx,
69
+ "yaml": self.from_yaml,
70
+ "tsv": self.from_tsv,
71
+ }
72
+ if self.format not in importers:
73
+ raise ValueError("Unsupported import format")
74
+
75
+ raw_data = importers[self.format]()
76
+ # Обработка: фильтрация полей, упаковка сложных значений и приведение типов
77
+ processed_data = self.process_records(raw_data)
78
+ return processed_data
79
+
80
+ def from_csv(self):
81
+ """Импорт из CSV."""
82
+ text = self.get_text_content()
83
+ return list(csv.DictReader(StringIO(text)))
84
+
85
+ def from_json(self):
86
+ """Импорт из JSON."""
87
+ return json.loads(self.get_text_content())
88
+
89
+ def from_xlsx(self):
90
+ """Импорт из XLSX (Excel)."""
91
+ df = pd.read_excel(BytesIO(self.file_content))
92
+ return df.to_dict(orient="records")
93
+
94
+ def from_yaml(self):
95
+ """Импорт из YAML."""
96
+ return yaml.safe_load(self.get_text_content())
97
+
98
+ def from_tsv(self):
99
+ """Импорт из TSV."""
100
+ text = self.get_text_content()
101
+ return list(csv.DictReader(StringIO(text), delimiter="\t"))
102
+
103
+ def filter_fields(self, record):
104
+ """
105
+ Фильтрует запись согласно маппингу.
106
+ Остаются только нужные ключи, при этом имена переименовываются.
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
+ Если значение поля — словарь или список, упаковывает его в JSON-строку.
116
+ """
117
+ for key, value in record.items():
118
+ if isinstance(value, (list, dict)):
119
+ try:
120
+ record[key] = json.dumps(value, ensure_ascii=False)
121
+ except Exception as e:
122
+ logger.warning("Error serializing field %s: %s", key, e)
123
+ record[key] = None
124
+ return record
125
+
126
+ def cast_record_types(self, record):
127
+ """
128
+ Приводит значения полей записи к типам, определённым в модели.
129
+
130
+ Для каждого поля вызывается его метод to_python(). Если значение равно nan,
131
+ оно заменяется на None.
132
+
133
+ Для ForeignKey-полей (many-to-one) значение записывается в атрибут <field>_id,
134
+ а исходный ключ удаляется.
135
+ """
136
+ for field in self.model._meta.fields:
137
+ field_name = field.name
138
+ if field.is_relation and field.many_to_one:
139
+ if field_name in record:
140
+ value = record[field_name]
141
+ if isinstance(value, float) and math.isnan(value):
142
+ value = None
143
+ try:
144
+ converted = None if value is None else int(value)
145
+ # Записываем в атрибут, например, tn_parent_id
146
+ record[field.attname] = converted
147
+ except Exception as e:
148
+ logger.warning("Error converting FK field %s with value %r: %s",
149
+ field_name, value, e)
150
+ record[field.attname] = None
151
+ # Удаляем оригинальное значение, чтобы Django не пыталась его обработать
152
+ del record[field_name]
153
+ else:
154
+ if field_name in record:
155
+ value = record[field_name]
156
+ if isinstance(value, float) and math.isnan(value):
157
+ record[field_name] = None
158
+ else:
159
+ try:
160
+ record[field_name] = field.to_python(value)
161
+ except Exception as e:
162
+ logger.warning("Error converting field %s with value %r: %s",
163
+ field_name, value, e)
164
+ record[field_name] = None
165
+ return record
166
+
167
+ def process_records(self, records):
168
+ """
169
+ Обрабатывает список записей:
170
+ 1. Фильтрует поля по маппингу.
171
+ 2. Упаковывает сложные (вложенные) данные в JSON.
172
+ 3. Приводит значения каждого поля к типам, определённым в модели.
173
+ """
174
+ processed = []
175
+ for record in records:
176
+ filtered = self.filter_fields(record)
177
+ filtered = self.process_complex_fields(filtered)
178
+ filtered = self.cast_record_types(filtered)
179
+ processed.append(filtered)
180
+ return processed
181
+
182
+ def clean(self, raw_data):
183
+ """
184
+ Валидирует и подготавливает данные для массового сохранения объектов.
185
+
186
+ Для каждой записи:
187
+ - Проверяется наличие уникального поля 'id'.
188
+ - Значение родительской связи (tn_parent или tn_parent_id) сохраняется отдельно и удаляется из данных.
189
+ - Приводит данные к типам модели.
190
+ - Пытается создать экземпляр модели с валидацией через full_clean().
191
+
192
+ Возвращает словарь со следующими ключами:
193
+ 'create' - список объектов для создания,
194
+ 'update' - список объектов для обновления (в данном случае оставим пустым),
195
+ 'update_fields' - список полей, подлежащих обновлению (например, ['tn_parent']),
196
+ 'fk_mappings' - словарь {id_объекта: значение родительского ключа из исходных данных},
197
+ 'errors' - список ошибок валидации.
198
+ """
199
+ result = {
200
+ "create": [],
201
+ "update": [],
202
+ "update_fields": [],
203
+ "fk_mappings": {},
204
+ "errors": []
205
+ }
206
+
207
+ for data in raw_data:
208
+ if 'id' not in data:
209
+ error_message = f"Missing unique field 'id' in record: {data}"
210
+ result["errors"].append(error_message)
211
+ logger.warning(error_message)
212
+ continue
213
+
214
+ # Сохраняем значение родительской связи и удаляем его из данных
215
+ fk_value = None
216
+ if 'tn_parent' in data:
217
+ fk_value = data['tn_parent']
218
+ del data['tn_parent']
219
+ elif 'tn_parent_id' in data:
220
+ fk_value = data['tn_parent_id']
221
+ del data['tn_parent_id']
222
+
223
+ # Приводим значения к типам модели
224
+ data = self.cast_record_types(data)
225
+
226
+ try:
227
+ instance = self.model(**data)
228
+ instance.full_clean()
229
+ result["create"].append(instance)
230
+ # Сохраняем значение родительского ключа для последующего обновления
231
+ result["fk_mappings"][instance.id] = fk_value
232
+ except Exception as e:
233
+ error_message = f"Validation error creating {data}: {e}"
234
+ result["errors"].append(error_message)
235
+ logger.warning(error_message)
236
+ continue
237
+
238
+ # В данном сценарии обновление происходит только для родительской связи
239
+ result["update_fields"] = ['tn_parent']
240
+ return result
241
+
242
+ def save_data(self, create, update, fields):
243
+ """
244
+ Сохраняет объекты в базу в рамках атомарной транзакции.
245
+ :param create: список объектов для создания.
246
+ :param update: список объектов для обновления.
247
+ :param fields: список полей, которые обновляются (для bulk_update).
248
+ """
249
+ with transaction.atomic():
250
+ if update:
251
+ self.model.objects.bulk_update(update, fields, batch_size=1000)
252
+ if create:
253
+ self.model.objects.bulk_create(create, batch_size=1000)
254
+
255
+ def update_parent_relations(self, fk_mappings):
256
+ """
257
+ Обновляет поле tn_parent для объектов, используя сохранённые fk_mappings.
258
+ :param fk_mappings: словарь {id_объекта: значение родительского ключа из исходных данных}
259
+ """
260
+ instances_to_update = []
261
+ for obj_id, parent_id in fk_mappings.items():
262
+ # Если родитель не указан, пропускаем
263
+ if not parent_id:
264
+ continue
265
+ try:
266
+ instance = self.model.objects.get(pk=obj_id)
267
+ parent_instance = self.model.objects.get(pk=parent_id)
268
+ instance.tn_parent = parent_instance
269
+ instances_to_update.append(instance)
270
+ except self.model.DoesNotExist:
271
+ logger.warning(
272
+ "Parent with id %s not found for instance %s", parent_id, obj_id)
273
+ if instances_to_update:
274
+ update_fields = ['tn_parent']
275
+ self.model.objects.bulk_update(
276
+ instances_to_update, update_fields, batch_size=1000)
277
+
278
+ # Если захочешь объединить операции сохранения и обновления родителей,
279
+ # можно добавить метод, который вызовет save_data и update_parent_relations последовательно.
280
+ def finalize_import(self, clean_result):
281
+ """
282
+ Финализирует импорт: сохраняет новые объекты и обновляет родительские связи.
283
+ :param clean_result: словарь, возвращённый методом clean.
284
+ """
285
+ # Если есть ошибки – можно прервать импорт или вернуть их для обработки
286
+ if clean_result["errors"]:
287
+ return clean_result["errors"]
288
+
289
+ # Сначала выполняем массовое создание
290
+ self.save_data(
291
+ clean_result["create"], clean_result["update"], clean_result["update_fields"])
292
+ # Затем обновляем родительские связи
293
+ self.update_parent_relations(clean_result["fk_mappings"])
294
+ return None # Или вернуть успешное сообщение
295
+
296
+ # The End
treenode/version.py CHANGED
@@ -1,3 +1,13 @@
1
1
  # -*- coding: utf-8 -*-
2
+ """
3
+ TreeNode Version Module
2
4
 
3
- __version__ = '1.1.0'
5
+ This module defines the current version of the TreeNode package.
6
+
7
+ Version: 2.0.0
8
+ Author: Timur Kady
9
+ Email: timurkady@yandex.com
10
+ """
11
+
12
+
13
+ __version__ = '2.0.0'
treenode/views.py CHANGED
@@ -1,3 +1,103 @@
1
- from django.shortcuts import render
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ TreeNode Views Module
2
4
 
3
- # Create your views here.
5
+ This module provides API views for handling AJAX requests related to
6
+ tree-structured data in Django. It supports Select2 autocomplete
7
+ and retrieving child node counts.
8
+
9
+ Features:
10
+ - `TreeNodeAutocompleteView`: Returns JSON data for Select2 with hierarchical
11
+ structure.
12
+ - `GetChildrenCountView`: Retrieves the number of children for a given
13
+ parent node.
14
+ - Uses optimized QuerySets for efficient database queries.
15
+ - Handles validation and error responses gracefully.
16
+
17
+ Version: 2.0.0
18
+ Author: Timur Kady
19
+ Email: timurkady@yandex.com
20
+ """
21
+
22
+
23
+ from django.http import JsonResponse
24
+ from django.views import View
25
+ from django.apps import apps
26
+ from django.db.models import Case, When, Value, IntegerField
27
+ from django.core.exceptions import ObjectDoesNotExist
28
+
29
+
30
+ class TreeNodeAutocompleteView(View):
31
+ """Returns JSON data for Select2 with tree structure."""
32
+
33
+ def get(self, request):
34
+ """Get method."""
35
+ q = request.GET.get("q", "")
36
+ model_label = request.GET.get("model") # Получаем модель
37
+
38
+ if not model_label:
39
+ return JsonResponse(
40
+ {"error": "Missing model parameter"},
41
+ status=400
42
+ )
43
+
44
+ try:
45
+ model = apps.get_model(model_label)
46
+ except LookupError:
47
+ return JsonResponse(
48
+ {"error": f"Invalid model: {model_label}"},
49
+ status=400
50
+ )
51
+
52
+ queryset = model.objects.filter(name__icontains=q)
53
+ node_list = sorted(queryset, key=lambda x: x.tn_order)
54
+ pk_list = [node.pk for node in node_list]
55
+ nodes = queryset.filter(pk__in=pk_list).order_by(
56
+ Case(*[When(pk=pk, then=Value(index))
57
+ for index, pk in enumerate(pk_list)],
58
+ default=Value(len(pk_list)),
59
+ output_field=IntegerField())
60
+ )[:10]
61
+
62
+ results = [
63
+ {
64
+ "id": node.pk,
65
+ "text": node.name,
66
+ "level": node.get_level(),
67
+ "is_leaf": node.is_leaf(),
68
+ }
69
+ for node in nodes
70
+ ]
71
+ return JsonResponse({"results": results})
72
+
73
+
74
+ class GetChildrenCountView(View):
75
+ """Return the number of children for a given parent node."""
76
+
77
+ def get(self, request):
78
+ """Get method."""
79
+ parent_id = request.GET.get("parent_id")
80
+ model_label = request.GET.get("model") # Получаем модель
81
+
82
+ if not model_label or not parent_id:
83
+ return JsonResponse({"error": "Missing parameters"}, status=400)
84
+
85
+ try:
86
+ model = apps.get_model(model_label)
87
+ except LookupError:
88
+ return JsonResponse(
89
+ {"error": f"Invalid model: {model_label}"},
90
+ status=400
91
+ )
92
+
93
+ try:
94
+ parent_node = model.objects.get(pk=parent_id)
95
+ children_count = parent_node.get_children_count()
96
+ print("parent_id=", parent_id, " children_count=", children_count)
97
+ except ObjectDoesNotExist:
98
+ return JsonResponse(
99
+ {"error": "Parent node not found"},
100
+ status=404
101
+ )
102
+
103
+ return JsonResponse({"children_count": children_count})
treenode/widgets.py CHANGED
@@ -1,43 +1,65 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """
3
+ TreeNode Widgets Module
3
4
 
4
- DATACORE 6th Static Normal Form Models
5
- Widgets Module
5
+ This module defines custom form widgets for handling hierarchical data
6
+ within Django's admin interface. It includes a Select2-based widget
7
+ for tree-structured data selection.
6
8
 
7
- Author: Timur Kady
8
- Email: kaduevtr@gmail.com
9
+ Features:
10
+ - `TreeWidget`: A custom Select2 widget that enhances usability for
11
+ hierarchical models.
12
+ - Automatically fetches hierarchical data via AJAX.
13
+ - Supports dynamic model binding for reusable implementations.
14
+ - Integrates with Django’s form system.
9
15
 
16
+ Version: 2.0.0
17
+ Author: Timur Kady
18
+ Email: timurkady@yandex.com
10
19
  """
20
+
21
+
11
22
  from django import forms
12
23
 
13
24
 
14
25
  class TreeWidget(forms.Select):
15
-
16
- template_name = 'widgets/select2tree.html'
17
- option_template_name = 'widgets/options.html'
26
+ """Custom Select2 widget for hierarchical data."""
18
27
 
19
28
  class Media:
29
+ """Mrta class."""
30
+
20
31
  css = {
21
- 'all': (
22
- 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css',
23
- 'select2tree/select2tree.css',
24
- )}
32
+ "all": (
33
+ "https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css",
34
+ "treenode/css/tree_widget.css",
35
+ )
36
+ }
25
37
  js = (
26
- 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js',
27
- 'select2tree/select2tree.js',
28
-
38
+ "https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js",
39
+ "treenode/js/tree_widget.js",
29
40
  )
30
41
 
31
- def create_option(self, name, value, *args, **kwargs):
32
- option = super().create_option(name, value, *args, **kwargs)
33
- if value:
34
- # get icon instance
35
-
36
- item = self.choices.queryset.get(pk=value.value)
37
- if item.tn_parent:
38
- option['parent'] = item.tn_parent.id
39
- else:
40
- option['parent'] = ''
41
- option['level'] = item.level
42
- option['leaf'] = item.is_leaf()
43
- return option
42
+ def build_attrs(self, base_attrs, extra_attrs=None):
43
+ """Add attributes for Select2 integration."""
44
+ attrs = super().build_attrs(base_attrs, extra_attrs)
45
+ attrs.setdefault("data-url", "/treenode/tree-autocomplete/")
46
+ existing_class = attrs.get("class", "")
47
+ attrs["class"] = f"{existing_class} tree-widget".strip()
48
+ if "placeholder" in attrs:
49
+ del attrs["placeholder"]
50
+
51
+ # Принудительно передаём `model`
52
+ if "data-forward" not in attrs:
53
+ try:
54
+ model = self.choices.queryset.model
55
+ label = model._meta.app_label
56
+ model_name = model._meta.model_name
57
+ model_label = f"{label}.{model_name}"
58
+ attrs["data-forward"] = f'{{"model": "{model_label}"}}'
59
+ except Exception:
60
+ attrs["data-forward"] = '{"model": ""}'
61
+
62
+ return attrs
63
+
64
+
65
+ # The End