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,335 @@
|
|
|
1
|
+
"""Auto-route generation per registered model with RBAC."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
6
|
+
|
|
7
|
+
from fastapi_admin_kit.auth.csrf import require_csrf_token
|
|
8
|
+
from fastapi_admin_kit.auth.dependencies import require_permission
|
|
9
|
+
from fastapi_admin_kit.db import get_db_session
|
|
10
|
+
from fastapi_admin_kit.registry import RegisteredModel
|
|
11
|
+
from fastapi_admin_kit.views.class_views import (
|
|
12
|
+
BulkView,
|
|
13
|
+
CreateView,
|
|
14
|
+
DeleteView,
|
|
15
|
+
EditView,
|
|
16
|
+
ListView,
|
|
17
|
+
SearchView,
|
|
18
|
+
_resolve_view_class,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_model_router(registered: RegisteredModel) -> APIRouter:
|
|
23
|
+
router = APIRouter(prefix=f"/{registered.table_name}")
|
|
24
|
+
|
|
25
|
+
# DIP: view classes resolved from ModelAdmin config with defaults
|
|
26
|
+
admin = registered.admin
|
|
27
|
+
list_v = _resolve_view_class(admin, "list_view_class", ListView)(registered)
|
|
28
|
+
create_v = _resolve_view_class(admin, "create_view_class", CreateView)(registered)
|
|
29
|
+
edit_v = _resolve_view_class(admin, "edit_view_class", EditView)(registered)
|
|
30
|
+
delete_v = _resolve_view_class(admin, "delete_view_class", DeleteView)(registered)
|
|
31
|
+
bulk_v = _resolve_view_class(admin, "bulk_view_class", BulkView)(registered)
|
|
32
|
+
search_v = _resolve_view_class(admin, "search_view_class", SearchView)(registered)
|
|
33
|
+
|
|
34
|
+
router.add_api_route(
|
|
35
|
+
"/",
|
|
36
|
+
list_v.html_response,
|
|
37
|
+
methods=["GET"],
|
|
38
|
+
dependencies=[Depends(require_permission(registered.table_name, "view"))],
|
|
39
|
+
)
|
|
40
|
+
router.add_api_route(
|
|
41
|
+
"/create",
|
|
42
|
+
create_v.html_response,
|
|
43
|
+
methods=["GET"],
|
|
44
|
+
dependencies=[Depends(require_permission(registered.table_name, "create"))],
|
|
45
|
+
)
|
|
46
|
+
router.add_api_route(
|
|
47
|
+
"/create",
|
|
48
|
+
create_v.html_response,
|
|
49
|
+
methods=["POST"],
|
|
50
|
+
dependencies=[
|
|
51
|
+
Depends(require_permission(registered.table_name, "create")),
|
|
52
|
+
Depends(require_csrf_token),
|
|
53
|
+
],
|
|
54
|
+
)
|
|
55
|
+
router.add_api_route(
|
|
56
|
+
"/search",
|
|
57
|
+
search_v.html_response,
|
|
58
|
+
methods=["GET"],
|
|
59
|
+
dependencies=[Depends(require_permission(registered.table_name, "view"))],
|
|
60
|
+
)
|
|
61
|
+
router.add_api_route(
|
|
62
|
+
"/bulk",
|
|
63
|
+
bulk_v.html_response,
|
|
64
|
+
methods=["POST"],
|
|
65
|
+
dependencies=[
|
|
66
|
+
Depends(require_permission(registered.table_name, "edit")),
|
|
67
|
+
Depends(require_csrf_token),
|
|
68
|
+
],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
@router.post("/validate-field")
|
|
72
|
+
async def validate_field_endpoint(
|
|
73
|
+
request: Request,
|
|
74
|
+
_csrf: bool = Depends(require_csrf_token),
|
|
75
|
+
_: None = Depends(require_permission(registered.table_name, "view")),
|
|
76
|
+
):
|
|
77
|
+
templates = request.app.state.admin_jinja_env
|
|
78
|
+
form = await request.form()
|
|
79
|
+
field_name = form.get("field_name")
|
|
80
|
+
raw_value = form.get(field_name)
|
|
81
|
+
|
|
82
|
+
field_meta = next(
|
|
83
|
+
(f for f in registered.form_fields if f.name == field_name), None
|
|
84
|
+
)
|
|
85
|
+
if field_meta is None:
|
|
86
|
+
raise HTTPException(status_code=422, detail="Field not found")
|
|
87
|
+
|
|
88
|
+
widget = registered.get_widget(field_name)
|
|
89
|
+
parsed = widget.parse(raw_value)
|
|
90
|
+
errors = widget.validate(parsed, field_meta)
|
|
91
|
+
|
|
92
|
+
validator_fn = getattr(registered.admin, f"validate_{field_name}", None)
|
|
93
|
+
if validator_fn and not errors:
|
|
94
|
+
err = validator_fn(parsed, obj=None)
|
|
95
|
+
if err:
|
|
96
|
+
errors = [err]
|
|
97
|
+
|
|
98
|
+
from fastapi_admin_kit.types import FieldRenderContext
|
|
99
|
+
|
|
100
|
+
field_ctx = FieldRenderContext(
|
|
101
|
+
meta=field_meta,
|
|
102
|
+
widget_macro=widget.macro_name,
|
|
103
|
+
widget_context=widget.render_context(field_meta, raw_value),
|
|
104
|
+
errors=errors,
|
|
105
|
+
)
|
|
106
|
+
return templates.TemplateResponse(request, "partials/field_wrapper.html", {
|
|
107
|
+
"field_ctx": field_ctx,
|
|
108
|
+
"model_name": registered.table_name,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
router.add_api_route(
|
|
112
|
+
"/{id}",
|
|
113
|
+
edit_v.html_response,
|
|
114
|
+
methods=["GET"],
|
|
115
|
+
dependencies=[Depends(require_permission(registered.table_name, "edit"))],
|
|
116
|
+
)
|
|
117
|
+
router.add_api_route(
|
|
118
|
+
"/{id}",
|
|
119
|
+
edit_v.html_response,
|
|
120
|
+
methods=["POST"],
|
|
121
|
+
dependencies=[
|
|
122
|
+
Depends(require_permission(registered.table_name, "edit")),
|
|
123
|
+
Depends(require_csrf_token),
|
|
124
|
+
],
|
|
125
|
+
)
|
|
126
|
+
router.add_api_route(
|
|
127
|
+
"/{id}/delete",
|
|
128
|
+
delete_v.html_response,
|
|
129
|
+
methods=["POST"],
|
|
130
|
+
dependencies=[
|
|
131
|
+
Depends(require_permission(registered.table_name, "delete")),
|
|
132
|
+
Depends(require_csrf_token),
|
|
133
|
+
],
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# ── Custom Actions ─────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
@router.post("/action/{action_name}")
|
|
139
|
+
async def execute_list_action(
|
|
140
|
+
request: Request,
|
|
141
|
+
action_name: str,
|
|
142
|
+
_csrf: bool = Depends(require_csrf_token),
|
|
143
|
+
):
|
|
144
|
+
"""Execute a list-level action on selected objects."""
|
|
145
|
+
session = get_db_session(request)
|
|
146
|
+
form = await request.form()
|
|
147
|
+
ids = form.getlist("ids[]")
|
|
148
|
+
|
|
149
|
+
action_obj = None
|
|
150
|
+
for a in registered.admin.get_list_actions():
|
|
151
|
+
if a.name == action_name:
|
|
152
|
+
action_obj = a
|
|
153
|
+
break
|
|
154
|
+
|
|
155
|
+
if not action_obj:
|
|
156
|
+
raise HTTPException(status_code=404, detail=f"Unknown action: {action_name}")
|
|
157
|
+
|
|
158
|
+
objects = []
|
|
159
|
+
for pid in ids:
|
|
160
|
+
obj = await session.get(registered.model, pid)
|
|
161
|
+
if obj:
|
|
162
|
+
objects.append(obj)
|
|
163
|
+
|
|
164
|
+
if objects:
|
|
165
|
+
await action_obj.execute(objects, request)
|
|
166
|
+
await session.flush()
|
|
167
|
+
|
|
168
|
+
from fastapi.responses import HTMLResponse
|
|
169
|
+
return HTMLResponse(content="OK")
|
|
170
|
+
|
|
171
|
+
@router.post("/action/{action_name}/{id}")
|
|
172
|
+
async def execute_row_action(
|
|
173
|
+
request: Request,
|
|
174
|
+
action_name: str,
|
|
175
|
+
id: str,
|
|
176
|
+
_csrf: bool = Depends(require_csrf_token),
|
|
177
|
+
):
|
|
178
|
+
"""Execute a row-level action on a single object."""
|
|
179
|
+
session = get_db_session(request)
|
|
180
|
+
|
|
181
|
+
action_obj = None
|
|
182
|
+
for a in registered.admin.get_row_actions():
|
|
183
|
+
if a.name == action_name:
|
|
184
|
+
action_obj = a
|
|
185
|
+
break
|
|
186
|
+
|
|
187
|
+
if not action_obj:
|
|
188
|
+
raise HTTPException(status_code=404, detail=f"Unknown action: {action_name}")
|
|
189
|
+
|
|
190
|
+
obj = await session.get(registered.model, int(id))
|
|
191
|
+
if not obj:
|
|
192
|
+
raise HTTPException(status_code=404, detail="Not found")
|
|
193
|
+
|
|
194
|
+
await action_obj.execute([obj], request)
|
|
195
|
+
await session.flush()
|
|
196
|
+
|
|
197
|
+
from fastapi.responses import HTMLResponse
|
|
198
|
+
return HTMLResponse(content="OK")
|
|
199
|
+
|
|
200
|
+
# ── Sortable Endpoint ──────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
@router.post("/sort")
|
|
203
|
+
async def sort_items(
|
|
204
|
+
request: Request,
|
|
205
|
+
_csrf: bool = Depends(require_csrf_token),
|
|
206
|
+
):
|
|
207
|
+
"""Handle drag-drop sort updates."""
|
|
208
|
+
body = await request.json()
|
|
209
|
+
ordering_field = getattr(registered.admin, "ordering_field", None)
|
|
210
|
+
if not ordering_field:
|
|
211
|
+
raise HTTPException(status_code=400, detail="Sorting not configured")
|
|
212
|
+
|
|
213
|
+
session = get_db_session(request)
|
|
214
|
+
items = body.get("items", [])
|
|
215
|
+
for idx, item_id in enumerate(items):
|
|
216
|
+
obj = await session.get(registered.model, item_id)
|
|
217
|
+
if obj:
|
|
218
|
+
setattr(obj, ordering_field, idx)
|
|
219
|
+
await session.flush()
|
|
220
|
+
|
|
221
|
+
from fastapi.responses import HTMLResponse
|
|
222
|
+
return HTMLResponse(content="OK")
|
|
223
|
+
|
|
224
|
+
# ── Autocomplete Endpoint ──────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
@router.get("/autocomplete/")
|
|
227
|
+
async def autocomplete(
|
|
228
|
+
request: Request,
|
|
229
|
+
q: str = "",
|
|
230
|
+
_csrf: bool = Depends(require_permission(registered.table_name, "view")),
|
|
231
|
+
):
|
|
232
|
+
"""Search-as-you-type endpoint for relation pickers."""
|
|
233
|
+
from fastapi.responses import JSONResponse
|
|
234
|
+
|
|
235
|
+
session = get_db_session(request)
|
|
236
|
+
model = registered.model
|
|
237
|
+
results = []
|
|
238
|
+
|
|
239
|
+
search_fields = getattr(registered.admin, "search_fields", None) or ["name", "title"]
|
|
240
|
+
clauses = []
|
|
241
|
+
for sf in search_fields:
|
|
242
|
+
if hasattr(model, sf):
|
|
243
|
+
col = getattr(model, sf)
|
|
244
|
+
if hasattr(col, "ilike"):
|
|
245
|
+
clauses.append(col.ilike(f"%{q}%"))
|
|
246
|
+
|
|
247
|
+
if clauses:
|
|
248
|
+
from sqlalchemy import or_, select
|
|
249
|
+
query = select(model).where(or_(*clauses)).limit(20)
|
|
250
|
+
result = await session.execute(query)
|
|
251
|
+
for obj in result.scalars():
|
|
252
|
+
label = str(
|
|
253
|
+
getattr(obj, "name", None)
|
|
254
|
+
or getattr(obj, "title", None)
|
|
255
|
+
or f"#{getattr(obj, 'id', '?')}"
|
|
256
|
+
)
|
|
257
|
+
results.append({"id": str(obj.id), "label": label})
|
|
258
|
+
|
|
259
|
+
return JSONResponse(content=results)
|
|
260
|
+
|
|
261
|
+
# ── Inline Field Update (existing) ─────────────────────────────
|
|
262
|
+
@router.patch("/{id}/field")
|
|
263
|
+
async def update_field(
|
|
264
|
+
request: Request,
|
|
265
|
+
id: str,
|
|
266
|
+
_csrf: bool = Depends(require_csrf_token),
|
|
267
|
+
):
|
|
268
|
+
"""Inline field update — used by toggle switches in list view."""
|
|
269
|
+
from fastapi_admin_kit.auth.csrf import _get_secret_key, generate_csrf_token
|
|
270
|
+
|
|
271
|
+
content_type = request.headers.get("content-type", "")
|
|
272
|
+
if "application/json" in content_type:
|
|
273
|
+
body = await request.json()
|
|
274
|
+
else:
|
|
275
|
+
form = await request.form()
|
|
276
|
+
body = dict(form)
|
|
277
|
+
|
|
278
|
+
field_name = body.get("field")
|
|
279
|
+
new_value = body.get("value")
|
|
280
|
+
|
|
281
|
+
if not field_name:
|
|
282
|
+
raise HTTPException(status_code=400, detail="Missing field name")
|
|
283
|
+
|
|
284
|
+
readonly = getattr(registered.admin, "readonly_fields", []) or []
|
|
285
|
+
if field_name in readonly:
|
|
286
|
+
raise HTTPException(status_code=403, detail="Field is read-only")
|
|
287
|
+
|
|
288
|
+
col = next((c for c in registered.columns if c.name == field_name), None)
|
|
289
|
+
if col is None:
|
|
290
|
+
raise HTTPException(status_code=400, detail="Invalid field name")
|
|
291
|
+
|
|
292
|
+
if col.type.__class__.__name__ == "Boolean":
|
|
293
|
+
new_value = str(new_value).lower() in ("true", "1", "yes")
|
|
294
|
+
|
|
295
|
+
session = get_db_session(request)
|
|
296
|
+
try:
|
|
297
|
+
obj = await session.get(registered.model, int(id))
|
|
298
|
+
if obj is None:
|
|
299
|
+
raise HTTPException(status_code=404, detail="Object not found")
|
|
300
|
+
|
|
301
|
+
setattr(obj, field_name, new_value)
|
|
302
|
+
await session.flush()
|
|
303
|
+
except Exception:
|
|
304
|
+
await session.rollback()
|
|
305
|
+
raise
|
|
306
|
+
|
|
307
|
+
val = getattr(obj, field_name)
|
|
308
|
+
secret = _get_secret_key(request) or ""
|
|
309
|
+
csrf_token = generate_csrf_token(secret)
|
|
310
|
+
admin_path = request.app.state.admin_config["admin_path"]
|
|
311
|
+
|
|
312
|
+
if val:
|
|
313
|
+
toggle_html = (
|
|
314
|
+
f'<button class="toggle-switch toggle-switch--on"'
|
|
315
|
+
f' hx-patch="{admin_path}/{registered.table_name}/{obj.id}/field"'
|
|
316
|
+
f' hx-vals=\'{{"field": "{field_name}", "value": "false"}}\''
|
|
317
|
+
f' hx-target="this" hx-swap="outerHTML"'
|
|
318
|
+
f' hx-headers=\'{{"X-CSRF-Token": "{csrf_token}"}}\''
|
|
319
|
+
f' role="checkbox" aria-checked="true">'
|
|
320
|
+
f'<span class="toggle-switch__thumb"></span></button>'
|
|
321
|
+
)
|
|
322
|
+
else:
|
|
323
|
+
toggle_html = (
|
|
324
|
+
f'<button class="toggle-switch"'
|
|
325
|
+
f' hx-patch="{admin_path}/{registered.table_name}/{obj.id}/field"'
|
|
326
|
+
f' hx-vals=\'{{"field": "{field_name}", "value": "true"}}\''
|
|
327
|
+
f' hx-target="this" hx-swap="outerHTML"'
|
|
328
|
+
f' hx-headers=\'{{"X-CSRF-Token": "{csrf_token}"}}\''
|
|
329
|
+
f' role="checkbox" aria-checked="false">'
|
|
330
|
+
f'<span class="toggle-switch__thumb"></span></button>'
|
|
331
|
+
)
|
|
332
|
+
from fastapi.responses import HTMLResponse
|
|
333
|
+
return HTMLResponse(content=toggle_html)
|
|
334
|
+
|
|
335
|
+
return router
|