fastapi-admin-kit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastapi_admin_kit/__init__.py +73 -0
- fastapi_admin_kit/actions/__init__.py +63 -0
- fastapi_admin_kit/actions/base.py +68 -0
- fastapi_admin_kit/actions/registry.py +43 -0
- fastapi_admin_kit/admin/__init__.py +17 -0
- fastapi_admin_kit/admin/admin_config.py +95 -0
- fastapi_admin_kit/admin/admin_database.py +138 -0
- fastapi_admin_kit/admin/admin_router.py +74 -0
- fastapi_admin_kit/admin/admin_template.py +203 -0
- fastapi_admin_kit/admin/builtin_models.py +284 -0
- fastapi_admin_kit/admin/core.py +1036 -0
- fastapi_admin_kit/admin/decorators.py +70 -0
- fastapi_admin_kit/admin/state.py +76 -0
- fastapi_admin_kit/admin.py +728 -0
- fastapi_admin_kit/api/__init__.py +44 -0
- fastapi_admin_kit/api/auth.py +342 -0
- fastapi_admin_kit/api/crud.py +128 -0
- fastapi_admin_kit/api/deps.py +79 -0
- fastapi_admin_kit/api/roles.py +128 -0
- fastapi_admin_kit/api/schema_generator.py +171 -0
- fastapi_admin_kit/api/schemas.py +81 -0
- fastapi_admin_kit/api/search.py +132 -0
- fastapi_admin_kit/audit/__init__.py +36 -0
- fastapi_admin_kit/audit/context.py +62 -0
- fastapi_admin_kit/audit/diff.py +77 -0
- fastapi_admin_kit/audit/event_bus.py +96 -0
- fastapi_admin_kit/audit/events.py +48 -0
- fastapi_admin_kit/audit/listener.py +159 -0
- fastapi_admin_kit/audit/logger.py +28 -0
- fastapi_admin_kit/audit/middleware.py +39 -0
- fastapi_admin_kit/audit/models.py +53 -0
- fastapi_admin_kit/audit/sqlalchemy_logger.py +58 -0
- fastapi_admin_kit/auth/__init__.py +34 -0
- fastapi_admin_kit/auth/backend.py +95 -0
- fastapi_admin_kit/auth/csrf.py +240 -0
- fastapi_admin_kit/auth/dependencies.py +150 -0
- fastapi_admin_kit/auth/identity.py +181 -0
- fastapi_admin_kit/auth/models.py +246 -0
- fastapi_admin_kit/auth/password.py +35 -0
- fastapi_admin_kit/auth/permissions.py +205 -0
- fastapi_admin_kit/auth/protocol.py +22 -0
- fastapi_admin_kit/auth/ratelimit.py +88 -0
- fastapi_admin_kit/auth/router.py +10 -0
- fastapi_admin_kit/auth/session.py +79 -0
- fastapi_admin_kit/auth/totp.py +83 -0
- fastapi_admin_kit/auth/views.py +165 -0
- fastapi_admin_kit/cli.py +229 -0
- fastapi_admin_kit/config/__init__.py +19 -0
- fastapi_admin_kit/config/audit.py +18 -0
- fastapi_admin_kit/config/auth.py +54 -0
- fastapi_admin_kit/config/behavior.py +27 -0
- fastapi_admin_kit/config/nav.py +32 -0
- fastapi_admin_kit/config/storage.py +22 -0
- fastapi_admin_kit/config/theme.py +215 -0
- fastapi_admin_kit/config/ui.py +147 -0
- fastapi_admin_kit/dashboard/__init__.py +64 -0
- fastapi_admin_kit/db.py +133 -0
- fastapi_admin_kit/exceptions.py +5 -0
- fastapi_admin_kit/field_types.py +81 -0
- fastapi_admin_kit/filters/__init__.py +21 -0
- fastapi_admin_kit/filters/base.py +170 -0
- fastapi_admin_kit/filters/registry.py +68 -0
- fastapi_admin_kit/flash.py +45 -0
- fastapi_admin_kit/form/__init__.py +1 -0
- fastapi_admin_kit/form/pipeline.py +106 -0
- fastapi_admin_kit/inspection/__init__.py +117 -0
- fastapi_admin_kit/inspection/registry.py +253 -0
- fastapi_admin_kit/inspection.py +115 -0
- fastapi_admin_kit/modeladmin.py +375 -0
- fastapi_admin_kit/models/__init__.py +7 -0
- fastapi_admin_kit/models/base.py +7 -0
- fastapi_admin_kit/nav.py +208 -0
- fastapi_admin_kit/pagination/__init__.py +14 -0
- fastapi_admin_kit/pagination/base.py +40 -0
- fastapi_admin_kit/pagination/cursor.py +97 -0
- fastapi_admin_kit/pagination/dynamic.py +48 -0
- fastapi_admin_kit/pagination/offset.py +42 -0
- fastapi_admin_kit/plugins/__init__.py +1 -0
- fastapi_admin_kit/py.typed +0 -0
- fastapi_admin_kit/registry/__init__.py +5 -0
- fastapi_admin_kit/registry/core.py +287 -0
- fastapi_admin_kit/registry/validation.py +107 -0
- fastapi_admin_kit/registry.py +15 -0
- fastapi_admin_kit/router.py +335 -0
- fastapi_admin_kit/static/css/admin.css +4736 -0
- fastapi_admin_kit/static/css/presets.css +317 -0
- fastapi_admin_kit/static/css/tokens.css +217 -0
- fastapi_admin_kit/static/css/variables.css +74 -0
- fastapi_admin_kit/static/icons/heroicons.svg +160 -0
- fastapi_admin_kit/static/js/admin.js +692 -0
- fastapi_admin_kit/static/js/htmx-config.js +42 -0
- fastapi_admin_kit/storage/__init__.py +6 -0
- fastapi_admin_kit/storage/base.py +48 -0
- fastapi_admin_kit/storage/local.py +73 -0
- fastapi_admin_kit/templates/base.html +142 -0
- fastapi_admin_kit/templates/macros/form_fields.html +660 -0
- fastapi_admin_kit/templates/macros/icons.html +50 -0
- fastapi_admin_kit/templates/macros/table.html +108 -0
- fastapi_admin_kit/templates/macros/widgets.html +159 -0
- fastapi_admin_kit/templates/pages/2fa/setup.html +122 -0
- fastapi_admin_kit/templates/pages/2fa/verify.html +55 -0
- fastapi_admin_kit/templates/pages/audit_detail.html +122 -0
- fastapi_admin_kit/templates/pages/audit_log.html +102 -0
- fastapi_admin_kit/templates/pages/dashboard.html +295 -0
- fastapi_admin_kit/templates/pages/detail.html +183 -0
- fastapi_admin_kit/templates/pages/form.html +119 -0
- fastapi_admin_kit/templates/pages/list.html +277 -0
- fastapi_admin_kit/templates/pages/login.html +85 -0
- fastapi_admin_kit/templates/pages/profile/password.html +78 -0
- fastapi_admin_kit/templates/pages/profile/profile.html +73 -0
- fastapi_admin_kit/templates/pages/role_form.html +75 -0
- fastapi_admin_kit/templates/pages/roles/form.html +117 -0
- fastapi_admin_kit/templates/pages/roles/list.html +69 -0
- fastapi_admin_kit/templates/pages/roles.html +77 -0
- fastapi_admin_kit/templates/pages/settings/theme.html +255 -0
- fastapi_admin_kit/templates/pages/users/form.html +229 -0
- fastapi_admin_kit/templates/pages/users/list.html +83 -0
- fastapi_admin_kit/templates/partials/command_palette.html +52 -0
- fastapi_admin_kit/templates/partials/field_wrapper.html +2 -0
- fastapi_admin_kit/templates/partials/flash_messages.html +39 -0
- fastapi_admin_kit/templates/partials/head.html +21 -0
- fastapi_admin_kit/templates/partials/head_minimal.html +18 -0
- fastapi_admin_kit/templates/partials/list_table.html +178 -0
- fastapi_admin_kit/templates/partials/mobile_backdrop.html +2 -0
- fastapi_admin_kit/templates/partials/pagination.html +82 -0
- fastapi_admin_kit/templates/partials/permission_widget.html +86 -0
- fastapi_admin_kit/templates/partials/scripts.html +13 -0
- fastapi_admin_kit/templates/partials/sidebar.html +94 -0
- fastapi_admin_kit/templates/partials/topbar.html +95 -0
- fastapi_admin_kit/types.py +145 -0
- fastapi_admin_kit/validation.py +43 -0
- fastapi_admin_kit/views/__init__.py +78 -0
- fastapi_admin_kit/views/audit.py +134 -0
- fastapi_admin_kit/views/bulk.py +28 -0
- fastapi_admin_kit/views/class_views.py +1040 -0
- fastapi_admin_kit/views/context.py +588 -0
- fastapi_admin_kit/views/dashboard.py +162 -0
- fastapi_admin_kit/views/delete.py +31 -0
- fastapi_admin_kit/views/extra.py +65 -0
- fastapi_admin_kit/views/factory.py +667 -0
- fastapi_admin_kit/views/form.py +159 -0
- fastapi_admin_kit/views/list.py +28 -0
- fastapi_admin_kit/views/profile.py +219 -0
- fastapi_admin_kit/views/protocols.py +54 -0
- fastapi_admin_kit/views/renderers.py +634 -0
- fastapi_admin_kit/views/roles.py +230 -0
- fastapi_admin_kit/views/search.py +31 -0
- fastapi_admin_kit/views/settings.py +31 -0
- fastapi_admin_kit/views/sidebar.py +101 -0
- fastapi_admin_kit/views/totp.py +249 -0
- fastapi_admin_kit/views/users.py +347 -0
- fastapi_admin_kit/views.py +117 -0
- fastapi_admin_kit/widgets/__init__.py +44 -0
- fastapi_admin_kit/widgets/base.py +44 -0
- fastapi_admin_kit/widgets/inputs.py +363 -0
- fastapi_admin_kit/widgets/registry.py +110 -0
- fastapi_admin_kit/widgets/relation.py +70 -0
- fastapi_admin_kit/widgets/resolver.py +102 -0
- fastapi_admin_kit-0.1.0.dist-info/METADATA +210 -0
- fastapi_admin_kit-0.1.0.dist-info/RECORD +163 -0
- fastapi_admin_kit-0.1.0.dist-info/WHEEL +4 -0
- fastapi_admin_kit-0.1.0.dist-info/entry_points.txt +3 -0
- fastapi_admin_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
"""ViewContextBuilder — builds template contexts for CRUD views."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from fastapi import Request
|
|
8
|
+
from sqlalchemy import and_, asc, desc, or_, select
|
|
9
|
+
from sqlalchemy.orm import joinedload
|
|
10
|
+
|
|
11
|
+
from fastapi_admin_kit.db import get_db_session
|
|
12
|
+
from fastapi_admin_kit.registry import RegisteredModel
|
|
13
|
+
from fastapi_admin_kit.types import PermissionSet
|
|
14
|
+
from fastapi_admin_kit.views.sidebar import inject_sidebar_context
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DisplayColumn:
|
|
18
|
+
"""Helper to render a column in the list view."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
name: str,
|
|
23
|
+
label: str,
|
|
24
|
+
is_relation: bool = False,
|
|
25
|
+
display_fn: Any = None,
|
|
26
|
+
options: Any = None,
|
|
27
|
+
):
|
|
28
|
+
self.name = name
|
|
29
|
+
self.label = label
|
|
30
|
+
self.is_relation = is_relation
|
|
31
|
+
self.display_fn = display_fn
|
|
32
|
+
self.options = options
|
|
33
|
+
self.boolean = options.boolean if options else False
|
|
34
|
+
self.css_class = options.css_class if options else ""
|
|
35
|
+
self.width = options.width if options else None
|
|
36
|
+
self.icon = options.icon if options else ""
|
|
37
|
+
|
|
38
|
+
def value(self, obj: Any) -> Any:
|
|
39
|
+
if self.display_fn:
|
|
40
|
+
result = self.display_fn(obj)
|
|
41
|
+
if result is None:
|
|
42
|
+
return self.options.empty_value if self.options else "-"
|
|
43
|
+
if self.options and self.options.format and result is not None:
|
|
44
|
+
try:
|
|
45
|
+
return self.options.format.format(result)
|
|
46
|
+
except (ValueError, IndexError):
|
|
47
|
+
return str(result)
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
val = getattr(obj, self.name, "")
|
|
51
|
+
|
|
52
|
+
if self.is_relation and val is not None:
|
|
53
|
+
from fastapi_admin_kit.inspection import model_display_name
|
|
54
|
+
|
|
55
|
+
return model_display_name(val)
|
|
56
|
+
|
|
57
|
+
return val
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ViewContextBuilder:
|
|
61
|
+
"""Builds template contexts for list, form, and delete views.
|
|
62
|
+
|
|
63
|
+
Centralizes context construction logic that was previously duplicated
|
|
64
|
+
across multiple factory functions.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
registry: Any = None,
|
|
70
|
+
permission_checker: Any = None,
|
|
71
|
+
widget_resolver: Any = None,
|
|
72
|
+
):
|
|
73
|
+
self.registry = registry
|
|
74
|
+
self.permission_checker = permission_checker
|
|
75
|
+
self.widget_resolver = widget_resolver
|
|
76
|
+
|
|
77
|
+
def _get_eager_loads(self, model: Any, list_display: list[str]) -> list:
|
|
78
|
+
"""Build eager load options for relationship columns."""
|
|
79
|
+
from sqlalchemy import inspect as sa_inspect
|
|
80
|
+
|
|
81
|
+
mapper = sa_inspect(model)
|
|
82
|
+
rel_names = {r.key for r in mapper.relationships}
|
|
83
|
+
options = []
|
|
84
|
+
for col_name in list_display:
|
|
85
|
+
if col_name in rel_names:
|
|
86
|
+
options.append(joinedload(getattr(model, col_name)))
|
|
87
|
+
return options
|
|
88
|
+
|
|
89
|
+
def _get_field_type(self, model: Any, field_name: str) -> str:
|
|
90
|
+
"""Detect the abstract field type for a model field."""
|
|
91
|
+
from sqlalchemy import inspect as sa_inspect
|
|
92
|
+
|
|
93
|
+
mapper = sa_inspect(model)
|
|
94
|
+
rel_names = {r.key for r in mapper.relationships}
|
|
95
|
+
|
|
96
|
+
if field_name in rel_names:
|
|
97
|
+
return "relation"
|
|
98
|
+
|
|
99
|
+
for prop in mapper.column_attrs:
|
|
100
|
+
if prop.key == field_name:
|
|
101
|
+
col = prop.columns[0] if prop.columns else None
|
|
102
|
+
if col is None:
|
|
103
|
+
break
|
|
104
|
+
type_name = col.type.__class__.__name__
|
|
105
|
+
if type_name == "Boolean":
|
|
106
|
+
return "boolean"
|
|
107
|
+
if type_name == "DateTime":
|
|
108
|
+
return "datetime"
|
|
109
|
+
if type_name == "Date":
|
|
110
|
+
return "date"
|
|
111
|
+
if type_name == "Time":
|
|
112
|
+
return "time"
|
|
113
|
+
if hasattr(col.type, "enums") and col.type.enums:
|
|
114
|
+
return "enum"
|
|
115
|
+
if col.foreign_keys:
|
|
116
|
+
return "relation"
|
|
117
|
+
return "text"
|
|
118
|
+
return "text"
|
|
119
|
+
|
|
120
|
+
async def _get_filter_choices(
|
|
121
|
+
self, model: Any, field_name: str, session: Any = None
|
|
122
|
+
) -> dict[str, Any]:
|
|
123
|
+
"""Get filter field type and available choices for a field."""
|
|
124
|
+
from sqlalchemy import inspect as sa_inspect
|
|
125
|
+
from sqlalchemy import select as sa_select
|
|
126
|
+
|
|
127
|
+
mapper = sa_inspect(model)
|
|
128
|
+
field_type = self._get_field_type(model, field_name)
|
|
129
|
+
|
|
130
|
+
if field_type == "relation":
|
|
131
|
+
rel_map = {r.key: r for r in mapper.relationships}
|
|
132
|
+
target_model = None
|
|
133
|
+
if field_name in rel_map:
|
|
134
|
+
target_model = rel_map[field_name].mapper.class_
|
|
135
|
+
else:
|
|
136
|
+
for rel in mapper.relationships:
|
|
137
|
+
if rel.direction.name == "MANYTOONE":
|
|
138
|
+
for prop in mapper.column_attrs:
|
|
139
|
+
if prop.key == field_name:
|
|
140
|
+
col = prop.columns[0] if prop.columns else None
|
|
141
|
+
if col is not None:
|
|
142
|
+
for fk in col.foreign_keys:
|
|
143
|
+
if (
|
|
144
|
+
fk.column.table
|
|
145
|
+
== rel.mapper.persist_selectable
|
|
146
|
+
):
|
|
147
|
+
target_model = rel.mapper.class_
|
|
148
|
+
break
|
|
149
|
+
if target_model is not None:
|
|
150
|
+
break
|
|
151
|
+
|
|
152
|
+
choices: list[tuple[str, str]] = [("", "All")]
|
|
153
|
+
if target_model is not None and session is not None:
|
|
154
|
+
try:
|
|
155
|
+
order_col = getattr(target_model, "name", None) or getattr(
|
|
156
|
+
target_model, "title", None
|
|
157
|
+
)
|
|
158
|
+
if order_col is not None:
|
|
159
|
+
q = (
|
|
160
|
+
sa_select(target_model)
|
|
161
|
+
.order_by(order_col)
|
|
162
|
+
.limit(100)
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
pk = sa_inspect(target_model).primary_key[0]
|
|
166
|
+
q = sa_select(target_model).order_by(pk).limit(100)
|
|
167
|
+
result = await session.execute(q)
|
|
168
|
+
for obj in result.scalars():
|
|
169
|
+
label = str(
|
|
170
|
+
getattr(obj, "name", None)
|
|
171
|
+
or getattr(obj, "title", None)
|
|
172
|
+
or f"#{getattr(obj, 'id', '?')}"
|
|
173
|
+
)
|
|
174
|
+
choices.append((str(obj.id), label))
|
|
175
|
+
except Exception:
|
|
176
|
+
pass
|
|
177
|
+
return {"field_type": field_type, "choices": choices}
|
|
178
|
+
|
|
179
|
+
if field_type == "boolean":
|
|
180
|
+
return {
|
|
181
|
+
"field_type": "boolean",
|
|
182
|
+
"choices": [("", "All"), ("1", "Yes"), ("0", "No")],
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if field_type == "enum":
|
|
186
|
+
for prop in mapper.column_attrs:
|
|
187
|
+
if prop.key == field_name:
|
|
188
|
+
col = prop.columns[0] if prop.columns else None
|
|
189
|
+
if col is not None and hasattr(col.type, "enums"):
|
|
190
|
+
choices = [("", "All")]
|
|
191
|
+
for val in col.type.enums:
|
|
192
|
+
choices.append((val, val.replace("_", " ").title()))
|
|
193
|
+
return {"field_type": "enum", "choices": choices}
|
|
194
|
+
|
|
195
|
+
if field_type in ("date", "datetime", "time"):
|
|
196
|
+
return {"field_type": field_type, "choices": [("", "All")]}
|
|
197
|
+
|
|
198
|
+
choices = [("", "All")]
|
|
199
|
+
for prop in mapper.column_attrs:
|
|
200
|
+
if prop.key == field_name:
|
|
201
|
+
col = prop.columns[0] if prop.columns else None
|
|
202
|
+
if col is not None and session is not None:
|
|
203
|
+
try:
|
|
204
|
+
q = (
|
|
205
|
+
select(col)
|
|
206
|
+
.where(col.isnot(None))
|
|
207
|
+
.group_by(col)
|
|
208
|
+
.order_by(col)
|
|
209
|
+
.limit(100)
|
|
210
|
+
)
|
|
211
|
+
result = session.execute(q)
|
|
212
|
+
for (val,) in result:
|
|
213
|
+
label = str(val).replace("_", " ").title()
|
|
214
|
+
choices.append((str(val), label))
|
|
215
|
+
except Exception:
|
|
216
|
+
pass
|
|
217
|
+
return {"field_type": "text", "choices": choices}
|
|
218
|
+
|
|
219
|
+
async def build_list_context(
|
|
220
|
+
self,
|
|
221
|
+
registered: RegisteredModel,
|
|
222
|
+
request: Request,
|
|
223
|
+
q: str = "",
|
|
224
|
+
page: int = 1,
|
|
225
|
+
permission_checker: Any = None,
|
|
226
|
+
) -> dict[str, Any]:
|
|
227
|
+
"""Build the template context for a list view.
|
|
228
|
+
|
|
229
|
+
Returns a dict suitable for passing to TemplateResponse.
|
|
230
|
+
"""
|
|
231
|
+
session = get_db_session(request)
|
|
232
|
+
model = registered.model
|
|
233
|
+
base = select(model)
|
|
234
|
+
|
|
235
|
+
list_display = registered.admin.list_display or [
|
|
236
|
+
c.name for c in registered.columns if c.name != "id"
|
|
237
|
+
]
|
|
238
|
+
|
|
239
|
+
eager_loads = self._get_eager_loads(model, list_display)
|
|
240
|
+
for opt in eager_loads:
|
|
241
|
+
base = base.options(opt)
|
|
242
|
+
|
|
243
|
+
active_filters: dict[str, str] = {}
|
|
244
|
+
if registered.admin.list_filter:
|
|
245
|
+
filter_clauses = []
|
|
246
|
+
for filter_field in registered.admin.list_filter:
|
|
247
|
+
param_key = f"filter_{filter_field}"
|
|
248
|
+
filter_value = request.query_params.get(param_key, "")
|
|
249
|
+
if filter_value and hasattr(model, filter_field):
|
|
250
|
+
field_type = self._get_field_type(model, filter_field)
|
|
251
|
+
col = getattr(model, filter_field)
|
|
252
|
+
|
|
253
|
+
if field_type == "boolean":
|
|
254
|
+
bool_val = filter_value == "1"
|
|
255
|
+
filter_clauses.append(col == bool_val)
|
|
256
|
+
elif field_type == "datetime":
|
|
257
|
+
from datetime import datetime as _dt
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
parsed = _dt.fromisoformat(filter_value)
|
|
261
|
+
except (ValueError, TypeError):
|
|
262
|
+
parsed = None
|
|
263
|
+
if parsed is not None:
|
|
264
|
+
filter_clauses.append(col == parsed)
|
|
265
|
+
elif field_type == "date":
|
|
266
|
+
from datetime import date as _date
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
parsed = _date.fromisoformat(filter_value)
|
|
270
|
+
except (ValueError, TypeError):
|
|
271
|
+
parsed = None
|
|
272
|
+
if parsed is not None:
|
|
273
|
+
filter_clauses.append(col == parsed)
|
|
274
|
+
elif field_type == "time":
|
|
275
|
+
from datetime import time as _time
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
parsed = _time.fromisoformat(filter_value)
|
|
279
|
+
except (ValueError, TypeError):
|
|
280
|
+
parsed = None
|
|
281
|
+
if parsed is not None:
|
|
282
|
+
filter_clauses.append(col == parsed)
|
|
283
|
+
else:
|
|
284
|
+
filter_clauses.append(col == filter_value)
|
|
285
|
+
|
|
286
|
+
if filter_value:
|
|
287
|
+
active_filters[filter_field] = filter_value
|
|
288
|
+
|
|
289
|
+
# Support range filters: filter_field__gte, filter_field__lte, etc.
|
|
290
|
+
for filter_field in registered.admin.list_filter:
|
|
291
|
+
gte_val = request.query_params.get(
|
|
292
|
+
f"filter_{filter_field}__gte", ""
|
|
293
|
+
)
|
|
294
|
+
lte_val = request.query_params.get(
|
|
295
|
+
f"filter_{filter_field}__lte", ""
|
|
296
|
+
)
|
|
297
|
+
from_val = request.query_params.get(
|
|
298
|
+
f"filter_{filter_field}__from", ""
|
|
299
|
+
)
|
|
300
|
+
to_val = request.query_params.get(
|
|
301
|
+
f"filter_{filter_field}__to", ""
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
if (gte_val or lte_val) and hasattr(model, filter_field):
|
|
305
|
+
col = getattr(model, filter_field)
|
|
306
|
+
if gte_val:
|
|
307
|
+
try:
|
|
308
|
+
col_type = type(col.property.columns[0].type)
|
|
309
|
+
filter_clauses.append(
|
|
310
|
+
col >= col_type().coerce(gte_val)
|
|
311
|
+
)
|
|
312
|
+
except Exception:
|
|
313
|
+
pass
|
|
314
|
+
if lte_val:
|
|
315
|
+
try:
|
|
316
|
+
col_type = type(col.property.columns[0].type)
|
|
317
|
+
filter_clauses.append(
|
|
318
|
+
col <= col_type().coerce(lte_val)
|
|
319
|
+
)
|
|
320
|
+
except Exception:
|
|
321
|
+
pass
|
|
322
|
+
|
|
323
|
+
if (from_val or to_val) and hasattr(model, filter_field):
|
|
324
|
+
col = getattr(model, filter_field)
|
|
325
|
+
field_type = self._get_field_type(model, filter_field)
|
|
326
|
+
if field_type == "date" and from_val:
|
|
327
|
+
try:
|
|
328
|
+
from datetime import date as _date
|
|
329
|
+
|
|
330
|
+
d = _date.fromisoformat(from_val)
|
|
331
|
+
filter_clauses.append(col >= d)
|
|
332
|
+
except (ValueError, TypeError):
|
|
333
|
+
pass
|
|
334
|
+
if field_type == "date" and to_val:
|
|
335
|
+
try:
|
|
336
|
+
from datetime import date as _date
|
|
337
|
+
|
|
338
|
+
d = _date.fromisoformat(to_val)
|
|
339
|
+
filter_clauses.append(col <= d)
|
|
340
|
+
except (ValueError, TypeError):
|
|
341
|
+
pass
|
|
342
|
+
if field_type == "datetime" and from_val:
|
|
343
|
+
try:
|
|
344
|
+
from datetime import datetime as _dt
|
|
345
|
+
|
|
346
|
+
dt = _dt.fromisoformat(from_val)
|
|
347
|
+
filter_clauses.append(col >= dt)
|
|
348
|
+
except (ValueError, TypeError):
|
|
349
|
+
pass
|
|
350
|
+
if field_type == "datetime" and to_val:
|
|
351
|
+
try:
|
|
352
|
+
from datetime import datetime as _dt
|
|
353
|
+
|
|
354
|
+
dt = _dt.fromisoformat(to_val)
|
|
355
|
+
filter_clauses.append(col <= dt)
|
|
356
|
+
except (ValueError, TypeError):
|
|
357
|
+
pass
|
|
358
|
+
|
|
359
|
+
if gte_val:
|
|
360
|
+
active_filters[f"{filter_field}__gte"] = gte_val
|
|
361
|
+
if lte_val:
|
|
362
|
+
active_filters[f"{filter_field}__lte"] = lte_val
|
|
363
|
+
if from_val:
|
|
364
|
+
active_filters[f"{filter_field}__from"] = from_val
|
|
365
|
+
if to_val:
|
|
366
|
+
active_filters[f"{filter_field}__to"] = to_val
|
|
367
|
+
|
|
368
|
+
if filter_clauses:
|
|
369
|
+
base = base.where(and_(*filter_clauses))
|
|
370
|
+
|
|
371
|
+
if q and registered.admin.search_fields:
|
|
372
|
+
clauses = []
|
|
373
|
+
for sf in registered.admin.search_fields:
|
|
374
|
+
if hasattr(model, sf):
|
|
375
|
+
col = getattr(model, sf)
|
|
376
|
+
if hasattr(col, "ilike"):
|
|
377
|
+
clauses.append(col.ilike(f"%{q}%"))
|
|
378
|
+
if clauses:
|
|
379
|
+
base = base.where(or_(*clauses))
|
|
380
|
+
|
|
381
|
+
query_ordering = request.query_params.get("ordering", "")
|
|
382
|
+
if query_ordering:
|
|
383
|
+
order = [query_ordering]
|
|
384
|
+
else:
|
|
385
|
+
order = registered.admin.ordering or []
|
|
386
|
+
if order:
|
|
387
|
+
col_name = order[0].lstrip("-")
|
|
388
|
+
col = (
|
|
389
|
+
getattr(model, col_name, None)
|
|
390
|
+
if hasattr(model, col_name)
|
|
391
|
+
else None
|
|
392
|
+
)
|
|
393
|
+
if col is not None:
|
|
394
|
+
base = base.order_by(
|
|
395
|
+
desc(col) if order[0].startswith("-") else asc(col)
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
per_page = registered.admin.per_page
|
|
399
|
+
|
|
400
|
+
from fastapi_admin_kit.pagination import OffsetPagination, PaginationResult
|
|
401
|
+
|
|
402
|
+
pagination = getattr(registered.admin, "pagination", None) or OffsetPagination()
|
|
403
|
+
pk_col = getattr(model, registered.pk_field) if registered.pk_field else None
|
|
404
|
+
pagination_result: PaginationResult = await pagination.paginate(
|
|
405
|
+
base,
|
|
406
|
+
session,
|
|
407
|
+
per_page=per_page,
|
|
408
|
+
page=page,
|
|
409
|
+
after=request.query_params.get("after"),
|
|
410
|
+
before=request.query_params.get("before"),
|
|
411
|
+
pk_col=pk_col,
|
|
412
|
+
model=model,
|
|
413
|
+
)
|
|
414
|
+
items = pagination_result.items
|
|
415
|
+
total = pagination_result.total
|
|
416
|
+
page = pagination_result.page or 1
|
|
417
|
+
total_pages = pagination_result.total_pages or 1
|
|
418
|
+
|
|
419
|
+
from sqlalchemy import inspect as sa_inspect
|
|
420
|
+
|
|
421
|
+
mapper = sa_inspect(model)
|
|
422
|
+
rel_names = {r.key for r in mapper.relationships}
|
|
423
|
+
|
|
424
|
+
# Collect @column decorated methods (check _column_options attribute)
|
|
425
|
+
decorated_columns: dict[str, Any] = {}
|
|
426
|
+
for col_name in list_display:
|
|
427
|
+
method = getattr(registered.admin, col_name, None)
|
|
428
|
+
if method and hasattr(method, "_column_options"):
|
|
429
|
+
decorated_columns[col_name] = method._column_options
|
|
430
|
+
|
|
431
|
+
display_columns = []
|
|
432
|
+
for col_name in list_display:
|
|
433
|
+
label = col_name.replace("_", " ").title()
|
|
434
|
+
display_fn = None
|
|
435
|
+
options = None
|
|
436
|
+
|
|
437
|
+
# Check @column decorator first
|
|
438
|
+
if col_name in decorated_columns:
|
|
439
|
+
options = decorated_columns[col_name]
|
|
440
|
+
display_fn = getattr(registered.admin, col_name)
|
|
441
|
+
label = options.header or label
|
|
442
|
+
# Check display_functions dict fallback
|
|
443
|
+
elif (
|
|
444
|
+
registered.admin.display_functions
|
|
445
|
+
and col_name in registered.admin.display_functions
|
|
446
|
+
):
|
|
447
|
+
display_fn = registered.admin.display_functions[col_name]
|
|
448
|
+
|
|
449
|
+
display_columns.append(
|
|
450
|
+
DisplayColumn(col_name, label, col_name in rel_names, display_fn, options)
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
filter_fields: dict[str, dict[str, Any]] = {}
|
|
454
|
+
if registered.admin.list_filter:
|
|
455
|
+
for filter_field in registered.admin.list_filter:
|
|
456
|
+
filter_fields[filter_field] = await self._get_filter_choices(
|
|
457
|
+
model, filter_field, session
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
ordering = request.query_params.get("ordering", "")
|
|
461
|
+
if not ordering and registered.admin.ordering:
|
|
462
|
+
ordering = registered.admin.ordering[0]
|
|
463
|
+
|
|
464
|
+
template_context = {
|
|
465
|
+
"model": registered,
|
|
466
|
+
"registered": registered,
|
|
467
|
+
"display_columns": display_columns,
|
|
468
|
+
"items": items,
|
|
469
|
+
"search_query": q,
|
|
470
|
+
"page": page,
|
|
471
|
+
"total_pages": total_pages,
|
|
472
|
+
"total": total,
|
|
473
|
+
"per_page": per_page,
|
|
474
|
+
"next_cursor": pagination_result.next_cursor,
|
|
475
|
+
"has_next": pagination_result.has_next,
|
|
476
|
+
"pagination_mode": pagination_result.mode,
|
|
477
|
+
"filter_fields": filter_fields,
|
|
478
|
+
"active_filters": active_filters,
|
|
479
|
+
"ordering": ordering,
|
|
480
|
+
"permissions": permission_checker.permission_set(
|
|
481
|
+
registered.table_name
|
|
482
|
+
)
|
|
483
|
+
if permission_checker
|
|
484
|
+
else PermissionSet(
|
|
485
|
+
can_view=True, can_create=True, can_edit=True, can_delete=True
|
|
486
|
+
),
|
|
487
|
+
"list_actions": registered.admin.get_list_actions(),
|
|
488
|
+
"row_actions": registered.admin.get_row_actions(),
|
|
489
|
+
"list_tabs": getattr(registered.admin, "list_tabs", []),
|
|
490
|
+
"list_sections": getattr(registered.admin, "list_sections", []),
|
|
491
|
+
"ordering_field": getattr(registered.admin, "ordering_field", None),
|
|
492
|
+
"hide_ordering_field": getattr(
|
|
493
|
+
registered.admin, "hide_ordering_field", False
|
|
494
|
+
),
|
|
495
|
+
"list_filter_options": getattr(
|
|
496
|
+
registered.admin, "list_filter_options", {}
|
|
497
|
+
),
|
|
498
|
+
"list_filter_horizontal": getattr(
|
|
499
|
+
registered.admin, "list_filter_horizontal", False
|
|
500
|
+
),
|
|
501
|
+
}
|
|
502
|
+
await inject_sidebar_context(request, template_context)
|
|
503
|
+
return template_context
|
|
504
|
+
|
|
505
|
+
async def build_form_context(
|
|
506
|
+
self,
|
|
507
|
+
registered: RegisteredModel,
|
|
508
|
+
request: Request,
|
|
509
|
+
obj: Any | None = None,
|
|
510
|
+
values: dict[str, Any] | None = None,
|
|
511
|
+
errors: dict[str, list[str]] | None = None,
|
|
512
|
+
is_create: bool = False,
|
|
513
|
+
permission_checker: Any = None,
|
|
514
|
+
rel_labels: dict[str, str] | None = None,
|
|
515
|
+
) -> dict[str, Any]:
|
|
516
|
+
"""Build the template context for a form view.
|
|
517
|
+
|
|
518
|
+
Returns a dict suitable for passing to TemplateResponse.
|
|
519
|
+
"""
|
|
520
|
+
from fastapi_admin_kit.form.pipeline import (
|
|
521
|
+
build_form_context as _build_form_ctx,
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
ctx = _build_form_ctx(
|
|
525
|
+
registered,
|
|
526
|
+
obj=obj,
|
|
527
|
+
values=values,
|
|
528
|
+
errors=errors,
|
|
529
|
+
request=request,
|
|
530
|
+
is_create=is_create,
|
|
531
|
+
rel_labels=rel_labels,
|
|
532
|
+
)
|
|
533
|
+
template_context = {
|
|
534
|
+
"form_context": ctx,
|
|
535
|
+
"registered": registered,
|
|
536
|
+
"obj": ctx.obj,
|
|
537
|
+
"form_fields": ctx.fieldsets[0].fields if ctx.fieldsets else [],
|
|
538
|
+
"fieldsets": ctx.fieldsets,
|
|
539
|
+
"errors": ctx.errors,
|
|
540
|
+
"is_create": is_create,
|
|
541
|
+
"permissions": permission_checker.permission_set(
|
|
542
|
+
registered.table_name
|
|
543
|
+
)
|
|
544
|
+
if permission_checker
|
|
545
|
+
else PermissionSet(
|
|
546
|
+
can_view=True, can_create=True, can_edit=True, can_delete=True
|
|
547
|
+
),
|
|
548
|
+
"detail_actions": registered.admin.get_detail_actions(),
|
|
549
|
+
"submit_line_actions": registered.admin.get_submit_line_actions(),
|
|
550
|
+
"conditional_fields": getattr(
|
|
551
|
+
registered.admin, "conditional_fields", {}
|
|
552
|
+
),
|
|
553
|
+
"warn_unsaved_form": getattr(
|
|
554
|
+
registered.admin, "warn_unsaved_form", True
|
|
555
|
+
),
|
|
556
|
+
"compressed_fields": getattr(
|
|
557
|
+
registered.admin, "compressed_fields", True
|
|
558
|
+
),
|
|
559
|
+
"change_form_show_cancel_button": getattr(
|
|
560
|
+
registered.admin, "change_form_show_cancel_button", True
|
|
561
|
+
),
|
|
562
|
+
}
|
|
563
|
+
await inject_sidebar_context(request, template_context)
|
|
564
|
+
return template_context
|
|
565
|
+
|
|
566
|
+
async def build_delete_context(
|
|
567
|
+
self,
|
|
568
|
+
registered: RegisteredModel,
|
|
569
|
+
request: Request,
|
|
570
|
+
permission_checker: Any = None,
|
|
571
|
+
) -> dict[str, Any]:
|
|
572
|
+
"""Build the template context for a delete confirmation view.
|
|
573
|
+
|
|
574
|
+
Returns a dict suitable for passing to TemplateResponse.
|
|
575
|
+
"""
|
|
576
|
+
template_context = {
|
|
577
|
+
"model": registered,
|
|
578
|
+
"registered": registered,
|
|
579
|
+
"permissions": permission_checker.permission_set(
|
|
580
|
+
registered.table_name
|
|
581
|
+
)
|
|
582
|
+
if permission_checker
|
|
583
|
+
else PermissionSet(
|
|
584
|
+
can_view=True, can_create=True, can_edit=True, can_delete=True
|
|
585
|
+
),
|
|
586
|
+
}
|
|
587
|
+
await inject_sidebar_context(request, template_context)
|
|
588
|
+
return template_context
|