fastadmin 0.4.2__tar.gz → 0.4.4__tar.gz

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 (51) hide show
  1. fastadmin-0.4.4/PKG-INFO +877 -0
  2. fastadmin-0.4.4/README.md +834 -0
  3. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/models/schemas.py +1 -0
  4. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/static/index.min.js +184 -184
  5. {fastadmin-0.4.2 → fastadmin-0.4.4}/pyproject.toml +1 -1
  6. fastadmin-0.4.2/PKG-INFO +0 -808
  7. fastadmin-0.4.2/README.md +0 -765
  8. {fastadmin-0.4.2 → fastadmin-0.4.4}/LICENSE +0 -0
  9. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/__init__.py +0 -0
  10. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/api/__init__.py +0 -0
  11. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/api/exceptions.py +0 -0
  12. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/api/frameworks/__init__.py +0 -0
  13. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/api/frameworks/django/__init__.py +0 -0
  14. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/api/frameworks/django/app/__init__.py +0 -0
  15. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/api/frameworks/django/app/api.py +0 -0
  16. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/api/frameworks/django/app/urls.py +0 -0
  17. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/api/frameworks/django/app/views.py +0 -0
  18. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/api/frameworks/fastapi/__init__.py +0 -0
  19. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/api/frameworks/fastapi/api.py +0 -0
  20. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/api/frameworks/fastapi/app.py +0 -0
  21. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/api/frameworks/fastapi/views.py +0 -0
  22. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/api/frameworks/flask/__init__.py +0 -0
  23. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/api/frameworks/flask/api.py +0 -0
  24. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/api/frameworks/flask/app.py +0 -0
  25. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/api/frameworks/flask/views.py +0 -0
  26. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/api/helpers.py +0 -0
  27. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/api/schemas.py +0 -0
  28. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/api/service.py +0 -0
  29. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/models/__init__.py +0 -0
  30. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/models/base.py +0 -0
  31. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/models/decorators.py +0 -0
  32. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/models/helpers.py +0 -0
  33. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/models/orms/__init__.py +0 -0
  34. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/models/orms/django.py +0 -0
  35. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/models/orms/ponyorm.py +0 -0
  36. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/models/orms/sqlalchemy.py +0 -0
  37. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/models/orms/tortoise.py +0 -0
  38. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/settings.py +0 -0
  39. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/static/assets/worker-6Z7niv9l.js +0 -0
  40. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/static/assets/worker-C151k0-L.js +0 -0
  41. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/static/assets/worker-C9KPOOKD.js +0 -0
  42. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/static/assets/worker-D1fCgYB7.js +0 -0
  43. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/static/assets/worker-D3TWcJOI.js +0 -0
  44. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/static/assets/worker-DRNcaZ-V.js +0 -0
  45. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/static/assets/worker-MF2p-l5_.js +0 -0
  46. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/static/images/favicon.png +0 -0
  47. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/static/images/header-logo.svg +0 -0
  48. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/static/images/sign-in-logo.svg +0 -0
  49. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/static/index.html +0 -0
  50. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/static/index.min.css +0 -0
  51. {fastadmin-0.4.2 → fastadmin-0.4.4}/fastadmin/templates/index.html +0 -0
@@ -0,0 +1,877 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastadmin
3
+ Version: 0.4.4
4
+ Summary: FastAdmin is an easy-to-use Admin Dashboard App for FastAPI/Flask/Django inspired by Django Admin.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: fastapi,admin
8
+ Author: Seva D
9
+ Author-email: vsdudakov@gmail.com
10
+ Requires-Python: >=3.12,<4.0
11
+ Classifier: Environment :: Console
12
+ Classifier: Framework :: Django
13
+ Classifier: Framework :: FastAPI
14
+ Classifier: Framework :: Flask
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development :: Documentation
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: Software Development :: Widget Sets
24
+ Provides-Extra: django
25
+ Provides-Extra: fastapi
26
+ Provides-Extra: flask
27
+ Provides-Extra: pony
28
+ Provides-Extra: sqlalchemy
29
+ Provides-Extra: tortoise-orm
30
+ Requires-Dist: asgiref (>=3.11.1,<4.0.0)
31
+ Requires-Dist: django (==6.*) ; extra == "django"
32
+ Requires-Dist: fastapi (==0.*) ; extra == "fastapi"
33
+ Requires-Dist: flask (==3.*) ; extra == "flask"
34
+ Requires-Dist: greenlet (>=3.0,<4.0) ; extra == "sqlalchemy"
35
+ Requires-Dist: pony (==0.*) ; extra == "pony"
36
+ Requires-Dist: pyjwt (>=2.6.0,<3.0.0)
37
+ Requires-Dist: python-multipart (==0.*) ; extra == "fastapi"
38
+ Requires-Dist: sqlalchemy[asyncio] (==2.*) ; extra == "sqlalchemy"
39
+ Requires-Dist: tortoise-orm (==1.*) ; extra == "tortoise-orm"
40
+ Project-URL: Homepage, https://github.com/vsdudakov/fastadmin
41
+ Project-URL: Repository, https://github.com/vsdudakov/fastadmin
42
+ Description-Content-Type: text/markdown
43
+
44
+ # Admin Dashboard for FastAPI / Flask / Django
45
+
46
+ [![codecov](https://codecov.io/gh/vsdudakov/fastadmin/branch/main/graph/badge.svg?token=RNGX5HOW3T)](https://app.codecov.io/gh/vsdudakov/fastadmin)
47
+ [![License](https://img.shields.io/github/license/vsdudakov/fastadmin)](https://github.com/vsdudakov/fastadmin/blob/master/LICENSE)
48
+ [![PyPi](https://badgen.net/pypi/v/fastadmin)](https://pypi.org/project/fastadmin/)
49
+ [![Python 3.12](https://img.shields.io/badge/python-3.12-blue.svg)](https://www.python.org/downloads/release/python-3120/)
50
+ [![Python 3.13](https://img.shields.io/badge/python-3.13-blue.svg)](https://www.python.org/downloads/release/python-3130/)
51
+
52
+ ## Demo
53
+
54
+ ![FastAdmin demo](https://raw.githubusercontent.com/vsdudakov/fastadmin/main/docs/assets/images/demo.gif)
55
+
56
+ <p align="center">
57
+ <a href="https://twitter.com/intent/tweet?text=Admin%20Dashboard%20For%20FastAPI&url=https://github.com/vsdudakov/fastadmin&hashtags=FastAPI,AdminDashboard">
58
+ <img alt="tweet" src="https://img.shields.io/twitter/url/https/twitter?label=Share%20on%20twitter&style=social" target="_blank" />
59
+ </a>
60
+ </p>
61
+
62
+
63
+
64
+ ## Introduction
65
+
66
+
67
+ <a href='https://github.com/vsdudakov/fastadmin' target='_blank'>FastAdmin</a> is an easy-to-use admin dashboard for FastAPI, Django, and Flask, inspired by Django Admin.
68
+
69
+
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+
80
+
81
+
82
+
83
+ FastAdmin is built with relationships in mind and admiration for Django Admin. Its design focuses on making it as easy as possible to configure your admin dashboard for FastAPI, Django, or Flask.
84
+
85
+
86
+
87
+
88
+
89
+
90
+
91
+
92
+
93
+
94
+
95
+
96
+
97
+
98
+
99
+ FastAdmin aims to be minimal, functional, and familiar.
100
+
101
+
102
+
103
+
104
+
105
+
106
+
107
+
108
+
109
+
110
+
111
+
112
+
113
+
114
+
115
+
116
+
117
+ ## Getting Started
118
+
119
+
120
+
121
+
122
+
123
+
124
+ If you have questions beyond this documentation, feel free to <a href='mailto:vsdudakov@gmail.com' target='_blank'>email us</a>.
125
+
126
+
127
+
128
+
129
+
130
+
131
+
132
+
133
+
134
+
135
+
136
+
137
+
138
+
139
+ ### Installation
140
+
141
+
142
+
143
+
144
+ Follow the steps below to set up FastAdmin:
145
+
146
+
147
+
148
+
149
+
150
+
151
+
152
+
153
+
154
+
155
+
156
+
157
+
158
+ Install the package with pip:
159
+
160
+
161
+
162
+
163
+
164
+
165
+
166
+
167
+
168
+
169
+
170
+
171
+
172
+
173
+
174
+
175
+
176
+
177
+
178
+ On zsh and macOS, use quotes: <code>pip install 'fastadmin[fastapi,django]'</code>
179
+
180
+
181
+
182
+
183
+
184
+
185
+
186
+
187
+
188
+
189
+
190
+
191
+
192
+
193
+
194
+
195
+
196
+
197
+
198
+ ```bash
199
+
200
+ pip install fastadmin[fastapi,django] # FastAPI with Django ORM
201
+ pip install fastadmin[fastapi,tortoise-orm] # FastAPI with Tortoise ORM
202
+ pip install fastadmin[fastapi,pony] # FastAPI with Pony ORM
203
+ pip install fastadmin[fastapi,sqlalchemy] # FastAPI with SQLAlchemy (includes greenlet)
204
+ pip install fastadmin[django] # Django with Django ORM
205
+ pip install fastadmin[django,pony] # Django with Pony ORM
206
+ pip install fastadmin[flask,sqlalchemy] # Flask with SQLAlchemy (includes greenlet)
207
+
208
+ ```
209
+
210
+
211
+
212
+
213
+
214
+
215
+
216
+ Or install with Poetry:
217
+
218
+
219
+
220
+
221
+
222
+
223
+
224
+
225
+
226
+
227
+
228
+
229
+
230
+
231
+
232
+
233
+
234
+
235
+
236
+
237
+
238
+
239
+
240
+ ```bash
241
+
242
+ poetry add 'fastadmin[fastapi,django]'
243
+ poetry add 'fastadmin[fastapi,tortoise-orm]'
244
+ poetry add 'fastadmin[fastapi,pony]'
245
+ poetry add 'fastadmin[fastapi,sqlalchemy]'
246
+ poetry add 'fastadmin[django]'
247
+ poetry add 'fastadmin[django,pony]'
248
+ poetry add 'fastadmin[flask,sqlalchemy]'
249
+
250
+ ```
251
+
252
+
253
+
254
+
255
+
256
+
257
+
258
+
259
+
260
+
261
+
262
+ When using SQLAlchemy, the <code>greenlet</code> package is required (included in the <code>fastadmin[sqlalchemy]</code> extra).
263
+
264
+
265
+
266
+
267
+
268
+
269
+
270
+
271
+
272
+
273
+
274
+ Configure the required settings with environment variables:
275
+
276
+
277
+
278
+
279
+
280
+
281
+
282
+
283
+
284
+
285
+
286
+
287
+
288
+
289
+
290
+
291
+
292
+
293
+
294
+ You can add these variables to a <code>.env</code> file and load them with python-dotenv. See <a href='https://vsdudakov.github.io/fastadmin#settings'>all settings</a> in the full documentation.
295
+
296
+
297
+
298
+
299
+
300
+
301
+
302
+
303
+
304
+
305
+
306
+
307
+
308
+
309
+
310
+
311
+
312
+
313
+
314
+ ```bash
315
+
316
+ export ADMIN_USER_MODEL=User
317
+ export ADMIN_USER_MODEL_USERNAME_FIELD=username
318
+ export ADMIN_SECRET_KEY=secret_key
319
+
320
+ ```
321
+
322
+
323
+
324
+
325
+
326
+
327
+
328
+
329
+
330
+
331
+
332
+
333
+
334
+
335
+
336
+
337
+
338
+
339
+
340
+
341
+
342
+
343
+ ## Quick Examples
344
+
345
+ ### ORM setup (User, UserAttachment, actions, widgets)
346
+
347
+ #### Tortoise ORM
348
+
349
+ ```python
350
+ from tortoise import fields
351
+ from tortoise.models import Model
352
+
353
+
354
+ class User(Model):
355
+ username = fields.CharField(max_length=255, unique=True)
356
+ hash_password = fields.CharField(max_length=255)
357
+ is_superuser = fields.BooleanField(default=False)
358
+ is_active = fields.BooleanField(default=True)
359
+ avatar_url = fields.TextField(null=True)
360
+
361
+
362
+ class UserAttachment(Model):
363
+ user = fields.ForeignKeyField("models.User", related_name="attachments")
364
+ attachment_url = fields.TextField()
365
+ ```
366
+
367
+ ```python
368
+ from fastadmin import (
369
+ TortoiseInlineModelAdmin,
370
+ TortoiseModelAdmin,
371
+ WidgetType,
372
+ action,
373
+ register,
374
+ widget_action,
375
+ )
376
+ from fastadmin.models.schemas import (
377
+ WidgetActionChartProps,
378
+ WidgetActionInputSchema,
379
+ WidgetActionResponseSchema,
380
+ WidgetActionType,
381
+ )
382
+ from .models import User, UserAttachment
383
+
384
+
385
+ class UserAttachmentInline(TortoiseInlineModelAdmin):
386
+ model = UserAttachment
387
+ formfield_overrides = {
388
+ "attachment_url": (WidgetType.UploadFile, {"required": True}),
389
+ }
390
+
391
+ async def upload_file(self, field_name: str, file_name: str, file_content: bytes) -> str:
392
+ # save file to media directory or to s3/filestorage here
393
+ return f"/media/{file_name}"
394
+
395
+
396
+ @register(User)
397
+ class UserAdmin(TortoiseModelAdmin):
398
+ list_display = ("id", "username", "is_superuser", "is_active")
399
+ inlines = (UserAttachmentInline,)
400
+
401
+ formfield_overrides = {
402
+ "avatar_url": (WidgetType.UploadImage, {"required": False}),
403
+ }
404
+
405
+ actions = ("activate", "deactivate")
406
+ widget_actions = ("users_chart", "users_list")
407
+
408
+ @action(description="Activate selected users")
409
+ async def activate(self, ids: list[int]) -> None:
410
+ await self.model_cls.filter(id__in=ids).update(is_active=True)
411
+
412
+ @action(description="Deactivate selected users")
413
+ async def deactivate(self, ids: list[int]) -> None:
414
+ await self.model_cls.filter(id__in=ids).update(is_active=False)
415
+
416
+ async def upload_file(self, field_name: str, file_name: str, file_content: bytes) -> str:
417
+ # handle avatar_url uploads for User (and other file fields if needed)
418
+ return f"/media/{file_name}"
419
+
420
+ @widget_action(
421
+ widget_action_type=WidgetActionType.ChartLine,
422
+ widget_action_props=WidgetActionChartProps(x_field="x", y_field="y", series_field="series"),
423
+ tab="Analytics",
424
+ title="Users over time",
425
+ )
426
+ async def users_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
427
+ return WidgetActionResponseSchema(
428
+ data=[
429
+ {"x": "2026-01-01", "y": 10, "series": "Active"},
430
+ {"x": "2026-01-02", "y": 15, "series": "Active"},
431
+ {"x": "2026-01-01", "y": 3, "series": "Inactive"},
432
+ {"x": "2026-01-02", "y": 5, "series": "Inactive"},
433
+ ]
434
+ )
435
+
436
+ @widget_action(
437
+ widget_action_type=WidgetActionType.Action,
438
+ tab="Data",
439
+ title="Users list",
440
+ description="Simple action widget that returns a table of users.",
441
+ )
442
+ async def users_list(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
443
+ return WidgetActionResponseSchema(
444
+ data=[
445
+ {"id": 1, "username": "alice"},
446
+ {"id": 2, "username": "bob"},
447
+ ]
448
+ )
449
+ ```
450
+
451
+ #### Django ORM
452
+
453
+ ```python
454
+ from django.db import models
455
+
456
+
457
+ class User(models.Model):
458
+ username = models.CharField(max_length=255, unique=True)
459
+ password = models.CharField(max_length=255)
460
+ is_superuser = models.BooleanField(default=False)
461
+ is_active = models.BooleanField(default=True)
462
+ avatar_url = models.ImageField(null=True)
463
+
464
+
465
+ class UserAttachment(models.Model):
466
+ user = models.ForeignKey(User, related_name="attachments", on_delete=models.CASCADE)
467
+ attachment_url = models.FileField()
468
+ ```
469
+
470
+ ```python
471
+ from fastadmin import (
472
+ DjangoInlineModelAdmin,
473
+ DjangoModelAdmin,
474
+ WidgetType,
475
+ action,
476
+ register,
477
+ widget_action,
478
+ )
479
+ from fastadmin.models.schemas import (
480
+ WidgetActionArgumentProps,
481
+ WidgetActionInputSchema,
482
+ WidgetActionProps,
483
+ WidgetActionResponseSchema,
484
+ WidgetActionType,
485
+ )
486
+ from .models import User, UserAttachment
487
+
488
+
489
+ class UserAttachmentInline(DjangoInlineModelAdmin):
490
+ model = UserAttachment
491
+ formfield_overrides = {
492
+ "attachment_url": (WidgetType.UploadFile, {"required": True}),
493
+ }
494
+
495
+ def upload_file(self, field_name: str, file_name: str, file_content: bytes) -> str:
496
+ # save file to media directory or to s3/filestorage here
497
+ return f"/media/{file_name}"
498
+
499
+
500
+ @register(User)
501
+ class UserAdmin(DjangoModelAdmin):
502
+ list_display = ("id", "username", "is_superuser", "is_active")
503
+ inlines = (UserAttachmentInline,)
504
+
505
+ formfield_overrides = {
506
+ "avatar_url": (WidgetType.UploadImage, {"required": False}),
507
+ }
508
+
509
+ actions = ("activate", "deactivate")
510
+ widget_actions = ("users_summary", "users_chart")
511
+
512
+ @action(description="Activate selected users")
513
+ def activate(self, ids):
514
+ self.model_cls.objects.filter(id__in=ids).update(is_active=True)
515
+
516
+ @action(description="Deactivate selected users")
517
+ def deactivate(self, ids):
518
+ self.model_cls.objects.filter(id__in=ids).update(is_active=False)
519
+
520
+ def upload_file(self, field_name: str, file_name: str, file_content: bytes) -> str:
521
+ # handle avatar_url uploads for User (and other file fields if needed)
522
+ return f"/media/{file_name}"
523
+
524
+ @widget_action(
525
+ widget_action_type=WidgetActionType.Action,
526
+ widget_action_props=WidgetActionProps(
527
+ arguments=[
528
+ WidgetActionArgumentProps(
529
+ name="only_active",
530
+ widget_type=WidgetType.Switch,
531
+ widget_props={"required": False},
532
+ )
533
+ ]
534
+ ),
535
+ tab="Data",
536
+ title="Users summary",
537
+ )
538
+ def users_summary(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
539
+ qs = self.model_cls.objects.filter(is_active=True) if payload.arguments.get("only_active") else self.model_cls.objects.all()
540
+ return WidgetActionResponseSchema(
541
+ data=[{"id": u.id, "username": u.username} for u in qs[:5]]
542
+ )
543
+
544
+ @widget_action(
545
+ widget_action_type=WidgetActionType.ChartLine,
546
+ widget_action_props=WidgetActionChartProps(x_field="label", y_field="value", series_field="series"),
547
+ tab="Analytics",
548
+ title="Active vs inactive users",
549
+ )
550
+ def users_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
551
+ active = self.model_cls.objects.filter(is_active=True).count()
552
+ inactive = self.model_cls.objects.filter(is_active=False).count()
553
+ return WidgetActionResponseSchema(
554
+ data=[
555
+ {"label": "users", "value": active, "series": "active"},
556
+ {"label": "users", "value": inactive, "series": "inactive"},
557
+ ]
558
+ )
559
+ ```
560
+
561
+ #### SQLAlchemy
562
+
563
+ ```python
564
+ from sqlalchemy import Boolean, ForeignKey, Integer, String, Text
565
+ from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
566
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
567
+
568
+ engine = create_async_engine("sqlite+aiosqlite:///:memory:")
569
+ sessionmaker = async_sessionmaker(engine, expire_on_commit=False)
570
+
571
+
572
+ class Base(DeclarativeBase):
573
+ pass
574
+
575
+
576
+ class User(Base):
577
+ __tablename__ = "user"
578
+
579
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
580
+ username: Mapped[str] = mapped_column(String(255), unique=True)
581
+ password: Mapped[str] = mapped_column(String(255))
582
+ is_superuser: Mapped[bool] = mapped_column(Boolean, default=False)
583
+ is_active: Mapped[bool] = mapped_column(Boolean, default=True)
584
+ avatar_url: Mapped[str | None] = mapped_column(Text, nullable=True)
585
+
586
+ attachments: Mapped[list["UserAttachment"]] = relationship(back_populates="user")
587
+
588
+
589
+ class UserAttachment(Base):
590
+ __tablename__ = "user_attachment"
591
+
592
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
593
+ user_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
594
+ attachment_url: Mapped[str] = mapped_column(Text)
595
+
596
+ user: Mapped[User] = relationship(back_populates="attachments")
597
+ ```
598
+
599
+ ```python
600
+ from sqlalchemy import update
601
+
602
+ from fastadmin import (
603
+ SqlAlchemyInlineModelAdmin,
604
+ SqlAlchemyModelAdmin,
605
+ WidgetType,
606
+ action,
607
+ register,
608
+ widget_action,
609
+ )
610
+ from fastadmin.models.schemas import (
611
+ WidgetActionChartProps,
612
+ WidgetActionInputSchema,
613
+ WidgetActionResponseSchema,
614
+ WidgetActionType,
615
+ )
616
+ from .models import User, UserAttachment, sessionmaker
617
+
618
+
619
+ class UserAttachmentInline(SqlAlchemyInlineModelAdmin):
620
+ model = UserAttachment
621
+ formfield_overrides = {
622
+ "attachment_url": (WidgetType.UploadFile, {"required": True}),
623
+ }
624
+
625
+ async def upload_file(self, field_name: str, file_name: str, file_content: bytes) -> str:
626
+ # save file to media directory or to s3/filestorage here
627
+ return f"/media/{file_name}"
628
+
629
+
630
+ @register(User, sqlalchemy_sessionmaker=sessionmaker)
631
+ class UserAdmin(SqlAlchemyModelAdmin):
632
+ list_display = ("id", "username", "is_superuser", "is_active")
633
+ inlines = (UserAttachmentInline,)
634
+
635
+ formfield_overrides = {
636
+ "avatar_url": (WidgetType.UploadImage, {"required": False}),
637
+ }
638
+
639
+ actions = ("activate", "deactivate")
640
+ widget_actions = ("users_chart", "users_list")
641
+
642
+ @action(description="Activate selected users")
643
+ async def activate(self, ids):
644
+ sm = self.get_sessionmaker()
645
+ async with sm() as s:
646
+ await s.execute(update(User).where(User.id.in_(ids)).values(is_active=True))
647
+ await s.commit()
648
+
649
+ @action(description="Deactivate selected users")
650
+ async def deactivate(self, ids):
651
+ sm = self.get_sessionmaker()
652
+ async with sm() as s:
653
+ await s.execute(update(User).where(User.id.in_(ids)).values(is_active=False))
654
+ await s.commit()
655
+
656
+ async def upload_file(self, field_name: str, file_name: str, file_content: bytes) -> str:
657
+ # handle avatar_url uploads for User (and other file fields if needed)
658
+ return f"/media/{file_name}"
659
+
660
+ @widget_action(
661
+ widget_action_type=WidgetActionType.ChartBar,
662
+ widget_action_props=WidgetActionChartProps(x_field="label", y_field="value", series_field="series"),
663
+ tab="Analytics",
664
+ title="Users count",
665
+ )
666
+ async def users_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
667
+ return WidgetActionResponseSchema(
668
+ data=[
669
+ {"label": "users", "value": 42, "series": "all"},
670
+ ]
671
+ )
672
+
673
+ @widget_action(
674
+ widget_action_type=WidgetActionType.Action,
675
+ tab="Data",
676
+ title="Users list",
677
+ )
678
+ async def users_list(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
679
+ # In a real app, fetch from the DB; here it's just a static example
680
+ return WidgetActionResponseSchema(
681
+ data=[
682
+ {"id": 1, "username": "alice"},
683
+ {"id": 2, "username": "bob"},
684
+ ]
685
+ )
686
+ ```
687
+
688
+ #### Pony ORM
689
+
690
+ ```python
691
+ from pony.orm import Database, LongStr, PrimaryKey, Required, Set
692
+
693
+ db = Database()
694
+
695
+
696
+ class User(db.Entity): # type: ignore[misc]
697
+ _table_ = "user"
698
+ id = PrimaryKey(int, auto=True)
699
+ username = Required(str)
700
+ password = Required(str)
701
+ is_superuser = Required(bool, default=False)
702
+ is_active = Required(bool, default=True)
703
+ avatar_url = Required(LongStr, nullable=True)
704
+
705
+ attachments = Set("UserAttachment")
706
+
707
+
708
+ class UserAttachment(db.Entity): # type: ignore[misc]
709
+ _table_ = "user_attachment"
710
+ id = PrimaryKey(int, auto=True)
711
+ user = Required(User)
712
+ attachment_url = Required(LongStr)
713
+ ```
714
+
715
+ ```python
716
+ from pony.orm import commit, db_session
717
+
718
+ from fastadmin import (
719
+ PonyORMInlineModelAdmin,
720
+ PonyORMModelAdmin,
721
+ WidgetType,
722
+ action,
723
+ register,
724
+ widget_action,
725
+ )
726
+ from fastadmin.models.schemas import (
727
+ WidgetActionInputSchema,
728
+ WidgetActionResponseSchema,
729
+ WidgetActionType,
730
+ )
731
+ from .models import User, UserAttachment
732
+
733
+
734
+ class UserAttachmentInline(PonyORMInlineModelAdmin):
735
+ model = UserAttachment
736
+ formfield_overrides = {
737
+ "attachment_url": (WidgetType.UploadFile, {"required": True}),
738
+ }
739
+
740
+ def upload_file(self, field_name: str, file_name: str, file_content: bytes) -> str:
741
+ # save file to media directory or to s3/filestorage here
742
+ return f"/media/{file_name}"
743
+
744
+
745
+ @register(User)
746
+ class UserAdmin(PonyORMModelAdmin):
747
+ list_display = ("id", "username", "is_superuser", "is_active")
748
+ inlines = (UserAttachmentInline,)
749
+
750
+ formfield_overrides = {
751
+ "avatar_url": (WidgetType.UploadImage, {"required": False}),
752
+ }
753
+
754
+ actions = ("activate", "deactivate")
755
+ widget_actions = ("users_list", "users_chart")
756
+
757
+ @action(description="Activate selected users")
758
+ @db_session
759
+ def activate(self, ids):
760
+ for u in User.select(lambda o: o.id in ids):
761
+ u.is_active = True
762
+ commit()
763
+
764
+ @action(description="Deactivate selected users")
765
+ @db_session
766
+ def deactivate(self, ids):
767
+ for u in User.select(lambda o: o.id in ids):
768
+ u.is_active = False
769
+ commit()
770
+
771
+ def upload_file(self, field_name: str, file_name: str, file_content: bytes) -> str:
772
+ # handle avatar_url uploads for User (and other file fields if needed)
773
+ return f"/media/{file_name}"
774
+
775
+ @widget_action(widget_action_type=WidgetActionType.Action, tab="Data", title="Users list")
776
+ @db_session
777
+ def users_list(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
778
+ return WidgetActionResponseSchema(
779
+ data=[{"id": u.id, "username": u.username} for u in User.select()[:5]]
780
+ )
781
+
782
+ @widget_action(widget_action_type=WidgetActionType.ChartPie, tab="Analytics", title="Users by activity")
783
+ @db_session
784
+ def users_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
785
+ active = User.select(lambda u: u.is_active).count()
786
+ inactive = User.select(lambda u: not u.is_active).count()
787
+ return WidgetActionResponseSchema(
788
+ data=[
789
+ {"type": "active", "value": active},
790
+ {"type": "inactive", "value": inactive},
791
+ ]
792
+ )
793
+ ```
794
+
795
+ ### Request and user context in admin methods
796
+
797
+ You can access the current **request** and **authenticated user** in your admin methods via `self.request` and `self.user`. This works the same way for both `ModelAdmin` and `InlineModelAdmin`.
798
+
799
+ ```python
800
+ from fastadmin import TortoiseModelAdmin, register
801
+ from .models import Event
802
+
803
+
804
+ @register(Event)
805
+ class EventAdmin(TortoiseModelAdmin):
806
+ async def has_change_permission(self, user_id: int | None = None) -> bool:
807
+ # you can either use user_id to load the user from the DB,
808
+ # or rely on self.user – the current authenticated admin user
809
+ if self.user and self.user.get("is_superuser"):
810
+ return True
811
+ return False
812
+
813
+ async def save_model(self, id: int | None, payload: dict) -> dict:
814
+ # self.request is the current HTTP request
815
+ if self.request and getattr(self.request, "client", None):
816
+ payload["changed_from_ip"] = getattr(
817
+ self.request.client,
818
+ "host",
819
+ None,
820
+ )
821
+ return await super().save_model(id, payload)
822
+ ```
823
+
824
+ Inline admins get the same properties (`self.user`, `self.request`), so you can reuse this pattern in inline-specific hooks like `save_model` or custom `action` / `widget_action` methods.
825
+
826
+ ### Framework integration (register User admin)
827
+
828
+ #### FastAPI
829
+
830
+ ```python
831
+ from fastapi import FastAPI
832
+
833
+ from fastadmin import fastapi_app as admin_app
834
+
835
+ import myapp.admin # import to register User admin
836
+
837
+ app = FastAPI()
838
+
839
+ app.mount("/admin", admin_app)
840
+ ```
841
+
842
+ #### Django
843
+
844
+ ```python
845
+ from django.urls import path
846
+
847
+ from fastadmin import get_django_admin_urls as get_admin_urls
848
+ from fastadmin.settings import settings
849
+
850
+ import myapp.admin # imports @register(User)
851
+
852
+ urlpatterns = [
853
+ path(f"{settings.ADMIN_PREFIX}/", get_admin_urls()),
854
+ ]
855
+ ```
856
+
857
+ #### Flask
858
+
859
+ ```python
860
+ from flask import Flask
861
+
862
+ from fastadmin import flask_app as admin_app
863
+ from fastadmin.settings import settings
864
+
865
+ import myapp.admin # imports @register(User)
866
+
867
+ app = Flask(__name__)
868
+
869
+ app.register_blueprint(admin_app, url_prefix=f"/{settings.ADMIN_PREFIX}")
870
+ ```
871
+ ## Documentation
872
+
873
+ Full documentation is available at [vsdudakov.github.io/fastadmin](https://vsdudakov.github.io/fastadmin).
874
+
875
+ ## License
876
+
877
+ This project is licensed under the MIT License — see the [LICENSE](https://github.com/vsdudakov/fastadmin/blob/main/LICENSE) file for details.