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,347 @@
|
|
|
1
|
+
"""Admin user management views — list, create, edit, delete."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
6
|
+
from fastapi.responses import JSONResponse
|
|
7
|
+
from sqlalchemy import or_, select
|
|
8
|
+
|
|
9
|
+
from fastapi_admin_kit.auth.dependencies import get_current_admin_user
|
|
10
|
+
from fastapi_admin_kit.auth.models import AdminRole
|
|
11
|
+
from fastapi_admin_kit.auth.protocol import AdminUserProtocol
|
|
12
|
+
from fastapi_admin_kit.db import get_db_session
|
|
13
|
+
|
|
14
|
+
router = APIRouter()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def _require_superuser(
|
|
18
|
+
user: AdminUserProtocol = Depends(get_current_admin_user),
|
|
19
|
+
) -> AdminUserProtocol:
|
|
20
|
+
if not getattr(user, "is_superuser", False):
|
|
21
|
+
raise HTTPException(
|
|
22
|
+
status_code=403, detail="Superuser access required."
|
|
23
|
+
)
|
|
24
|
+
return user
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@router.get("/users/roles/search")
|
|
28
|
+
async def roles_search(
|
|
29
|
+
request: Request,
|
|
30
|
+
q: str = Query("", description="Search query"),
|
|
31
|
+
ids: str = Query("", description="Comma-separated role IDs to load"),
|
|
32
|
+
_: AdminUserProtocol = Depends(_require_superuser),
|
|
33
|
+
):
|
|
34
|
+
"""Search roles for the multi-relation picker. Supports ?q= and ?ids=."""
|
|
35
|
+
session = get_db_session(request)
|
|
36
|
+
|
|
37
|
+
if ids:
|
|
38
|
+
id_list = [
|
|
39
|
+
int(i.strip()) for i in ids.split(",") if i.strip().isdigit()
|
|
40
|
+
]
|
|
41
|
+
result = await session.execute(
|
|
42
|
+
select(AdminRole).where(AdminRole.id.in_(id_list))
|
|
43
|
+
)
|
|
44
|
+
elif q:
|
|
45
|
+
result = await session.execute(
|
|
46
|
+
select(AdminRole)
|
|
47
|
+
.where(
|
|
48
|
+
or_(
|
|
49
|
+
AdminRole.name.ilike(f"%{q}%"),
|
|
50
|
+
AdminRole.description.ilike(f"%{q}%"),
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
.limit(20)
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
result = await session.execute(
|
|
57
|
+
select(AdminRole).order_by(AdminRole.name).limit(20)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
roles = result.scalars().all()
|
|
61
|
+
return JSONResponse(content=[{"id": r.id, "label": r.name} for r in roles])
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# @router.get("/users", response_class=HTMLResponse)
|
|
65
|
+
# async def user_list_view(
|
|
66
|
+
# request: Request,
|
|
67
|
+
# _: AdminUserProtocol = Depends(_require_superuser),
|
|
68
|
+
# ):
|
|
69
|
+
# """List admin users (superuser only)."""
|
|
70
|
+
# templates = request.app.state.admin_jinja_env
|
|
71
|
+
# session = get_db_session(request)
|
|
72
|
+
|
|
73
|
+
# result = await session.execute(
|
|
74
|
+
# select(AdminUser).options(selectinload(AdminUser.roles)).order_by(AdminUser.id)
|
|
75
|
+
# )
|
|
76
|
+
# users = list(result.scalars().all())
|
|
77
|
+
|
|
78
|
+
# return templates.TemplateResponse(
|
|
79
|
+
# request,
|
|
80
|
+
# "pages/users/list.html",
|
|
81
|
+
# await inject_sidebar_context(
|
|
82
|
+
# request,
|
|
83
|
+
# {
|
|
84
|
+
# "users": users,
|
|
85
|
+
# },
|
|
86
|
+
# ),
|
|
87
|
+
# )
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# @router.get("/users/create", response_class=HTMLResponse)
|
|
91
|
+
# async def user_create_view(
|
|
92
|
+
# request: Request,
|
|
93
|
+
# _: AdminUserProtocol = Depends(_require_superuser),
|
|
94
|
+
# ):
|
|
95
|
+
# """Show user create form."""
|
|
96
|
+
# templates = request.app.state.admin_jinja_env
|
|
97
|
+
# session = get_db_session(request)
|
|
98
|
+
|
|
99
|
+
# result = await session.execute(select(AdminRole).order_by(AdminRole.name))
|
|
100
|
+
# roles = list(result.scalars().all())
|
|
101
|
+
|
|
102
|
+
# return templates.TemplateResponse(
|
|
103
|
+
# request,
|
|
104
|
+
# "pages/users/form.html",
|
|
105
|
+
# await inject_sidebar_context(
|
|
106
|
+
# request,
|
|
107
|
+
# {
|
|
108
|
+
# "user": None,
|
|
109
|
+
# "roles": roles,
|
|
110
|
+
# },
|
|
111
|
+
# ),
|
|
112
|
+
# )
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# @router.post("/users/create")
|
|
116
|
+
# async def user_create_post(
|
|
117
|
+
# request: Request,
|
|
118
|
+
# _: AdminUserProtocol = Depends(_require_superuser),
|
|
119
|
+
# _csrf: bool = Depends(require_csrf_token),
|
|
120
|
+
# ):
|
|
121
|
+
# """Handle user creation."""
|
|
122
|
+
# from fastapi_admin_kit.auth.backend import pwd_context
|
|
123
|
+
# from fastapi_admin_kit.auth.password import validate_password_strength
|
|
124
|
+
|
|
125
|
+
# session = get_db_session(request)
|
|
126
|
+
# form = await request.form()
|
|
127
|
+
|
|
128
|
+
# email = form.get("email", "").strip()
|
|
129
|
+
# password = form.get("password", "")
|
|
130
|
+
# full_name = form.get("full_name", "").strip()
|
|
131
|
+
# is_superuser = form.get("is_superuser") == "on"
|
|
132
|
+
|
|
133
|
+
# if not email:
|
|
134
|
+
# raise HTTPException(status_code=400, detail="Email is required.")
|
|
135
|
+
|
|
136
|
+
# existing = await session.execute(
|
|
137
|
+
# select(AdminUser).where(AdminUser.email == email)
|
|
138
|
+
# )
|
|
139
|
+
# if existing.scalar_one_or_none():
|
|
140
|
+
# raise HTTPException(status_code=400, detail="Email already exists.")
|
|
141
|
+
|
|
142
|
+
# password_errors = validate_password_strength(password)
|
|
143
|
+
# if password_errors:
|
|
144
|
+
# templates = request.app.state.admin_jinja_env
|
|
145
|
+
# result = await session.execute(
|
|
146
|
+
# select(AdminRole).order_by(AdminRole.name)
|
|
147
|
+
# )
|
|
148
|
+
# roles = list(result.scalars().all())
|
|
149
|
+
# return templates.TemplateResponse(
|
|
150
|
+
# request,
|
|
151
|
+
# "pages/users/form.html",
|
|
152
|
+
# await inject_sidebar_context(
|
|
153
|
+
# request,
|
|
154
|
+
# {
|
|
155
|
+
# "user": None,
|
|
156
|
+
# "roles": roles,
|
|
157
|
+
# "error": password_errors[0],
|
|
158
|
+
# },
|
|
159
|
+
# ),
|
|
160
|
+
# )
|
|
161
|
+
|
|
162
|
+
# user = AdminUser(
|
|
163
|
+
# email=email,
|
|
164
|
+
# hashed_password=pwd_context.hash(password),
|
|
165
|
+
# full_name=full_name,
|
|
166
|
+
# is_superuser=is_superuser,
|
|
167
|
+
# )
|
|
168
|
+
# session.add(user)
|
|
169
|
+
# await session.flush()
|
|
170
|
+
|
|
171
|
+
# # Assign selected roles (from multiRelation JSON string or multi-select)
|
|
172
|
+
# import json as _json
|
|
173
|
+
|
|
174
|
+
# raw_role_ids = form.get("role_ids", "")
|
|
175
|
+
# if isinstance(raw_role_ids, list):
|
|
176
|
+
# raw_role_ids = raw_role_ids[0] if raw_role_ids else ""
|
|
177
|
+
# if raw_role_ids:
|
|
178
|
+
# try:
|
|
179
|
+
# parsed = _json.loads(raw_role_ids)
|
|
180
|
+
# if isinstance(parsed, list):
|
|
181
|
+
# role_id_strs = [str(v) for v in parsed]
|
|
182
|
+
# else:
|
|
183
|
+
# role_id_strs = [str(parsed)]
|
|
184
|
+
# except (ValueError, TypeError):
|
|
185
|
+
# role_id_strs = [raw_role_ids]
|
|
186
|
+
# for rid in role_id_strs:
|
|
187
|
+
# try:
|
|
188
|
+
# role = await session.get(AdminRole, int(rid))
|
|
189
|
+
# if role:
|
|
190
|
+
# user.roles.append(role)
|
|
191
|
+
# except (ValueError, TypeError):
|
|
192
|
+
# pass
|
|
193
|
+
|
|
194
|
+
# await session.commit()
|
|
195
|
+
|
|
196
|
+
# return RedirectResponse(url="/admin/users", status_code=302)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# @router.get("/users/{user_id}", response_class=HTMLResponse)
|
|
200
|
+
# async def user_edit_view(
|
|
201
|
+
# request: Request,
|
|
202
|
+
# user_id: int,
|
|
203
|
+
# _: AdminUserProtocol = Depends(_require_superuser),
|
|
204
|
+
# ):
|
|
205
|
+
# """Show user edit form."""
|
|
206
|
+
# templates = request.app.state.admin_jinja_env
|
|
207
|
+
# session = get_db_session(request)
|
|
208
|
+
|
|
209
|
+
# result = await session.execute(
|
|
210
|
+
# select(AdminUser)
|
|
211
|
+
# .options(selectinload(AdminUser.roles))
|
|
212
|
+
# .where(AdminUser.id == user_id)
|
|
213
|
+
# )
|
|
214
|
+
# user = result.scalar_one_or_none()
|
|
215
|
+
# if user is None:
|
|
216
|
+
# raise HTTPException(status_code=404, detail="User not found.")
|
|
217
|
+
|
|
218
|
+
# result = await session.execute(select(AdminRole).order_by(AdminRole.name))
|
|
219
|
+
# roles = list(result.scalars().all())
|
|
220
|
+
|
|
221
|
+
# return templates.TemplateResponse(
|
|
222
|
+
# request,
|
|
223
|
+
# "pages/users/form.html",
|
|
224
|
+
# await inject_sidebar_context(
|
|
225
|
+
# request,
|
|
226
|
+
# {
|
|
227
|
+
# "user": user,
|
|
228
|
+
# "roles": roles,
|
|
229
|
+
# },
|
|
230
|
+
# ),
|
|
231
|
+
# )
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# @router.post("/users/{user_id}")
|
|
235
|
+
# async def user_edit_post(
|
|
236
|
+
# request: Request,
|
|
237
|
+
# user_id: int,
|
|
238
|
+
# current_user: AdminUserProtocol = Depends(_require_superuser),
|
|
239
|
+
# _csrf: bool = Depends(require_csrf_token),
|
|
240
|
+
# ):
|
|
241
|
+
# """Handle user edit."""
|
|
242
|
+
# from fastapi_admin_kit.auth.backend import pwd_context
|
|
243
|
+
# from fastapi_admin_kit.auth.password import validate_password_strength
|
|
244
|
+
|
|
245
|
+
# session = get_db_session(request)
|
|
246
|
+
# user = await session.get(AdminUser, user_id)
|
|
247
|
+
# if user is None:
|
|
248
|
+
# raise HTTPException(status_code=404, detail="User not found.")
|
|
249
|
+
|
|
250
|
+
# form = await request.form()
|
|
251
|
+
# email = form.get("email", "").strip()
|
|
252
|
+
# password = form.get("password", "")
|
|
253
|
+
# full_name = form.get("full_name", "").strip()
|
|
254
|
+
# is_superuser = form.get("is_superuser") == "on"
|
|
255
|
+
# is_active = form.get("is_active") != "off"
|
|
256
|
+
|
|
257
|
+
# if email:
|
|
258
|
+
# user.email = email
|
|
259
|
+
# user.full_name = full_name
|
|
260
|
+
|
|
261
|
+
# # Prevent superuser from deactivating themselves
|
|
262
|
+
# if user.id == current_user.id and not is_active:
|
|
263
|
+
# raise HTTPException(
|
|
264
|
+
# status_code=400, detail="Cannot deactivate your own account."
|
|
265
|
+
# )
|
|
266
|
+
|
|
267
|
+
# if user.id == current_user.id and not is_superuser:
|
|
268
|
+
# raise HTTPException(
|
|
269
|
+
# status_code=400,
|
|
270
|
+
# detail="Cannot remove superuser from your own account.",
|
|
271
|
+
# )
|
|
272
|
+
|
|
273
|
+
# user.is_superuser = is_superuser
|
|
274
|
+
# user.is_active = is_active
|
|
275
|
+
|
|
276
|
+
# if password:
|
|
277
|
+
# password_errors = validate_password_strength(password)
|
|
278
|
+
# if password_errors:
|
|
279
|
+
# templates = request.app.state.admin_jinja_env
|
|
280
|
+
# result = await session.execute(
|
|
281
|
+
# select(AdminRole).order_by(AdminRole.name)
|
|
282
|
+
# )
|
|
283
|
+
# roles = list(result.scalars().all())
|
|
284
|
+
# return templates.TemplateResponse(
|
|
285
|
+
# request,
|
|
286
|
+
# "pages/users/form.html",
|
|
287
|
+
# await inject_sidebar_context(
|
|
288
|
+
# request,
|
|
289
|
+
# {
|
|
290
|
+
# "user": user,
|
|
291
|
+
# "roles": roles,
|
|
292
|
+
# "error": password_errors[0],
|
|
293
|
+
# },
|
|
294
|
+
# ),
|
|
295
|
+
# )
|
|
296
|
+
# user.hashed_password = pwd_context.hash(password)
|
|
297
|
+
|
|
298
|
+
# # Update roles: clear existing, add selected
|
|
299
|
+
# import json as _json
|
|
300
|
+
|
|
301
|
+
# user.roles.clear()
|
|
302
|
+
# raw_role_ids = form.get("role_ids", "")
|
|
303
|
+
# if isinstance(raw_role_ids, list):
|
|
304
|
+
# raw_role_ids = raw_role_ids[0] if raw_role_ids else ""
|
|
305
|
+
# if raw_role_ids:
|
|
306
|
+
# try:
|
|
307
|
+
# parsed = _json.loads(raw_role_ids)
|
|
308
|
+
# if isinstance(parsed, list):
|
|
309
|
+
# role_id_strs = [str(v) for v in parsed]
|
|
310
|
+
# else:
|
|
311
|
+
# role_id_strs = [str(parsed)]
|
|
312
|
+
# except (ValueError, TypeError):
|
|
313
|
+
# role_id_strs = [raw_role_ids]
|
|
314
|
+
# for rid in role_id_strs:
|
|
315
|
+
# try:
|
|
316
|
+
# role = await session.get(AdminRole, int(rid))
|
|
317
|
+
# if role:
|
|
318
|
+
# user.roles.append(role)
|
|
319
|
+
# except (ValueError, TypeError):
|
|
320
|
+
# pass
|
|
321
|
+
|
|
322
|
+
# await session.commit()
|
|
323
|
+
# return RedirectResponse(url="/admin/users", status_code=302)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# @router.post("/users/{user_id}/delete")
|
|
327
|
+
# async def user_delete_post(
|
|
328
|
+
# request: Request,
|
|
329
|
+
# user_id: int,
|
|
330
|
+
# current_user: AdminUserProtocol = Depends(_require_superuser),
|
|
331
|
+
# _csrf: bool = Depends(require_csrf_token),
|
|
332
|
+
# ):
|
|
333
|
+
# """Soft-delete user (set is_active=False)."""
|
|
334
|
+
# session = get_db_session(request)
|
|
335
|
+
# user = await session.get(AdminUser, user_id)
|
|
336
|
+
# if user is None:
|
|
337
|
+
# raise HTTPException(status_code=404, detail="User not found.")
|
|
338
|
+
|
|
339
|
+
# if user.id == current_user.id:
|
|
340
|
+
# raise HTTPException(
|
|
341
|
+
# status_code=400, detail="Cannot deactivate your own account."
|
|
342
|
+
# )
|
|
343
|
+
|
|
344
|
+
# user.is_active = False
|
|
345
|
+
# await session.commit()
|
|
346
|
+
|
|
347
|
+
# return RedirectResponse(url="/admin/users", status_code=302)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Route handler factories — list, form, delete, roles, dashboard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Request
|
|
8
|
+
from fastapi.responses import HTMLResponse
|
|
9
|
+
|
|
10
|
+
from fastapi_admin_kit.admin.decorators import column
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from fastapi_admin_kit.registry import RegisteredModel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ModelAdmin:
|
|
17
|
+
"""Base class for model admin configuration."""
|
|
18
|
+
|
|
19
|
+
# List view config
|
|
20
|
+
list_display: list[str] | None = None
|
|
21
|
+
list_filter: list[str] | None = None
|
|
22
|
+
search_fields: list[str] | None = None
|
|
23
|
+
ordering: list[str] | None = None
|
|
24
|
+
per_page: int = 20
|
|
25
|
+
|
|
26
|
+
# Form config
|
|
27
|
+
fields: list[str] | None = None
|
|
28
|
+
exclude: list[str] | None = None
|
|
29
|
+
readonly_fields: list[str] | None = None
|
|
30
|
+
|
|
31
|
+
# Labels and display
|
|
32
|
+
verbose_name: str | None = None
|
|
33
|
+
verbose_name_plural: str | None = None
|
|
34
|
+
icon: str | None = None
|
|
35
|
+
|
|
36
|
+
# Custom display functions (dict-based fallback)
|
|
37
|
+
display_functions: dict[str, Any] | None = None
|
|
38
|
+
|
|
39
|
+
# Decorator for custom column display
|
|
40
|
+
column = staticmethod(column)
|
|
41
|
+
|
|
42
|
+
def __str__(self, obj: Any) -> str:
|
|
43
|
+
"""How to display an object in dropdowns/links."""
|
|
44
|
+
return str(getattr(obj, "name", None) or getattr(obj, "title", None) or f"#{obj.id}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def create_list_view(registered: RegisteredModel) -> Any:
|
|
48
|
+
"""Create a list view handler for a model."""
|
|
49
|
+
|
|
50
|
+
async def list_view(request: Request) -> HTMLResponse:
|
|
51
|
+
# Placeholder — will render list template
|
|
52
|
+
return HTMLResponse(
|
|
53
|
+
f"<h1>{registered.verbose_name_plural}</h1><p>List view coming soon.</p>"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return list_view
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def create_create_view(registered: RegisteredModel) -> Any:
|
|
60
|
+
"""Create a create view handler for a model."""
|
|
61
|
+
|
|
62
|
+
async def create_view(request: Request) -> HTMLResponse:
|
|
63
|
+
# Placeholder — will render form template
|
|
64
|
+
return HTMLResponse(
|
|
65
|
+
f"<h1>Create {registered.verbose_name}</h1><p>Form coming soon.</p>"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return create_view
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def create_edit_view(registered: RegisteredModel) -> Any:
|
|
72
|
+
"""Create an edit view handler for a model."""
|
|
73
|
+
|
|
74
|
+
async def edit_view(request: Request, item_id: str) -> HTMLResponse:
|
|
75
|
+
# Placeholder — will render form template
|
|
76
|
+
return HTMLResponse(
|
|
77
|
+
f"<h1>Edit {registered.verbose_name}</h1><p>Form for {item_id} coming soon.</p>"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return edit_view
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def create_delete_view(registered: RegisteredModel) -> Any:
|
|
84
|
+
"""Create a delete view handler for a model."""
|
|
85
|
+
|
|
86
|
+
async def delete_view(request: Request, item_id: str) -> HTMLResponse:
|
|
87
|
+
# Placeholder — will handle deletion
|
|
88
|
+
return HTMLResponse(
|
|
89
|
+
f"<h1>Delete {registered.verbose_name}</h1><p>Delete {item_id} coming soon.</p>"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return delete_view
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def create_model_router(registered: RegisteredModel) -> APIRouter:
|
|
96
|
+
"""Create all CRUD routes for a registered model."""
|
|
97
|
+
router = APIRouter(
|
|
98
|
+
prefix=f"/{registered.table_name}", tags=[registered.verbose_name]
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
router.add_api_route(
|
|
102
|
+
"/", create_list_view(registered), methods=["GET"], name="list"
|
|
103
|
+
)
|
|
104
|
+
router.add_api_route(
|
|
105
|
+
"/create", create_create_view(registered),
|
|
106
|
+
methods=["GET", "POST"], name="create",
|
|
107
|
+
)
|
|
108
|
+
router.add_api_route(
|
|
109
|
+
"/{item_id}", create_edit_view(registered),
|
|
110
|
+
methods=["GET", "POST"], name="edit",
|
|
111
|
+
)
|
|
112
|
+
router.add_api_route(
|
|
113
|
+
"/{item_id}/delete", create_delete_view(registered),
|
|
114
|
+
methods=["POST"], name="delete",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return router
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Widgets module — base class, all built-in widgets, WidgetRegistry, WidgetResolver."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi_admin_kit.widgets.base import Widget
|
|
6
|
+
from fastapi_admin_kit.widgets.inputs import (
|
|
7
|
+
DatePickerWidget,
|
|
8
|
+
DateTimePickerWidget,
|
|
9
|
+
FileUploadWidget,
|
|
10
|
+
HiddenWidget,
|
|
11
|
+
ImageUploadWidget,
|
|
12
|
+
JsonEditorWidget,
|
|
13
|
+
NumberInputWidget,
|
|
14
|
+
PasswordWidget,
|
|
15
|
+
ReadOnlyWidget,
|
|
16
|
+
SelectWidget,
|
|
17
|
+
TextareaWidget,
|
|
18
|
+
TextInputWidget,
|
|
19
|
+
ToggleWidget,
|
|
20
|
+
)
|
|
21
|
+
from fastapi_admin_kit.widgets.registry import WidgetRegistry
|
|
22
|
+
from fastapi_admin_kit.widgets.relation import MultiRelationWidget, RelationPickerWidget
|
|
23
|
+
from fastapi_admin_kit.widgets.resolver import WidgetResolver
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"Widget",
|
|
27
|
+
"WidgetRegistry",
|
|
28
|
+
"WidgetResolver",
|
|
29
|
+
"TextInputWidget",
|
|
30
|
+
"TextareaWidget",
|
|
31
|
+
"NumberInputWidget",
|
|
32
|
+
"ToggleWidget",
|
|
33
|
+
"DatePickerWidget",
|
|
34
|
+
"DateTimePickerWidget",
|
|
35
|
+
"SelectWidget",
|
|
36
|
+
"JsonEditorWidget",
|
|
37
|
+
"PasswordWidget",
|
|
38
|
+
"ReadOnlyWidget",
|
|
39
|
+
"HiddenWidget",
|
|
40
|
+
"FileUploadWidget",
|
|
41
|
+
"ImageUploadWidget",
|
|
42
|
+
"RelationPickerWidget",
|
|
43
|
+
"MultiRelationWidget",
|
|
44
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Widget base class — two-layer: Python class (parse + validate) + Jinja2 macro (HTML)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from fastapi_admin_kit.types import FieldMeta
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Widget:
|
|
11
|
+
"""Base widget class.
|
|
12
|
+
|
|
13
|
+
Stateless: receives FieldMeta + value at render/validate time.
|
|
14
|
+
Override ``parse`` to transform raw form data.
|
|
15
|
+
Override ``validate`` to add custom validation.
|
|
16
|
+
Override ``render_context`` to customise template variables.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
macro_name: str = "text_input"
|
|
20
|
+
|
|
21
|
+
def render_context(self, field: FieldMeta, value: Any) -> dict:
|
|
22
|
+
"""Variables injected into the Jinja2 macro."""
|
|
23
|
+
return {
|
|
24
|
+
"field": field,
|
|
25
|
+
"value": value if value is not None else "",
|
|
26
|
+
"id": f"field-{field.name}",
|
|
27
|
+
"name": field.name,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def parse(self, raw: str | list | None) -> Any:
|
|
31
|
+
"""Convert raw FormData string to typed Python value."""
|
|
32
|
+
if raw is None or raw == "":
|
|
33
|
+
return None
|
|
34
|
+
return raw
|
|
35
|
+
|
|
36
|
+
def validate(self, value: Any, field: FieldMeta) -> list[str]:
|
|
37
|
+
"""Return a list of error messages. Empty list means valid."""
|
|
38
|
+
errors: list[str] = []
|
|
39
|
+
if field.required and (value is None or value == ""):
|
|
40
|
+
errors.append(f"{field.label} is required.")
|
|
41
|
+
return errors
|
|
42
|
+
|
|
43
|
+
def __repr__(self) -> str:
|
|
44
|
+
return f"<{self.__class__.__name__} macro={self.macro_name!r}>"
|