fastadmin 0.4.2__tar.gz → 0.4.3__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.
- fastadmin-0.4.3/PKG-INFO +877 -0
- fastadmin-0.4.3/README.md +834 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/models/schemas.py +1 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/static/index.min.js +165 -165
- {fastadmin-0.4.2 → fastadmin-0.4.3}/pyproject.toml +1 -1
- fastadmin-0.4.2/PKG-INFO +0 -808
- fastadmin-0.4.2/README.md +0 -765
- {fastadmin-0.4.2 → fastadmin-0.4.3}/LICENSE +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/__init__.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/api/__init__.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/api/exceptions.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/api/frameworks/__init__.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/api/frameworks/django/__init__.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/api/frameworks/django/app/__init__.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/api/frameworks/django/app/api.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/api/frameworks/django/app/urls.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/api/frameworks/django/app/views.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/api/frameworks/fastapi/__init__.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/api/frameworks/fastapi/api.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/api/frameworks/fastapi/app.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/api/frameworks/fastapi/views.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/api/frameworks/flask/__init__.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/api/frameworks/flask/api.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/api/frameworks/flask/app.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/api/frameworks/flask/views.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/api/helpers.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/api/schemas.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/api/service.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/models/__init__.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/models/base.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/models/decorators.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/models/helpers.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/models/orms/__init__.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/models/orms/django.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/models/orms/ponyorm.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/models/orms/sqlalchemy.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/models/orms/tortoise.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/settings.py +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/static/assets/worker-6Z7niv9l.js +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/static/assets/worker-C151k0-L.js +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/static/assets/worker-C9KPOOKD.js +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/static/assets/worker-D1fCgYB7.js +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/static/assets/worker-D3TWcJOI.js +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/static/assets/worker-DRNcaZ-V.js +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/static/assets/worker-MF2p-l5_.js +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/static/images/favicon.png +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/static/images/header-logo.svg +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/static/images/sign-in-logo.svg +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/static/index.html +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/static/index.min.css +0 -0
- {fastadmin-0.4.2 → fastadmin-0.4.3}/fastadmin/templates/index.html +0 -0
fastadmin-0.4.3/PKG-INFO
ADDED
|
@@ -0,0 +1,877 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastadmin
|
|
3
|
+
Version: 0.4.3
|
|
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
|
+
[](https://app.codecov.io/gh/vsdudakov/fastadmin)
|
|
47
|
+
[](https://github.com/vsdudakov/fastadmin/blob/master/LICENSE)
|
|
48
|
+
[](https://pypi.org/project/fastadmin/)
|
|
49
|
+
[](https://www.python.org/downloads/release/python-3120/)
|
|
50
|
+
[](https://www.python.org/downloads/release/python-3130/)
|
|
51
|
+
|
|
52
|
+
## Demo
|
|
53
|
+
|
|
54
|
+

|
|
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.
|