django-fast-treenode 1.1.3__py3-none-any.whl → 2.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 (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