Registering models¶
Register a model by decorating a model admin class with @register(Model).
The admin base class must match your ORM:
| ORM | Model admin | Inline admin |
|---|---|---|
| Tortoise ORM | TortoiseModelAdmin |
TortoiseInlineModelAdmin |
| Django ORM | DjangoModelAdmin |
DjangoInlineModelAdmin |
| SQLAlchemy | SqlAlchemyModelAdmin |
SqlAlchemyInlineModelAdmin |
| Pony ORM | PonyORMModelAdmin |
PonyORMInlineModelAdmin |
| Yara ORM | YaraOrmModelAdmin |
YaraOrmInlineModelAdmin |
from fastadmin import TortoiseModelAdmin, register
from models import Tournament
@register(Tournament)
class TournamentAdmin(TortoiseModelAdmin):
list_display = ("id", "name")
Info
For SQLAlchemy, pass the async session maker when registering:
@register(Model, sqlalchemy_sessionmaker=sqlalchemy_sessionmaker).
You can also register and unregister imperatively with
register_admin_model_class(AdminCls, [Model]) and
unregister_admin_model_class([Model]).
Tip
If you mix several ORMs in one project, set model_name_prefix on the
model admin to avoid name collisions.
Complete examples¶
Each tab below is a full, runnable application from the
examples/
folder of the repository — models, admins (with inlines, actions, display
fields, uploads and dashboard widgets), authentication and app mounting.
import os
import uuid
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
os.environ["ADMIN_USER_MODEL"] = "User"
os.environ["ADMIN_USER_MODEL_USERNAME_FIELD"] = "username"
os.environ["ADMIN_SECRET_KEY"] = "secret"
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from models import BaseEvent, Event, Tournament, User, UserAttachment
from tortoise.contrib.fastapi import RegisterTortoise
from tortoise.models import Model
from fastadmin import (
TortoiseInlineModelAdmin,
TortoiseModelAdmin,
WidgetActionArgumentProps,
WidgetActionChartProps,
WidgetActionFilter,
WidgetActionInputSchema,
WidgetActionParentArgumentProps,
WidgetActionProps,
WidgetActionResponseSchema,
WidgetActionType,
WidgetType,
action,
display,
register,
widget_action,
)
from fastadmin import fastapi_app as admin_app
class UserAttachmentModelInline(TortoiseInlineModelAdmin):
model = UserAttachment
formfield_overrides = { # noqa: RUF012
"attachment_url": (
WidgetType.UploadFile,
{
"required": True,
},
),
}
async def upload_file(
self,
field_name: str,
file_name: str,
file_content: bytes,
obj: Model | None = None,
) -> str:
"""This method is used to upload files.
:params field_name: a name of field.
:params file_name: a name of file.
:params file_content: a content of file.
:params obj: the existing ORM model instance when uploading on the change page, or None on the add page.
:return: A file url.
"""
# save file to media directory or to s3/filestorage here
# return a full url to the file
return f"https://fastadmin.io/media/{file_name}"
@register(User)
class UserModelAdmin(TortoiseModelAdmin):
menu_section = "Users"
list_display = ("id", "username", "is_superuser")
list_display_links = ("id", "username")
list_filter = ("id", "username", "is_superuser")
search_fields = ("username",)
formfield_overrides = { # noqa: RUF012
"username": (WidgetType.SlugInput, {"required": True}),
"password": (WidgetType.PasswordInput, {"passwordModalForm": True}),
"avatar_url": (
WidgetType.UploadImage,
{
"required": False,
# Disable crop image for upload field
# "disableCropImage": True,
},
),
}
inlines = (UserAttachmentModelInline,)
widget_actions = (
"sales_chart",
"sales_area_chart",
"sales_column_chart",
"sales_bar_chart",
"sales_pie_chart",
"sales_action",
)
async def authenticate(self, username: str, password: str) -> uuid.UUID | int | None:
obj = await self.model_cls.filter(username=username, password=password, is_superuser=True).first()
if not obj:
return None
return obj.id
async def change_password(self, id: uuid.UUID | int, password: str) -> None:
user = await self.model_cls.filter(id=id).first()
if not user:
return
# direct saving password is only for tests - use hash
user.password = password
await user.save()
async def upload_file(
self,
field_name: str,
file_name: str,
file_content: bytes,
obj: Model | None = None,
) -> str:
"""This method is used to upload files.
:params field_name: a name of field.
:params file_name: a name of file.
:params file_content: a content of file.
:params obj: the existing ORM model instance when uploading on the change page, or None on the add page.
:return: A file url.
"""
# save file to media directory or to s3/filestorage here
return f"/media/{file_name}"
async def pre_generate_models_schema(self) -> None:
model_cls: User = self.model_cls
options = await model_cls.all().values_list("username", flat=True)
# inject options to the class
widget_action_props: WidgetActionProps = self.__class__.sales_action.widget_action_props
for argument in widget_action_props.arguments:
if argument.name == "username":
argument.widget_props["options"] = [{"label": option, "value": option} for option in options]
break
@widget_action(
widget_action_type=WidgetActionType.ChartLine,
widget_action_props=WidgetActionChartProps(
x_field="x",
y_field="y",
series_field="series",
series_color={
"Sales": "#1677ff",
"Sales 2": "#52c41a",
},
),
widget_action_filters=[
WidgetActionFilter(
field_name="x",
widget_type=WidgetType.DatePicker,
),
WidgetActionFilter(
field_name="y",
widget_type=WidgetType.Select,
widget_props={
"options": [
{"label": "Sales", "value": "sales"},
{"label": "Revenue", "value": "revenue"},
],
},
),
],
tab="Analytics",
sub_tab="Sales",
title="Sales over time",
description="Line chart of sales",
width=24,
)
async def sales_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{
"x": "2026-01-01",
"y": 100,
"series": "Sales",
},
{
"x": "2026-01-02",
"y": 200,
"series": "Sales",
},
{
"x": "2026-01-01",
"y": 300,
"series": "Sales 2",
},
{
"x": "2026-01-04",
"y": 400,
"series": "Sales 2",
},
],
)
@widget_action(
widget_action_type=WidgetActionType.ChartArea,
widget_action_props=WidgetActionChartProps(
x_field="x",
y_field="y",
series_field="series",
series_color={
"Online": "#722ed1",
"Retail": "#13c2c2",
},
),
widget_action_filters=[
WidgetActionFilter(
field_name="period",
widget_type=WidgetType.RangePicker,
),
WidgetActionFilter(
field_name="channel",
widget_type=WidgetType.Select,
widget_props={
"mode": "multiple",
"options": [
{"label": "Online", "value": "Online"},
{"label": "Retail", "value": "Retail"},
],
},
),
],
tab="Analytics",
title="Sales trend area",
description="Area chart of sales",
width=12,
)
async def sales_area_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{"x": "2026-01-01", "y": 80, "series": "Online"},
{"x": "2026-01-02", "y": 120, "series": "Online"},
{"x": "2026-01-01", "y": 60, "series": "Retail"},
{"x": "2026-01-02", "y": 90, "series": "Retail"},
],
)
@widget_action(
widget_action_type=WidgetActionType.ChartColumn,
widget_action_props=WidgetActionChartProps(
x_field="x",
y_field="y",
series_field="series",
series_color={
"2025": "#fa8c16",
"2026": "#f5222d",
},
),
widget_action_filters=[
WidgetActionFilter(
field_name="year",
widget_type=WidgetType.Select,
widget_props={
"options": [
{"label": "2025", "value": "2025"},
{"label": "2026", "value": "2026"},
],
},
),
],
tab="Analytics",
title="Sales by month",
description="Column chart of sales",
width=12,
)
async def sales_column_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{"x": "Jan", "y": 320, "series": "2025"},
{"x": "Feb", "y": 410, "series": "2025"},
{"x": "Jan", "y": 380, "series": "2026"},
{"x": "Feb", "y": 460, "series": "2026"},
],
)
@widget_action(
widget_action_type=WidgetActionType.ChartBar,
widget_action_props=WidgetActionChartProps(
x_field="x",
y_field="y",
series_field="series",
series_color={
"Q1": "#2f54eb",
"Q2": "#eb2f96",
},
),
widget_action_filters=[
WidgetActionFilter(
field_name="quarter",
widget_type=WidgetType.Select,
widget_props={
"options": [
{"label": "Q1", "value": "Q1"},
{"label": "Q2", "value": "Q2"},
],
},
),
],
tab="Analytics",
title="Sales by region",
description="Bar chart of sales",
width=12,
)
async def sales_bar_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{"x": "North", "y": 540, "series": "Q1"},
{"x": "South", "y": 420, "series": "Q1"},
{"x": "North", "y": 610, "series": "Q2"},
{"x": "South", "y": 480, "series": "Q2"},
],
)
@widget_action(
widget_action_type=WidgetActionType.ChartPie,
widget_action_props=WidgetActionChartProps(
x_field="x",
y_field="y",
series_color=[
"#1677ff",
"#52c41a",
"#faad14",
],
),
widget_action_filters=[
WidgetActionFilter(
field_name="month",
widget_type=WidgetType.DatePicker,
),
],
tab="Analytics",
title="Sales share",
description="Pie chart of sales share",
width=12,
)
async def sales_pie_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{"x": "Online", "y": 45},
{"x": "Retail", "y": 30},
{"x": "Partners", "y": 25},
],
)
@widget_action(
widget_action_type=WidgetActionType.Action,
widget_action_props=WidgetActionProps(
arguments=[
# Example of using AsyncSelect widget with parentModel
WidgetActionArgumentProps(
name="user_id",
widget_type=WidgetType.AsyncSelect,
widget_props={
"required": True,
"parentModel": "User",
"idField": "id",
"labelFields": ["__str__", "id"],
},
),
# Example of using Select widget with dynamically loaded options
WidgetActionArgumentProps(
name="username",
widget_type=WidgetType.Select,
widget_props={
"required": True,
# dynamically load options from the database in pre_generate_models_schema method
"options": [],
},
),
# Example of using parent argument with filtered children arguments
WidgetActionArgumentProps(
name="type",
widget_type=WidgetType.Select,
widget_props={
"required": True,
"options": [
{"label": "Sales", "value": "sales"},
{"label": "Revenue", "value": "revenue"},
],
},
),
WidgetActionArgumentProps(
name="sales_date",
widget_type=WidgetType.DatePicker,
widget_props={
"required": True,
},
parent_argument=WidgetActionParentArgumentProps(
name="type",
value="sales",
),
),
WidgetActionArgumentProps(
name="revenue_date",
widget_type=WidgetType.DatePicker,
widget_props={
"required": True,
},
parent_argument=WidgetActionParentArgumentProps(
name="type",
value="revenue",
),
),
],
),
tab="Data",
title="Get sales data",
description="Get sales data",
width=12,
)
async def sales_action(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{
"id": 1,
"name": "Sales",
},
{
"id": 2,
"name": "Sales",
},
{
"id": 3,
"name": "Sales",
},
],
)
class EventInlineModelAdmin(TortoiseInlineModelAdmin):
model = Event
@register(Tournament)
class TournamentModelAdmin(TortoiseModelAdmin):
list_display = ("id", "name")
inlines = (EventInlineModelAdmin,)
widget_actions = ("tournament_action",)
@widget_action(
widget_action_type=WidgetActionType.Action,
tab="Data",
title="Get tournament data",
description="Get tournament data",
width=12,
)
async def tournament_action(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{
"id": 1,
"name": "Tournament 1",
},
{
"id": 2,
"name": "Tournament 2",
},
{
"id": 3,
"name": "Tournament 3",
},
],
)
@register(BaseEvent)
class BaseEventModelAdmin(TortoiseModelAdmin):
pass
@register(Event)
class EventModelAdmin(TortoiseModelAdmin):
actions = ("make_is_active", "make_is_not_active")
list_display = (
"id",
"tournament_name",
"name_with_price",
"rating",
"event_type",
"is_active",
"started",
"start_time",
"date",
)
list_filter = ("event_type", "is_active", "start_time", "date", "event_type")
search_fields = ("name", "tournament__name")
list_select_related = ("tournament",)
@action(description="Make event active")
async def make_is_active(self, ids):
await self.model_cls.filter(id__in=ids).update(is_active=True)
@action
async def make_is_not_active(self, ids):
await self.model_cls.filter(id__in=ids).update(is_active=False)
@display
async def started(self, obj):
return bool(obj.start_time)
@display
async def tournament_name(self, obj):
tournament = await obj.tournament
return tournament.name
@display()
async def name_with_price(self, obj):
return f"{obj.name} - {obj.price}"
async def create_superuser():
await User.create(
username="admin",
password="admin",
is_superuser=True,
)
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
# RegisterTortoise sets up TortoiseContext so request handlers can use the DB
# (_enable_global_fallback=True by default for ASGI lifespan in a background task)
async with RegisterTortoise(
app=app,
db_url="sqlite://:memory:",
modules={"models": ["models"]},
generate_schemas=True,
use_tz=False,
timezone="UTC",
):
await create_superuser()
yield
app = FastAPI(lifespan=lifespan)
app.mount("/admin", admin_app)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3030", "http://localhost:8090"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
import asyncio
import uuid
from django.db import models
from fastadmin import (
DjangoInlineModelAdmin,
DjangoModelAdmin,
WidgetActionArgumentProps,
WidgetActionChartProps,
WidgetActionFilter,
WidgetActionInputSchema,
WidgetActionParentArgumentProps,
WidgetActionProps,
WidgetActionResponseSchema,
WidgetActionType,
WidgetType,
action,
display,
register,
widget_action,
)
EventTypeEnum = (
("PRIVATE", "PRIVATE"),
("PUBLIC", "PUBLIC"),
)
class BaseModel(models.Model):
id = models.AutoField(primary_key=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class User(BaseModel):
username = models.CharField(max_length=255)
password = models.CharField(max_length=255)
is_superuser = models.BooleanField(default=False)
avatar_url = models.ImageField(null=True)
attachment_url = models.FileField()
def __str__(self):
return self.username
class Meta:
db_table = "user"
class Tournament(BaseModel):
name = models.CharField(max_length=255)
def __str__(self):
return self.name
class Meta:
db_table = "tournament"
class BaseEvent(BaseModel):
name = models.CharField(max_length=255)
def __str__(self):
return self.name
class Meta:
db_table = "base_event"
class Event(BaseModel):
base = models.OneToOneField(BaseEvent, related_name="event", null=True, on_delete=models.SET_NULL)
name = models.CharField(max_length=255)
tournament = models.ForeignKey(Tournament, related_name="events", on_delete=models.CASCADE)
participants = models.ManyToManyField(User, related_name="events")
rating = models.IntegerField(default=0)
description = models.TextField(null=True)
event_type = models.CharField(max_length=255, default="PUBLIC", choices=EventTypeEnum)
is_active = models.BooleanField(default=True)
start_time = models.TimeField(null=True)
date = models.DateField(null=True)
latitude = models.FloatField(null=True)
longitude = models.FloatField(null=True)
price = models.DecimalField(max_digits=10, decimal_places=2, null=True)
json = models.JSONField(null=True)
def __str__(self):
return self.name
class Meta:
db_table = "event"
@register(User)
class UserModelAdmin(DjangoModelAdmin):
menu_section = "Users"
list_display = ("id", "username", "is_superuser")
list_display_links = ("id", "username")
list_filter = ("id", "username", "is_superuser")
search_fields = ("username",)
formfield_overrides = { # noqa: RUF012
"username": (WidgetType.SlugInput, {"required": True}),
"password": (WidgetType.PasswordInput, {"passwordModalForm": True}),
}
widget_actions = (
"sales_chart",
"sales_area_chart",
"sales_column_chart",
"sales_bar_chart",
"sales_pie_chart",
"sales_action",
)
def authenticate(self, username: str, password: str) -> uuid.UUID | int | None:
obj = self.model_cls.objects.filter(username=username, is_superuser=True).first()
if not obj:
return None
# if not obj.check_password(password):
# return None
return obj.id
def change_password(self, id: uuid.UUID | int, password: str) -> None:
user = self.model_cls.objects.filter(id=id).first()
if not user:
return
# direct saving password is only for tests - use hash
user.password = password
user.save()
def upload_file(
self,
field_name: str,
file_name: str,
file_content: bytes,
obj: models.Model | None = None,
) -> str:
"""This method is used to upload files.
:params field_name: a name of field.
:params file_name: a name of file.
:params file_content: a content of file.
:params obj: the existing ORM model instance when uploading on the change page,
or None when uploading on the add (create) page.
:return: A file url.
"""
# save file to media directory or to s3/filestorage here
# return a full url to the file
return f"https://fastadmin.io/media/{file_name}"
async def pre_generate_models_schema(self) -> None:
def get_options() -> list:
return list(self.model_cls.objects.values_list("username", flat=True))
options = await asyncio.to_thread(get_options)
widget_action_props: WidgetActionProps = self.__class__.sales_action.widget_action_props
for argument in widget_action_props.arguments:
if argument.name == "username":
argument.widget_props["options"] = [{"label": option, "value": option} for option in options]
break
@widget_action(
widget_action_type=WidgetActionType.ChartLine,
widget_action_props=WidgetActionChartProps(
x_field="x",
y_field="y",
series_field="series",
series_color={
"Sales": "#1677ff",
"Sales 2": "#52c41a",
},
),
widget_action_filters=[
WidgetActionFilter(
field_name="x",
widget_type=WidgetType.DatePicker,
),
WidgetActionFilter(
field_name="y",
widget_type=WidgetType.Select,
widget_props={
"options": [
{"label": "Sales", "value": "sales"},
{"label": "Revenue", "value": "revenue"},
],
},
),
],
tab="Analytics",
sub_tab="Sales",
title="Sales over time",
description="Line chart of sales",
width=24,
)
def sales_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{"x": "2026-01-01", "y": 100, "series": "Sales"},
{"x": "2026-01-02", "y": 200, "series": "Sales"},
{"x": "2026-01-01", "y": 300, "series": "Sales 2"},
{"x": "2026-01-04", "y": 400, "series": "Sales 2"},
],
)
@widget_action(
widget_action_type=WidgetActionType.ChartArea,
widget_action_props=WidgetActionChartProps(
x_field="x",
y_field="y",
series_field="series",
series_color={
"Online": "#722ed1",
"Retail": "#13c2c2",
},
),
widget_action_filters=[
WidgetActionFilter(
field_name="period",
widget_type=WidgetType.RangePicker,
),
WidgetActionFilter(
field_name="channel",
widget_type=WidgetType.Select,
widget_props={
"mode": "multiple",
"options": [
{"label": "Online", "value": "Online"},
{"label": "Retail", "value": "Retail"},
],
},
),
],
tab="Analytics",
title="Sales trend area",
description="Area chart of sales",
width=12,
)
def sales_area_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{"x": "2026-01-01", "y": 80, "series": "Online"},
{"x": "2026-01-02", "y": 120, "series": "Online"},
{"x": "2026-01-01", "y": 60, "series": "Retail"},
{"x": "2026-01-02", "y": 90, "series": "Retail"},
],
)
@widget_action(
widget_action_type=WidgetActionType.ChartColumn,
widget_action_props=WidgetActionChartProps(
x_field="x",
y_field="y",
series_field="series",
series_color={
"2025": "#fa8c16",
"2026": "#f5222d",
},
),
widget_action_filters=[
WidgetActionFilter(
field_name="year",
widget_type=WidgetType.Select,
widget_props={
"options": [
{"label": "2025", "value": "2025"},
{"label": "2026", "value": "2026"},
],
},
),
],
tab="Analytics",
title="Sales by month",
description="Column chart of sales",
width=12,
)
def sales_column_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{"x": "Jan", "y": 320, "series": "2025"},
{"x": "Feb", "y": 410, "series": "2025"},
{"x": "Jan", "y": 380, "series": "2026"},
{"x": "Feb", "y": 460, "series": "2026"},
],
)
@widget_action(
widget_action_type=WidgetActionType.ChartBar,
widget_action_props=WidgetActionChartProps(
x_field="x",
y_field="y",
series_field="series",
series_color={
"Q1": "#2f54eb",
"Q2": "#eb2f96",
},
),
widget_action_filters=[
WidgetActionFilter(
field_name="quarter",
widget_type=WidgetType.Select,
widget_props={
"options": [
{"label": "Q1", "value": "Q1"},
{"label": "Q2", "value": "Q2"},
],
},
),
],
tab="Analytics",
title="Sales by region",
description="Bar chart of sales",
width=12,
)
def sales_bar_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{"x": "North", "y": 540, "series": "Q1"},
{"x": "South", "y": 420, "series": "Q1"},
{"x": "North", "y": 610, "series": "Q2"},
{"x": "South", "y": 480, "series": "Q2"},
],
)
@widget_action(
widget_action_type=WidgetActionType.ChartPie,
widget_action_props=WidgetActionChartProps(
x_field="x",
y_field="y",
series_color=[
"#1677ff",
"#52c41a",
"#faad14",
],
),
widget_action_filters=[
WidgetActionFilter(
field_name="month",
widget_type=WidgetType.DatePicker,
),
],
tab="Analytics",
title="Sales share",
description="Pie chart of sales share",
width=12,
)
def sales_pie_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{"x": "Online", "y": 45},
{"x": "Retail", "y": 30},
{"x": "Partners", "y": 25},
],
)
@widget_action(
widget_action_type=WidgetActionType.Action,
widget_action_props=WidgetActionProps(
arguments=[
# Example of using AsyncSelect widget with parentModel
WidgetActionArgumentProps(
name="user_id",
widget_type=WidgetType.AsyncSelect,
widget_props={
"required": True,
"parentModel": "User",
"idField": "id",
"labelFields": ["__str__", "id"],
},
),
# Example of using Select widget with dynamically loaded options
WidgetActionArgumentProps(
name="username",
widget_type=WidgetType.Select,
widget_props={
"required": True,
# dynamically load options from the database in pre_generate_models_schema method
"options": [],
},
),
# Example of using parent argument with filtered children arguments
WidgetActionArgumentProps(
name="type",
widget_type=WidgetType.Select,
widget_props={
"required": True,
"options": [
{"label": "Sales", "value": "sales"},
{"label": "Revenue", "value": "revenue"},
],
},
),
WidgetActionArgumentProps(
name="sales_date",
widget_type=WidgetType.DatePicker,
widget_props={
"required": True,
},
parent_argument=WidgetActionParentArgumentProps(
name="type",
value="sales",
),
),
WidgetActionArgumentProps(
name="revenue_date",
widget_type=WidgetType.DatePicker,
widget_props={
"required": True,
},
parent_argument=WidgetActionParentArgumentProps(
name="type",
value="revenue",
),
),
],
),
tab="Data",
title="Get sales data",
description="Get sales data",
width=12,
)
def sales_action(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{"id": 1, "name": "Sales"},
{"id": 2, "name": "Sales"},
{"id": 3, "name": "Sales"},
],
)
class EventInlineModelAdmin(DjangoInlineModelAdmin):
model = Event
@register(Tournament)
class TournamentModelAdmin(DjangoModelAdmin):
list_display = ("id", "name")
inlines = (EventInlineModelAdmin,)
@register(BaseEvent)
class BaseEventModelAdmin(DjangoModelAdmin):
pass
@register(Event)
class EventModelAdmin(DjangoModelAdmin):
actions = ("make_is_active", "make_is_not_active")
list_display = ("id", "tournament", "name_with_price", "rating", "event_type", "is_active", "started")
list_filter = ("tournament", "event_type", "is_active")
search_fields = ("name", "tournament__name")
@action(description="Make event active")
def make_is_active(self, ids):
self.model_cls.objects.filter(id__in=ids).update(is_active=True)
@action
def make_is_not_active(self, ids):
self.model_cls.objects.filter(id__in=ids).update(is_active=False)
@display
def started(self, obj):
return bool(obj.start_time)
@display()
def name_with_price(self, obj):
return f"{obj.name} - {obj.price}"
import os
import uuid
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
os.environ["ADMIN_USER_MODEL"] = "User"
os.environ["ADMIN_USER_MODEL_USERNAME_FIELD"] = "username"
os.environ["ADMIN_SECRET_KEY"] = "secret"
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from models import Base, BaseEvent, Event, Tournament, User, sqlalchemy_engine, sqlalchemy_sessionmaker
from sqlalchemy import select, update
from fastadmin import (
SqlAlchemyInlineModelAdmin,
SqlAlchemyModelAdmin,
WidgetActionArgumentProps,
WidgetActionChartProps,
WidgetActionFilter,
WidgetActionInputSchema,
WidgetActionParentArgumentProps,
WidgetActionProps,
WidgetActionResponseSchema,
WidgetActionType,
WidgetType,
action,
display,
register,
widget_action,
)
from fastadmin import fastapi_app as admin_app
@register(User, sqlalchemy_sessionmaker=sqlalchemy_sessionmaker)
class UserModelAdmin(SqlAlchemyModelAdmin):
menu_section = "Users"
list_display = ("id", "username", "is_superuser")
list_display_links = ("id", "username")
list_filter = ("id", "username", "is_superuser")
search_fields = ("username",)
formfield_overrides = { # noqa: RUF012
"username": (WidgetType.SlugInput, {"required": True}),
"password": (WidgetType.PasswordInput, {"passwordModalForm": True}),
"avatar_url": (
WidgetType.UploadImage,
{
"required": False,
# Disable crop image for upload field
# "disableCropImage": True,
},
),
"attachment_url": (
WidgetType.UploadFile,
{
"required": True,
},
),
}
widget_actions = (
"sales_chart",
"sales_area_chart",
"sales_column_chart",
"sales_bar_chart",
"sales_pie_chart",
"sales_action",
)
async def authenticate(self, username: str, password: str) -> uuid.UUID | int | None:
sessionmaker = self.get_sessionmaker()
async with sessionmaker() as session:
query = select(self.model_cls).filter_by(username=username, password=password, is_superuser=True)
result = await session.scalars(query)
obj = result.first()
if not obj:
return None
return obj.id
async def change_password(self, id: uuid.UUID | int, password: str) -> None:
sessionmaker = self.get_sessionmaker()
async with sessionmaker() as session:
# use hash password for real usage
query = update(self.model_cls).where(User.id.in_([id])).values(password=password)
await session.execute(query)
await session.commit()
async def upload_file(
self,
field_name: str,
file_name: str,
file_content: bytes,
obj: Base | None = None,
) -> str:
"""This method is used to upload files.
:params field_name: a name of field.
:params file_name: a name of file.
:params file_content: a content of file.
:params obj: the existing ORM model instance when uploading on the change page, or None when on the add page.
:return: A file url.
"""
# save file to media directory or to s3/filestorage here
# return a full url to the file
return f"https://fastadmin.io/media/{file_name}"
async def pre_generate_models_schema(self) -> None:
sessionmaker = self.get_sessionmaker()
async with sessionmaker() as session:
result = await session.execute(select(self.model_cls.username))
options = list(result.scalars().all())
widget_action_props: WidgetActionProps = self.__class__.sales_action.widget_action_props
for argument in widget_action_props.arguments:
if argument.name == "username":
argument.widget_props["options"] = [{"label": option, "value": option} for option in options]
break
@widget_action(
widget_action_type=WidgetActionType.ChartLine,
widget_action_props=WidgetActionChartProps(
x_field="x",
y_field="y",
series_field="series",
series_color={
"Sales": "#1677ff",
"Sales 2": "#52c41a",
},
),
widget_action_filters=[
WidgetActionFilter(
field_name="x",
widget_type=WidgetType.DatePicker,
),
WidgetActionFilter(
field_name="y",
widget_type=WidgetType.Select,
widget_props={
"options": [
{"label": "Sales", "value": "sales"},
{"label": "Revenue", "value": "revenue"},
],
},
),
],
tab="Analytics",
sub_tab="Sales",
title="Sales over time",
description="Line chart of sales",
width=24,
)
async def sales_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{"x": "2026-01-01", "y": 100, "series": "Sales"},
{"x": "2026-01-02", "y": 200, "series": "Sales"},
{"x": "2026-01-01", "y": 300, "series": "Sales 2"},
{"x": "2026-01-04", "y": 400, "series": "Sales 2"},
],
)
@widget_action(
widget_action_type=WidgetActionType.ChartArea,
widget_action_props=WidgetActionChartProps(
x_field="x",
y_field="y",
series_field="series",
series_color={
"Online": "#722ed1",
"Retail": "#13c2c2",
},
),
widget_action_filters=[
WidgetActionFilter(
field_name="period",
widget_type=WidgetType.RangePicker,
),
WidgetActionFilter(
field_name="channel",
widget_type=WidgetType.Select,
widget_props={
"mode": "multiple",
"options": [
{"label": "Online", "value": "Online"},
{"label": "Retail", "value": "Retail"},
],
},
),
],
tab="Analytics",
title="Sales trend area",
description="Area chart of sales",
width=12,
)
async def sales_area_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{"x": "2026-01-01", "y": 80, "series": "Online"},
{"x": "2026-01-02", "y": 120, "series": "Online"},
{"x": "2026-01-01", "y": 60, "series": "Retail"},
{"x": "2026-01-02", "y": 90, "series": "Retail"},
],
)
@widget_action(
widget_action_type=WidgetActionType.ChartColumn,
widget_action_props=WidgetActionChartProps(
x_field="x",
y_field="y",
series_field="series",
series_color={
"2025": "#fa8c16",
"2026": "#f5222d",
},
),
widget_action_filters=[
WidgetActionFilter(
field_name="year",
widget_type=WidgetType.Select,
widget_props={
"options": [
{"label": "2025", "value": "2025"},
{"label": "2026", "value": "2026"},
],
},
),
],
tab="Analytics",
title="Sales by month",
description="Column chart of sales",
width=12,
)
async def sales_column_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{"x": "Jan", "y": 320, "series": "2025"},
{"x": "Feb", "y": 410, "series": "2025"},
{"x": "Jan", "y": 380, "series": "2026"},
{"x": "Feb", "y": 460, "series": "2026"},
],
)
@widget_action(
widget_action_type=WidgetActionType.ChartBar,
widget_action_props=WidgetActionChartProps(
x_field="x",
y_field="y",
series_field="series",
series_color={
"Q1": "#2f54eb",
"Q2": "#eb2f96",
},
),
widget_action_filters=[
WidgetActionFilter(
field_name="quarter",
widget_type=WidgetType.Select,
widget_props={
"options": [
{"label": "Q1", "value": "Q1"},
{"label": "Q2", "value": "Q2"},
],
},
),
],
tab="Analytics",
title="Sales by region",
description="Bar chart of sales",
width=12,
)
async def sales_bar_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{"x": "North", "y": 540, "series": "Q1"},
{"x": "South", "y": 420, "series": "Q1"},
{"x": "North", "y": 610, "series": "Q2"},
{"x": "South", "y": 480, "series": "Q2"},
],
)
@widget_action(
widget_action_type=WidgetActionType.ChartPie,
widget_action_props=WidgetActionChartProps(
x_field="x",
y_field="y",
series_color=[
"#1677ff",
"#52c41a",
"#faad14",
],
),
widget_action_filters=[
WidgetActionFilter(
field_name="month",
widget_type=WidgetType.DatePicker,
),
],
tab="Analytics",
title="Sales share",
description="Pie chart of sales share",
width=12,
)
async def sales_pie_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{"x": "Online", "y": 45},
{"x": "Retail", "y": 30},
{"x": "Partners", "y": 25},
],
)
@widget_action(
widget_action_type=WidgetActionType.Action,
widget_action_props=WidgetActionProps(
arguments=[
# Example of using AsyncSelect widget with parentModel
WidgetActionArgumentProps(
name="user_id",
widget_type=WidgetType.AsyncSelect,
widget_props={
"required": True,
"parentModel": "User",
"idField": "id",
"labelFields": ["__str__", "id"],
},
),
# Example of using Select widget with dynamically loaded options
WidgetActionArgumentProps(
name="username",
widget_type=WidgetType.Select,
widget_props={
"required": True,
# dynamically load options from the database in pre_generate_models_schema method
"options": [],
},
),
# Example of using parent argument with filtered children arguments
WidgetActionArgumentProps(
name="type",
widget_type=WidgetType.Select,
widget_props={
"required": True,
"options": [
{"label": "Sales", "value": "sales"},
{"label": "Revenue", "value": "revenue"},
],
},
),
WidgetActionArgumentProps(
name="sales_date",
widget_type=WidgetType.DatePicker,
widget_props={
"required": True,
},
parent_argument=WidgetActionParentArgumentProps(
name="type",
value="sales",
),
),
WidgetActionArgumentProps(
name="revenue_date",
widget_type=WidgetType.DatePicker,
widget_props={
"required": True,
},
parent_argument=WidgetActionParentArgumentProps(
name="type",
value="revenue",
),
),
],
),
tab="Data",
title="Get sales data",
description="Get sales data",
width=12,
)
async def sales_action(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{"id": 1, "name": "Sales"},
{"id": 2, "name": "Sales"},
{"id": 3, "name": "Sales"},
],
)
class EventInlineModelAdmin(SqlAlchemyInlineModelAdmin):
model = Event
@register(Tournament, sqlalchemy_sessionmaker=sqlalchemy_sessionmaker)
class TournamentModelAdmin(SqlAlchemyModelAdmin):
list_display = ("id", "name")
inlines = (EventInlineModelAdmin,)
@register(BaseEvent, sqlalchemy_sessionmaker=sqlalchemy_sessionmaker)
class BaseEventModelAdmin(SqlAlchemyModelAdmin):
pass
@register(Event, sqlalchemy_sessionmaker=sqlalchemy_sessionmaker)
class EventModelAdmin(SqlAlchemyModelAdmin):
actions = ("make_is_active", "make_is_not_active")
list_display = ("id", "tournament", "name_with_price", "rating", "event_type", "is_active", "started")
list_filter = ("tournament", "event_type", "is_active")
search_fields = ("name", "tournament__name")
@action(description="Make event active")
async def make_is_active(self, ids):
sessionmaker = self.get_sessionmaker()
async with sessionmaker() as session:
query = update(Event).where(Event.id.in_(ids)).values(is_active=True)
await session.execute(query)
await session.commit()
@action
async def make_is_not_active(self, ids):
sessionmaker = self.get_sessionmaker()
async with sessionmaker() as session:
query = update(Event).where(Event.id.in_(ids)).values(is_active=False)
await session.execute(query)
await session.commit()
@display
async def started(self, obj):
return bool(obj.start_time)
@display()
async def name_with_price(self, obj):
return f"{obj.name} - {obj.price}"
async def init_db():
async with sqlalchemy_engine.begin() as c:
await c.run_sync(Base.metadata.drop_all)
await c.run_sync(Base.metadata.create_all)
async def create_superuser():
async with sqlalchemy_sessionmaker() as s:
user = User(
username="admin",
password="admin",
is_superuser=True,
attachment_url="/media/attachment.txt",
)
s.add(user)
await s.commit()
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
await init_db()
await create_superuser()
yield
app = FastAPI(lifespan=lifespan)
app.mount("/admin", admin_app)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3030", "http://localhost:8090"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
import asyncio
import os
import uuid
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
os.environ["ADMIN_USER_MODEL"] = "User"
os.environ["ADMIN_USER_MODEL_USERNAME_FIELD"] = "username"
os.environ["ADMIN_SECRET_KEY"] = "secret"
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from models import BaseEvent, Event, Tournament, User, db
from pony.orm import commit, db_session
from pony.orm import select as pony_select
from fastadmin import (
PonyORMInlineModelAdmin,
PonyORMModelAdmin,
WidgetActionArgumentProps,
WidgetActionChartProps,
WidgetActionFilter,
WidgetActionInputSchema,
WidgetActionParentArgumentProps,
WidgetActionProps,
WidgetActionResponseSchema,
WidgetActionType,
WidgetType,
action,
display,
register,
widget_action,
)
from fastadmin import fastapi_app as admin_app
@register(User)
class UserModelAdmin(PonyORMModelAdmin):
menu_section = "Users"
list_display = ("id", "username", "is_superuser")
list_display_links = ("id", "username")
list_filter = ("id", "username", "is_superuser")
search_fields = ("username",)
formfield_overrides = { # noqa: RUF012
"username": (WidgetType.SlugInput, {"required": True}),
"password": (WidgetType.PasswordInput, {"passwordModalForm": True}),
"avatar_url": (
WidgetType.UploadImage,
{
"required": False,
# Disable crop image for upload field
# "disableCropImage": True,
},
),
"attachment_url": (
WidgetType.UploadFile,
{
"required": True,
},
),
}
widget_actions = (
"sales_chart",
"sales_area_chart",
"sales_column_chart",
"sales_bar_chart",
"sales_pie_chart",
"sales_action",
)
@db_session
def authenticate(self, username: str, password: str) -> uuid.UUID | int | None:
obj = next((f for f in User.select(username=username, password=password, is_superuser=True)), None) # fmt: skip
if not obj:
return None
return obj.id
@db_session
def change_password(self, id: uuid.UUID | int, password: str) -> None:
obj = next((f for f in self.model_cls.select(id=id)), None)
if not obj:
return
# direct saving password is only for tests - use hash
obj.password = password
commit()
def upload_file(
self,
field_name: str,
file_name: str,
file_content: bytes,
obj: db.Entity | None = None,
) -> str:
"""This method is used to upload files.
:params field_name: a name of field.
:params file_name: a name of file.
:params file_content: a content of file.
:params obj: an orm/db model object. None on the add page, the existing instance on the change page.
:return: A file url.
"""
# save file to media directory or to s3/filestorage here
# return a full url to the file
return f"https://fastadmin.io/media/{file_name}"
async def pre_generate_models_schema(self) -> None:
def get_options() -> list:
with db_session:
return [u.username for u in pony_select(u for u in User)]
options = await asyncio.to_thread(get_options)
widget_action_props: WidgetActionProps = self.__class__.sales_action.widget_action_props
for argument in widget_action_props.arguments:
if argument.name == "username":
argument.widget_props["options"] = [{"label": option, "value": option} for option in options]
break
@widget_action(
widget_action_type=WidgetActionType.ChartLine,
widget_action_props=WidgetActionChartProps(
x_field="x",
y_field="y",
series_field="series",
series_color={
"Sales": "#1677ff",
"Sales 2": "#52c41a",
},
),
widget_action_filters=[
WidgetActionFilter(
field_name="x",
widget_type=WidgetType.DatePicker,
),
WidgetActionFilter(
field_name="y",
widget_type=WidgetType.Select,
widget_props={
"options": [
{"label": "Sales", "value": "sales"},
{"label": "Revenue", "value": "revenue"},
],
},
),
],
tab="Analytics",
sub_tab="Sales",
title="Sales over time",
description="Line chart of sales",
width=24,
)
def sales_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{"x": "2026-01-01", "y": 100, "series": "Sales"},
{"x": "2026-01-02", "y": 200, "series": "Sales"},
{"x": "2026-01-01", "y": 300, "series": "Sales 2"},
{"x": "2026-01-04", "y": 400, "series": "Sales 2"},
],
)
@widget_action(
widget_action_type=WidgetActionType.ChartArea,
widget_action_props=WidgetActionChartProps(
x_field="x",
y_field="y",
series_field="series",
series_color={
"Online": "#722ed1",
"Retail": "#13c2c2",
},
),
widget_action_filters=[
WidgetActionFilter(
field_name="period",
widget_type=WidgetType.RangePicker,
),
WidgetActionFilter(
field_name="channel",
widget_type=WidgetType.Select,
widget_props={
"mode": "multiple",
"options": [
{"label": "Online", "value": "Online"},
{"label": "Retail", "value": "Retail"},
],
},
),
],
tab="Analytics",
title="Sales trend area",
description="Area chart of sales",
width=12,
)
def sales_area_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{"x": "2026-01-01", "y": 80, "series": "Online"},
{"x": "2026-01-02", "y": 120, "series": "Online"},
{"x": "2026-01-01", "y": 60, "series": "Retail"},
{"x": "2026-01-02", "y": 90, "series": "Retail"},
],
)
@widget_action(
widget_action_type=WidgetActionType.ChartColumn,
widget_action_props=WidgetActionChartProps(
x_field="x",
y_field="y",
series_field="series",
series_color={
"2025": "#fa8c16",
"2026": "#f5222d",
},
),
widget_action_filters=[
WidgetActionFilter(
field_name="year",
widget_type=WidgetType.Select,
widget_props={
"options": [
{"label": "2025", "value": "2025"},
{"label": "2026", "value": "2026"},
],
},
),
],
tab="Analytics",
title="Sales by month",
description="Column chart of sales",
width=12,
)
def sales_column_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{"x": "Jan", "y": 320, "series": "2025"},
{"x": "Feb", "y": 410, "series": "2025"},
{"x": "Jan", "y": 380, "series": "2026"},
{"x": "Feb", "y": 460, "series": "2026"},
],
)
@widget_action(
widget_action_type=WidgetActionType.ChartBar,
widget_action_props=WidgetActionChartProps(
x_field="x",
y_field="y",
series_field="series",
series_color={
"Q1": "#2f54eb",
"Q2": "#eb2f96",
},
),
widget_action_filters=[
WidgetActionFilter(
field_name="quarter",
widget_type=WidgetType.Select,
widget_props={
"options": [
{"label": "Q1", "value": "Q1"},
{"label": "Q2", "value": "Q2"},
],
},
),
],
tab="Analytics",
title="Sales by region",
description="Bar chart of sales",
width=12,
)
def sales_bar_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{"x": "North", "y": 540, "series": "Q1"},
{"x": "South", "y": 420, "series": "Q1"},
{"x": "North", "y": 610, "series": "Q2"},
{"x": "South", "y": 480, "series": "Q2"},
],
)
@widget_action(
widget_action_type=WidgetActionType.ChartPie,
widget_action_props=WidgetActionChartProps(
x_field="x",
y_field="y",
series_color=[
"#1677ff",
"#52c41a",
"#faad14",
],
),
widget_action_filters=[
WidgetActionFilter(
field_name="month",
widget_type=WidgetType.DatePicker,
),
],
tab="Analytics",
title="Sales share",
description="Pie chart of sales share",
width=12,
)
def sales_pie_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{"x": "Online", "y": 45},
{"x": "Retail", "y": 30},
{"x": "Partners", "y": 25},
],
)
@widget_action(
widget_action_type=WidgetActionType.Action,
widget_action_props=WidgetActionProps(
arguments=[
# Example of using AsyncSelect widget with parentModel
WidgetActionArgumentProps(
name="user_id",
widget_type=WidgetType.AsyncSelect,
widget_props={
"required": True,
"parentModel": "User",
"idField": "id",
"labelFields": ["__str__", "id"],
},
),
# Example of using Select widget with dynamically loaded options
WidgetActionArgumentProps(
name="username",
widget_type=WidgetType.Select,
widget_props={
"required": True,
# dynamically load options from the database in pre_generate_models_schema method
"options": [],
},
),
# Example of using parent argument with filtered children arguments
WidgetActionArgumentProps(
name="type",
widget_type=WidgetType.Select,
widget_props={
"required": True,
"options": [
{"label": "Sales", "value": "sales"},
{"label": "Revenue", "value": "revenue"},
],
},
),
WidgetActionArgumentProps(
name="sales_date",
widget_type=WidgetType.DatePicker,
widget_props={
"required": True,
},
parent_argument=WidgetActionParentArgumentProps(
name="type",
value="sales",
),
),
WidgetActionArgumentProps(
name="revenue_date",
widget_type=WidgetType.DatePicker,
widget_props={
"required": True,
},
parent_argument=WidgetActionParentArgumentProps(
name="type",
value="revenue",
),
),
],
),
tab="Data",
title="Get sales data",
description="Get sales data",
width=12,
)
def sales_action(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{"id": 1, "name": "Sales"},
{"id": 2, "name": "Sales"},
{"id": 3, "name": "Sales"},
],
)
class EventInlineModelAdmin(PonyORMInlineModelAdmin):
model = Event
@register(Tournament)
class TournamentModelAdmin(PonyORMModelAdmin):
list_display = ("id", "name")
inlines = (EventInlineModelAdmin,)
@register(BaseEvent)
class BaseEventModelAdmin(PonyORMModelAdmin):
pass
@register(Event)
class EventModelAdmin(PonyORMModelAdmin):
actions = ("make_is_active", "make_is_not_active")
list_display = ("id", "tournament", "name_with_price", "rating", "event_type", "is_active", "started")
list_filter = ("tournament", "event_type", "is_active")
search_fields = ("name", "tournament__name")
@action(description="Make event active")
@db_session
def make_is_active(self, ids):
# update(o.set(is_active=True) for o in self.model_cls if o.id in ids)
objs = self.model_cls.select(lambda o: o.id in ids)
for obj in objs:
obj.is_active = True
commit()
@action
@db_session
def make_is_not_active(self, ids):
# update(o.set(is_active=False) for o in self.model_cls if o.id in ids)
objs = self.model_cls.select(lambda o: o.id in ids)
for obj in objs:
obj.is_active = False
commit()
@display
@db_session
def started(self, obj):
return bool(obj.start_time)
@display()
@db_session
def name_with_price(self, obj):
return f"{obj.name} - {obj.price}"
def init_db():
# Use shared in-memory sqlite DB so tables are visible across connections/threads.
db.bind(provider="sqlite", filename=":sharedmemory:", create_db=True)
db.generate_mapping(create_tables=True)
@db_session
def create_superuser():
User(
username="admin",
password="admin",
is_superuser=True,
attachment_url="/media/attachment.txt",
)
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
init_db()
create_superuser()
yield
app = FastAPI(lifespan=lifespan)
app.mount("/admin", admin_app)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3030", "http://localhost:8090"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
import os
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
os.environ["ADMIN_USER_MODEL"] = "User"
os.environ["ADMIN_USER_MODEL_USERNAME_FIELD"] = "username"
os.environ["ADMIN_SECRET_KEY"] = "secret"
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from models import BaseEvent, Event, Tournament, User
from yara_orm import Model, YaraOrm
from fastadmin import (
WidgetActionChartProps,
WidgetActionFilter,
WidgetActionInputSchema,
WidgetActionResponseSchema,
WidgetActionType,
WidgetType,
YaraOrmInlineModelAdmin,
YaraOrmModelAdmin,
action,
display,
register,
widget_action,
)
from fastadmin import fastapi_app as admin_app
@register(User)
class UserModelAdmin(YaraOrmModelAdmin):
menu_section = "Users"
list_display = ("id", "username", "is_superuser")
list_display_links = ("id", "username")
list_filter = ("id", "username", "is_superuser")
search_fields = ("username",)
formfield_overrides = { # noqa: RUF012
"username": (WidgetType.SlugInput, {"required": True}),
"password": (WidgetType.PasswordInput, {"passwordModalForm": True}),
"avatar_url": (WidgetType.UploadImage, {"required": False}),
}
widget_actions = ("sales_chart",)
async def authenticate(self, username: str, password: str) -> int | None:
obj = await self.model_cls.filter(username=username, password=password, is_superuser=True).first()
if not obj:
return None
return obj.id
async def change_password(self, id: int, password: str) -> None:
user = await self.model_cls.filter(id=id).first()
if not user:
return
# direct saving password is only for the example - hash it in production
user.password = password
await user.save()
async def upload_file(
self,
field_name: str,
file_name: str,
file_content: bytes,
obj: Model | None = None,
) -> str:
# save file to a media directory or to s3/filestorage here
return f"/media/{file_name}"
@widget_action(
widget_action_type=WidgetActionType.ChartLine,
widget_action_props=WidgetActionChartProps(x_field="x", y_field="y", series_field="series"),
widget_action_filters=[
WidgetActionFilter(field_name="x", widget_type=WidgetType.DatePicker),
],
tab="Analytics",
title="Sales over time",
description="Line chart of sales",
width=24,
)
async def sales_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
return WidgetActionResponseSchema(
data=[
{"x": "2026-01-01", "y": 100, "series": "Sales"},
{"x": "2026-01-02", "y": 200, "series": "Sales"},
],
)
class EventInlineModelAdmin(YaraOrmInlineModelAdmin):
model = Event
@register(Tournament)
class TournamentModelAdmin(YaraOrmModelAdmin):
list_display = ("id", "name")
list_display_links = ("id", "name")
search_fields = ("name",)
inlines = (EventInlineModelAdmin,)
@register(BaseEvent)
class BaseEventModelAdmin(YaraOrmModelAdmin):
list_display = ("id", "name")
@register(Event)
class EventModelAdmin(YaraOrmModelAdmin):
list_display = ("id", "name", "tournament", "is_active", "started")
list_display_links = ("id", "name")
list_filter = ("id", "name", "event_type", "is_active", "tournament")
list_select_related = ("tournament",)
search_fields = ("name",)
actions = ("make_is_active", "make_is_not_active")
@action(description="Make selected events active")
async def make_is_active(self, ids):
await self.model_cls.filter(id__in=ids).update(is_active=True)
@action(description="Make selected events inactive")
async def make_is_not_active(self, ids):
await self.model_cls.filter(id__in=ids).update(is_active=False)
@display
async def started(self, obj):
return bool(obj.start_time)
@display()
async def name_with_price(self, obj):
return f"{obj.name} - {obj.price}"
async def create_superuser() -> None:
if not await User.filter(username="admin").first():
await User.create(username="admin", password="admin", is_superuser=True)
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
await YaraOrm.init("sqlite://:memory:")
await YaraOrm.generate_schemas()
await create_superuser()
yield
await YaraOrm.close()
app = FastAPI(lifespan=lifespan)
app.mount("/admin", admin_app)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3030", "http://localhost:8090"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)