Skip to content

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=["*"],
)