django-fast-treenode 2.1.5__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.
Files changed (107) hide show
  1. django_fast_treenode-3.0.0.dist-info/METADATA +203 -0
  2. django_fast_treenode-3.0.0.dist-info/RECORD +90 -0
  3. {django_fast_treenode-2.1.5.dist-info → django_fast_treenode-3.0.0.dist-info}/WHEEL +1 -1
  4. treenode/admin/__init__.py +0 -5
  5. treenode/admin/admin.py +137 -208
  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 +42 -20
  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.5.dist-info/METADATA +0 -165
  81. django_fast_treenode-2.1.5.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.5.dist-info → django_fast_treenode-3.0.0.dist-info}/licenses/LICENSE +0 -0
  105. {django_fast_treenode-2.1.5.dist-info → django_fast_treenode-3.0.0.dist-info}/top_level.txt +0 -0
  106. /treenode/static/{treenode → css}/.gitkeep +0 -0
  107. /treenode/static/{treenode/css → js}/.gitkeep +0 -0
@@ -0,0 +1,171 @@
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: 3.0.0
16
+ Author: Timur Kady
17
+ Email: timurkady@yandex.com
18
+ """
19
+
20
+ # Новый модуль импорта для древовидных структур в Django
21
+
22
+ import csv
23
+ import json
24
+ import yaml
25
+ import openpyxl
26
+ from io import BytesIO, StringIO
27
+ from django.db import transaction
28
+ from django.core.exceptions import ValidationError, ObjectDoesNotExist
29
+
30
+ from ..cache import treenode_cache as cache
31
+
32
+
33
+ class TreeNodeImporter:
34
+ """Importer of tree data from various formats."""
35
+
36
+ def __init__(self, model, file, file_format):
37
+ """Init."""
38
+ self.model = model
39
+ self.file = file
40
+ self.format = file_format.lower()
41
+ self.rows = []
42
+ self.rows_by_id = {}
43
+ self.result = {"created": 0, "updated": 0, "errors": []}
44
+
45
+ def parse(self):
46
+ """Parse a file."""
47
+ if self.format == "xlsx":
48
+ content = self.file.read() # binary
49
+ self.rows = self._parse_xlsx(content)
50
+ else:
51
+ text = self.file.read()
52
+ if isinstance(text, bytes):
53
+ text = text.decode("utf-8")
54
+
55
+ if self.format == "csv":
56
+ self.rows = list(csv.DictReader(StringIO(text)))
57
+ elif self.format == "tsv":
58
+ self.rows = list(csv.DictReader(StringIO(text), delimiter="\t"))
59
+ elif self.format == "json":
60
+ self.rows = json.loads(text)
61
+ elif self.format == "yaml":
62
+ self.rows = yaml.safe_load(text)
63
+ else:
64
+ raise ValueError("Unsupported file format")
65
+
66
+ self._build_hierarchy()
67
+
68
+ def _parse_xlsx(self, content):
69
+ """Parse the xlsx format."""
70
+ wb = openpyxl.load_workbook(BytesIO(content), read_only=True)
71
+ ws = wb.active
72
+ headers = [cell.value for cell in next(
73
+ ws.iter_rows(min_row=1, max_row=1))]
74
+ return [
75
+ dict(zip(headers, row))
76
+ for row in ws.iter_rows(min_row=2, values_only=True)
77
+ ]
78
+
79
+ def _build_hierarchy(self):
80
+ """
81
+ Build and check hierarchy.
82
+
83
+ Calculates _depth and _path based on parent and priority. Checks that
84
+ each parent exists in either the imported set or the base. The _path
85
+ is built based on priority, as in the main package.
86
+ """
87
+ self.rows_by_id = {str(row.get("id")): row for row in self.rows}
88
+
89
+ def build_path_and_depth(row, visited=None):
90
+ if visited is None:
91
+ visited = set()
92
+ row_id = str(row.get("id"))
93
+ if row_id in visited:
94
+ raise ValueError(f"Cycle detected at row {row_id}")
95
+ visited.add(row_id)
96
+
97
+ parent_id = str(row.get("parent")) if row.get("parent") else None
98
+ if not parent_id:
99
+ row["_depth"] = 0
100
+ row["_path"] = str(row.get("priority", "0")).zfill(4)
101
+ return row["_path"]
102
+
103
+ parent_row = self.rows_by_id.get(parent_id)
104
+ if parent_row:
105
+ parent_path = build_path_and_depth(parent_row, visited)
106
+ else:
107
+ try:
108
+ self.model.objects.get(pk=parent_id)
109
+ parent_path = "fromdb"
110
+ except ObjectDoesNotExist:
111
+ self.result["errors"].append(
112
+ f"Parent {parent_id} for node {row_id} not found.")
113
+ parent_path = "invalid"
114
+
115
+ row["_depth"] = parent_path.count(
116
+ ".") + 1 if parent_path != "invalid" else 0
117
+ priority = str(row.get("priority", "0")).zfill(4)
118
+ row["_path"] = parent_path + "." + \
119
+ priority if parent_path != "invalid" else priority
120
+ return row["_path"]
121
+
122
+ for row in self.rows:
123
+ try:
124
+ build_path_and_depth(row)
125
+ except Exception as e:
126
+ self.result["errors"].append(str(e))
127
+
128
+ def import_tree(self):
129
+ """Import tree nodes level by level."""
130
+ with transaction.atomic():
131
+ rows_by_level = {}
132
+ for row in self.rows:
133
+ level = row.get("_depth", 0)
134
+ rows_by_level.setdefault(level, []).append(row)
135
+
136
+ id_map = {}
137
+ for depth in sorted(rows_by_level.keys()):
138
+ to_create = []
139
+ for row in rows_by_level[depth]:
140
+ pk = row.get("id")
141
+
142
+ if "parent" in row and (row["parent"] == "" or row["parent"] is None):
143
+ row["parent"] = None
144
+
145
+ if "parent" in row:
146
+ temp_parent_id = row.pop("parent")
147
+ if temp_parent_id is not None:
148
+ # Используем уже созданный ID родителя
149
+ row["parent_id"] = id_map.get(
150
+ temp_parent_id, temp_parent_id)
151
+
152
+ try:
153
+ obj = self.model(**row)
154
+ obj.full_clean()
155
+ to_create.append(obj)
156
+ except ValidationError as e:
157
+ self.result["errors"].append(
158
+ f"Validation error for {pk}: {e}")
159
+
160
+ created = self.model.objects.bulk_create(to_create)
161
+ for obj in created:
162
+ id_map[obj.pk] = obj.pk
163
+ self.result["created"] += len(created)
164
+
165
+ self.model.tasks.add("update", None)
166
+ cache.invalidate(self.model._meta.label)
167
+
168
+ return self.result
169
+
170
+
171
+ # The End
@@ -0,0 +1,291 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ TreeNode Admin Model Class Mixin
4
+
5
+ Views for AdminModel
6
+
7
+ Version: 3.0.0
8
+ Author: Timur Kady
9
+ Email: timurkady@yandex.com
10
+ """
11
+
12
+ import os
13
+ import json
14
+ from datetime import datetime
15
+ from django.contrib import admin
16
+ from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
17
+ from django.contrib.admin.utils import lookup_field
18
+ from django.db.models import Q
19
+ from django.http import HttpResponseBadRequest
20
+ from django.http import JsonResponse
21
+ from django.shortcuts import render
22
+ from django.template.loader import render_to_string
23
+ from django.urls import path
24
+ from django.utils.decorators import method_decorator
25
+ from django.utils.safestring import mark_safe
26
+ from django.utils.translation import gettext_lazy as _
27
+ from django.views.decorators.cache import never_cache
28
+
29
+
30
+ class AdminMixin(admin.ModelAdmin):
31
+ """Admin Mixin for lazy loading and search in tree."""
32
+
33
+ def get_search_help_text(self, request):
34
+ """Get search help text."""
35
+ return getattr(super(), 'get_search_help_text', lambda r: '')(request)
36
+
37
+ def get_urls(self):
38
+ """Add custom URLs for AJAX loading and searching."""
39
+ default_urls = super().get_urls()
40
+
41
+ custom_urls = [
42
+ path(
43
+ "change_list/",
44
+ self.admin_site.admin_view(self.ajax_changelist_view),
45
+ name="tree_changelist"
46
+ ),
47
+ path(
48
+ "move/",
49
+ self.admin_site.admin_view(self.ajax_move_view),
50
+ name="tree_changelist_move"
51
+ ),
52
+ path(
53
+ "import/",
54
+ self.admin_site.admin_view(self.import_view),
55
+ name="tree_node_import"
56
+ ),
57
+ path("export/",
58
+ self.admin_site.admin_view(self.export_view),
59
+ name="tree_node_export"
60
+ ),
61
+ ]
62
+ return custom_urls + default_urls
63
+
64
+ def render_changelist_rows(self, objs: list, request):
65
+ """Rander rows for incert to changelist."""
66
+ list_display = list(self.get_list_display(request))
67
+ checkbox_field_name = ACTION_CHECKBOX_NAME
68
+ if checkbox_field_name not in list_display:
69
+ list_display.insert(0, checkbox_field_name)
70
+
71
+ rows = []
72
+ for obj in objs:
73
+ row_data = []
74
+ td_classes = []
75
+
76
+ for field in list_display:
77
+ if field == ACTION_CHECKBOX_NAME:
78
+ checkbox = f'<input type="checkbox" id="action_{obj.pk}" name="{ACTION_CHECKBOX_NAME}" value="{obj.pk}" class="action-select" />' # noqa: D501
79
+ row_data.append(mark_safe(checkbox))
80
+ td_classes.append("action-checkbox")
81
+ continue
82
+
83
+ if callable(field):
84
+ value = field(obj)
85
+ field_name = getattr(field, "__name__", "field")
86
+ else:
87
+ attr, value = lookup_field(field, obj, self)
88
+ field_name = field
89
+
90
+ row_data.append(value)
91
+ td_classes.append(f"field-{field_name}")
92
+
93
+ rows.append({
94
+ "node_id": obj.pk,
95
+ "attrs": f'data-node-id="{obj.pk}" data-parent-of="{obj.parent_id or ""}" class="model-{self.model._meta.model_name} pk-{obj.pk}"', # noqa: D501
96
+ "parent_id": obj.parent_id,
97
+ "cells": list(zip(row_data, td_classes)),
98
+ })
99
+ return rows
100
+
101
+ @method_decorator(never_cache)
102
+ def ajax_changelist_view(self, request, extra_context=None):
103
+ """
104
+ Create a default changelist or load child elements via AJAX.
105
+
106
+ - If `parent_id` is passed, returns only child rows (HTML).
107
+ - If `q` is passed, returns up to 20 matching rows (HTML).
108
+ - Otherwise, displays the full changelist.
109
+ """
110
+ extra_context = extra_context or {}
111
+ extra_context['import_export_enabled'] = self.import_export
112
+ # Define the show_admin_actions variable in the context
113
+ extra_context['show_admin_actions'] = extra_context.get(
114
+ "show_admin_actions", False)
115
+ parent_id = request.GET.get("parent_id", "")
116
+ query = request.GET.get("q", "")
117
+ expanded = request.GET.get("expanded", "[]")
118
+
119
+ if parent_id:
120
+ parent = self.model.objects.filter(pk=parent_id).first()
121
+ rows_list = parent.get_children() if parent else []
122
+
123
+ elif query:
124
+ name_field = getattr(self.model, "display_field", "id")
125
+ rows_list = list(self.model.objects.filter(
126
+ Q(**{f"{name_field}__icontains": query})
127
+ | Q(pk__icontains=query)
128
+ ).order_by("_path")[:20])
129
+
130
+ elif expanded:
131
+ try:
132
+ expanded_list = json.loads(expanded)
133
+ except ValueError:
134
+ return JsonResponse({"error": _("Bad node list.")}, status=422)
135
+ if expanded_list:
136
+ queryset = self.model.objects.filter(
137
+ Q(parent__isnull=True) |
138
+ Q(pk__in=expanded_list) |
139
+ Q(parent_id__in=expanded_list)
140
+ ).distinct()
141
+ else:
142
+ queryset = self.model.objects.filter(parent__isnull=True)
143
+
144
+ rows_list = list(queryset.order_by("_path"))
145
+ else:
146
+ return JsonResponse({"html": ""})
147
+
148
+ rows = self.render_changelist_rows(rows_list, request)
149
+
150
+ # Take model verbose name
151
+ verbose_name = getattr(self.model._meta, "verbose_name_plural", None) \
152
+ or getattr(self.model._meta, "verbose_name", None) \
153
+ or self.model._meta.object_name
154
+
155
+ # Render HTML
156
+ html = render_to_string(
157
+ "admin/treenode_ajax_rows.html",
158
+ {"rows": rows},
159
+ request=request
160
+ )
161
+ return JsonResponse({"html": html, "label": verbose_name})
162
+
163
+ @method_decorator(never_cache)
164
+ def ajax_move_view(self, request, extra_context=None):
165
+ """
166
+ Perform drag-and-drop move operation for a node.
167
+
168
+ Moves a node relative to a target node using the specified mode.
169
+ """
170
+ # 1. Extracting parameters
171
+ node_id = request.POST.get("node_id")
172
+ target_id = request.POST.get("target_id")
173
+ mode = request.POST.get("mode")
174
+
175
+ if not (node_id and mode):
176
+ return JsonResponse({"error": _("Missing parameters.")}, status=400)
177
+
178
+ # 2. Getting objects
179
+ node = self.model.objects.filter(pk=node_id).first()
180
+ if not node or mode not in ("child", "after"):
181
+ return JsonResponse(
182
+ {"error": _(f"Invalid node ({node_id}) or mode ({mode}).")},
183
+ status=422
184
+ )
185
+
186
+ # Prepare the target
187
+ if not target_id or target_id == 'null':
188
+ # Insetr like a root
189
+ target_id = None
190
+ target = None
191
+ else:
192
+ target_id = int(target_id)
193
+ target = self.model.objects.filter(pk=target_id).first()
194
+
195
+ # 3. Protection from moving into your descendants
196
+ descendants_pks = node.query("descendants", include_self=True)
197
+ if target_id in descendants_pks:
198
+ return JsonResponse({
199
+ "error": _("Cannot move a node into its own descendants.")
200
+ }, status=409)
201
+
202
+ # 4. Positioning and moving
203
+ sorting_mode = self.model.sorting_field == 'priority'
204
+ if target:
205
+ position = {
206
+ "after": 'right-sibling' if sorting_mode else 'sorted-sibling',
207
+ "child": 'last-child' if sorting_mode else 'sorted-child'
208
+ }[mode]
209
+ else:
210
+ position = 'first-root' if sorting_mode else 'sorted-root'
211
+
212
+ # 5. Adjustments
213
+ if mode == 'after' and node.parent == target:
214
+ # User wants to insert a node after a node-parent
215
+ # print("User wants to insert a node after a node-parent.")
216
+ pass
217
+
218
+ # Debug
219
+ # print(mode, "-", position)
220
+
221
+ # Moving
222
+ node.move_to(target, position)
223
+
224
+ return JsonResponse({
225
+ "message": _("1 node successfully moved")
226
+ }, status=200)
227
+
228
+ def import_view(self, request):
229
+ """
230
+ Impoern View.
231
+
232
+ Handles file upload and initiates import processing via
233
+ TreeNodeImporter.
234
+ Renders summary and any errors.
235
+ """
236
+ if request.method == "POST":
237
+ file = request.FILES.get("file")
238
+ if not file:
239
+ return HttpResponseBadRequest("Missing file or format")
240
+
241
+ filename = file.name
242
+ extension = os.path.splitext(filename)[1].lower().lstrip('.')
243
+ if extension not in ['csv', 'tsv', 'json', 'xlsx', 'yaml']:
244
+ return JsonResponse(
245
+ {"error": _(f"Invalid file format ({extension}.")},
246
+ status=200
247
+ )
248
+ importer = self.TreeNodeImporter(self.model, file, extension)
249
+ importer.parse()
250
+ result = importer.import_tree()
251
+
252
+ return render(request, "admin/treenode_import_export.html", {
253
+ "created_count": result.get("created", 0),
254
+ "updated_count": result.get("updated", 0),
255
+ "errors": result.get("errors", []),
256
+ "import_active": True
257
+ })
258
+
259
+ return render(
260
+ request,
261
+ "admin/treenode_import_export.html",
262
+ {"import_active": True}
263
+ )
264
+
265
+ def export_view(self, request):
266
+ """
267
+ Export View.
268
+
269
+ Handles GET-based export of data in selected format via
270
+ TreeNodeExporter.
271
+ Returns a streaming response for large datasets.
272
+ """
273
+ if request.GET.get("download"):
274
+ fmt = request.GET.get("format", "csv")
275
+ filename = self.model._meta.object_name
276
+ timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M")
277
+ filename = f"{filename}-{timestamp}.{fmt}"
278
+ exporter = self.TreeNodeExporter(
279
+ self.model,
280
+ filename=filename,
281
+ fileformat=fmt
282
+ )
283
+ return exporter.process_record()
284
+
285
+ return render(
286
+ request,
287
+ "admin/treenode_import_export.html",
288
+ {"import_active": False}
289
+ )
290
+
291
+ # The End
treenode/apps.py CHANGED
@@ -1,34 +1,56 @@
1
- # -*- coding: utf-8 -*-
2
1
  """
3
- TreeNode Application Configuration
2
+ TreeNode configuration definition module.
4
3
 
5
- This module defines the application configuration for the TreeNode app.
6
- It sets the default auto field and specifies the app's name.
4
+ Customization:
5
+ - checks the correctness of the sorting fields
6
+ - checks the correctness of model inheritance
7
+ - starts asynchronous loading of node data into the cache
7
8
 
8
- Version: 2.1.0
9
+ Version: 3.0.0
9
10
  Author: Timur Kady
10
11
  Email: timurkady@yandex.com
11
12
  """
12
13
 
13
-
14
- import logging
15
- from django.apps import AppConfig
16
- from django.db.models.signals import post_migrate
17
-
18
- logger = logging.getLogger(__name__)
14
+ from django.apps import apps, AppConfig
19
15
 
20
16
 
21
17
  class TreeNodeConfig(AppConfig):
22
- """TreeNodeConfig Class."""
18
+ """Config Class."""
23
19
 
24
20
  default_auto_field = "django.db.models.BigAutoField"
25
- name = "treenode"
21
+ name = "supertree"
26
22
 
27
23
  def ready(self):
28
- """
29
- Attach a post_migrate handler.
30
-
31
- This allows you to perform operations after the migration is complete.
32
- """
33
- from .utils.db import post_migrate_update
34
- post_migrate.connect(post_migrate_update, sender=self)
24
+ """Ready method."""
25
+ from .models import TreeNodeModel
26
+
27
+ # Models checking
28
+ subclasses = [
29
+ m for m in apps.get_models()
30
+ if issubclass(m, TreeNodeModel) and m is not TreeNodeModel
31
+ ]
32
+
33
+ for model in subclasses:
34
+
35
+ field_names = {f.name for f in model._meta.get_fields()}
36
+
37
+ # Check display_field is correct
38
+ if model.display_field is not None:
39
+ if model.display_field not in field_names:
40
+ raise ValueError(
41
+ f'Invalid display_field "{model.display_field}. "'
42
+ f'Available fields: {field_names}')
43
+
44
+ # Check sorting_field is correct
45
+ if model.sorting_field is not None:
46
+ if model.sorting_field not in field_names:
47
+ raise ValueError(
48
+ f'Invalid sorting_field "{model.sorting_field}. "'
49
+ f'Available fields: {field_names}')
50
+
51
+ # Check if Meta is a descendant of TreeNodeModel.Meta
52
+ if not issubclass(model.Meta, TreeNodeModel.Meta):
53
+ raise ValueError(
54
+ f'{model.__name__} must inherit Meta class ' +
55
+ 'from TreeNodeModel.Meta.'
56
+ )