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.
Files changed (163) hide show
  1. fastapi_admin_kit/__init__.py +73 -0
  2. fastapi_admin_kit/actions/__init__.py +63 -0
  3. fastapi_admin_kit/actions/base.py +68 -0
  4. fastapi_admin_kit/actions/registry.py +43 -0
  5. fastapi_admin_kit/admin/__init__.py +17 -0
  6. fastapi_admin_kit/admin/admin_config.py +95 -0
  7. fastapi_admin_kit/admin/admin_database.py +138 -0
  8. fastapi_admin_kit/admin/admin_router.py +74 -0
  9. fastapi_admin_kit/admin/admin_template.py +203 -0
  10. fastapi_admin_kit/admin/builtin_models.py +284 -0
  11. fastapi_admin_kit/admin/core.py +1036 -0
  12. fastapi_admin_kit/admin/decorators.py +70 -0
  13. fastapi_admin_kit/admin/state.py +76 -0
  14. fastapi_admin_kit/admin.py +728 -0
  15. fastapi_admin_kit/api/__init__.py +44 -0
  16. fastapi_admin_kit/api/auth.py +342 -0
  17. fastapi_admin_kit/api/crud.py +128 -0
  18. fastapi_admin_kit/api/deps.py +79 -0
  19. fastapi_admin_kit/api/roles.py +128 -0
  20. fastapi_admin_kit/api/schema_generator.py +171 -0
  21. fastapi_admin_kit/api/schemas.py +81 -0
  22. fastapi_admin_kit/api/search.py +132 -0
  23. fastapi_admin_kit/audit/__init__.py +36 -0
  24. fastapi_admin_kit/audit/context.py +62 -0
  25. fastapi_admin_kit/audit/diff.py +77 -0
  26. fastapi_admin_kit/audit/event_bus.py +96 -0
  27. fastapi_admin_kit/audit/events.py +48 -0
  28. fastapi_admin_kit/audit/listener.py +159 -0
  29. fastapi_admin_kit/audit/logger.py +28 -0
  30. fastapi_admin_kit/audit/middleware.py +39 -0
  31. fastapi_admin_kit/audit/models.py +53 -0
  32. fastapi_admin_kit/audit/sqlalchemy_logger.py +58 -0
  33. fastapi_admin_kit/auth/__init__.py +34 -0
  34. fastapi_admin_kit/auth/backend.py +95 -0
  35. fastapi_admin_kit/auth/csrf.py +240 -0
  36. fastapi_admin_kit/auth/dependencies.py +150 -0
  37. fastapi_admin_kit/auth/identity.py +181 -0
  38. fastapi_admin_kit/auth/models.py +246 -0
  39. fastapi_admin_kit/auth/password.py +35 -0
  40. fastapi_admin_kit/auth/permissions.py +205 -0
  41. fastapi_admin_kit/auth/protocol.py +22 -0
  42. fastapi_admin_kit/auth/ratelimit.py +88 -0
  43. fastapi_admin_kit/auth/router.py +10 -0
  44. fastapi_admin_kit/auth/session.py +79 -0
  45. fastapi_admin_kit/auth/totp.py +83 -0
  46. fastapi_admin_kit/auth/views.py +165 -0
  47. fastapi_admin_kit/cli.py +229 -0
  48. fastapi_admin_kit/config/__init__.py +19 -0
  49. fastapi_admin_kit/config/audit.py +18 -0
  50. fastapi_admin_kit/config/auth.py +54 -0
  51. fastapi_admin_kit/config/behavior.py +27 -0
  52. fastapi_admin_kit/config/nav.py +32 -0
  53. fastapi_admin_kit/config/storage.py +22 -0
  54. fastapi_admin_kit/config/theme.py +215 -0
  55. fastapi_admin_kit/config/ui.py +147 -0
  56. fastapi_admin_kit/dashboard/__init__.py +64 -0
  57. fastapi_admin_kit/db.py +133 -0
  58. fastapi_admin_kit/exceptions.py +5 -0
  59. fastapi_admin_kit/field_types.py +81 -0
  60. fastapi_admin_kit/filters/__init__.py +21 -0
  61. fastapi_admin_kit/filters/base.py +170 -0
  62. fastapi_admin_kit/filters/registry.py +68 -0
  63. fastapi_admin_kit/flash.py +45 -0
  64. fastapi_admin_kit/form/__init__.py +1 -0
  65. fastapi_admin_kit/form/pipeline.py +106 -0
  66. fastapi_admin_kit/inspection/__init__.py +117 -0
  67. fastapi_admin_kit/inspection/registry.py +253 -0
  68. fastapi_admin_kit/inspection.py +115 -0
  69. fastapi_admin_kit/modeladmin.py +375 -0
  70. fastapi_admin_kit/models/__init__.py +7 -0
  71. fastapi_admin_kit/models/base.py +7 -0
  72. fastapi_admin_kit/nav.py +208 -0
  73. fastapi_admin_kit/pagination/__init__.py +14 -0
  74. fastapi_admin_kit/pagination/base.py +40 -0
  75. fastapi_admin_kit/pagination/cursor.py +97 -0
  76. fastapi_admin_kit/pagination/dynamic.py +48 -0
  77. fastapi_admin_kit/pagination/offset.py +42 -0
  78. fastapi_admin_kit/plugins/__init__.py +1 -0
  79. fastapi_admin_kit/py.typed +0 -0
  80. fastapi_admin_kit/registry/__init__.py +5 -0
  81. fastapi_admin_kit/registry/core.py +287 -0
  82. fastapi_admin_kit/registry/validation.py +107 -0
  83. fastapi_admin_kit/registry.py +15 -0
  84. fastapi_admin_kit/router.py +335 -0
  85. fastapi_admin_kit/static/css/admin.css +4736 -0
  86. fastapi_admin_kit/static/css/presets.css +317 -0
  87. fastapi_admin_kit/static/css/tokens.css +217 -0
  88. fastapi_admin_kit/static/css/variables.css +74 -0
  89. fastapi_admin_kit/static/icons/heroicons.svg +160 -0
  90. fastapi_admin_kit/static/js/admin.js +692 -0
  91. fastapi_admin_kit/static/js/htmx-config.js +42 -0
  92. fastapi_admin_kit/storage/__init__.py +6 -0
  93. fastapi_admin_kit/storage/base.py +48 -0
  94. fastapi_admin_kit/storage/local.py +73 -0
  95. fastapi_admin_kit/templates/base.html +142 -0
  96. fastapi_admin_kit/templates/macros/form_fields.html +660 -0
  97. fastapi_admin_kit/templates/macros/icons.html +50 -0
  98. fastapi_admin_kit/templates/macros/table.html +108 -0
  99. fastapi_admin_kit/templates/macros/widgets.html +159 -0
  100. fastapi_admin_kit/templates/pages/2fa/setup.html +122 -0
  101. fastapi_admin_kit/templates/pages/2fa/verify.html +55 -0
  102. fastapi_admin_kit/templates/pages/audit_detail.html +122 -0
  103. fastapi_admin_kit/templates/pages/audit_log.html +102 -0
  104. fastapi_admin_kit/templates/pages/dashboard.html +295 -0
  105. fastapi_admin_kit/templates/pages/detail.html +183 -0
  106. fastapi_admin_kit/templates/pages/form.html +119 -0
  107. fastapi_admin_kit/templates/pages/list.html +277 -0
  108. fastapi_admin_kit/templates/pages/login.html +85 -0
  109. fastapi_admin_kit/templates/pages/profile/password.html +78 -0
  110. fastapi_admin_kit/templates/pages/profile/profile.html +73 -0
  111. fastapi_admin_kit/templates/pages/role_form.html +75 -0
  112. fastapi_admin_kit/templates/pages/roles/form.html +117 -0
  113. fastapi_admin_kit/templates/pages/roles/list.html +69 -0
  114. fastapi_admin_kit/templates/pages/roles.html +77 -0
  115. fastapi_admin_kit/templates/pages/settings/theme.html +255 -0
  116. fastapi_admin_kit/templates/pages/users/form.html +229 -0
  117. fastapi_admin_kit/templates/pages/users/list.html +83 -0
  118. fastapi_admin_kit/templates/partials/command_palette.html +52 -0
  119. fastapi_admin_kit/templates/partials/field_wrapper.html +2 -0
  120. fastapi_admin_kit/templates/partials/flash_messages.html +39 -0
  121. fastapi_admin_kit/templates/partials/head.html +21 -0
  122. fastapi_admin_kit/templates/partials/head_minimal.html +18 -0
  123. fastapi_admin_kit/templates/partials/list_table.html +178 -0
  124. fastapi_admin_kit/templates/partials/mobile_backdrop.html +2 -0
  125. fastapi_admin_kit/templates/partials/pagination.html +82 -0
  126. fastapi_admin_kit/templates/partials/permission_widget.html +86 -0
  127. fastapi_admin_kit/templates/partials/scripts.html +13 -0
  128. fastapi_admin_kit/templates/partials/sidebar.html +94 -0
  129. fastapi_admin_kit/templates/partials/topbar.html +95 -0
  130. fastapi_admin_kit/types.py +145 -0
  131. fastapi_admin_kit/validation.py +43 -0
  132. fastapi_admin_kit/views/__init__.py +78 -0
  133. fastapi_admin_kit/views/audit.py +134 -0
  134. fastapi_admin_kit/views/bulk.py +28 -0
  135. fastapi_admin_kit/views/class_views.py +1040 -0
  136. fastapi_admin_kit/views/context.py +588 -0
  137. fastapi_admin_kit/views/dashboard.py +162 -0
  138. fastapi_admin_kit/views/delete.py +31 -0
  139. fastapi_admin_kit/views/extra.py +65 -0
  140. fastapi_admin_kit/views/factory.py +667 -0
  141. fastapi_admin_kit/views/form.py +159 -0
  142. fastapi_admin_kit/views/list.py +28 -0
  143. fastapi_admin_kit/views/profile.py +219 -0
  144. fastapi_admin_kit/views/protocols.py +54 -0
  145. fastapi_admin_kit/views/renderers.py +634 -0
  146. fastapi_admin_kit/views/roles.py +230 -0
  147. fastapi_admin_kit/views/search.py +31 -0
  148. fastapi_admin_kit/views/settings.py +31 -0
  149. fastapi_admin_kit/views/sidebar.py +101 -0
  150. fastapi_admin_kit/views/totp.py +249 -0
  151. fastapi_admin_kit/views/users.py +347 -0
  152. fastapi_admin_kit/views.py +117 -0
  153. fastapi_admin_kit/widgets/__init__.py +44 -0
  154. fastapi_admin_kit/widgets/base.py +44 -0
  155. fastapi_admin_kit/widgets/inputs.py +363 -0
  156. fastapi_admin_kit/widgets/registry.py +110 -0
  157. fastapi_admin_kit/widgets/relation.py +70 -0
  158. fastapi_admin_kit/widgets/resolver.py +102 -0
  159. fastapi_admin_kit-0.1.0.dist-info/METADATA +210 -0
  160. fastapi_admin_kit-0.1.0.dist-info/RECORD +163 -0
  161. fastapi_admin_kit-0.1.0.dist-info/WHEEL +4 -0
  162. fastapi_admin_kit-0.1.0.dist-info/entry_points.txt +3 -0
  163. 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}>"