-
Notifications
You must be signed in to change notification settings - Fork 35
Expand file tree
/
Copy pathindex.html
More file actions
1 lines (1 loc) · 63.8 KB
/
index.html
File metadata and controls
1 lines (1 loc) · 63.8 KB
1
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1.0, shrink-to-fit=no" /><meta name="google-site-verification" content="c45rQ5FmAgrb6scHcK2hORmVz_8MwX6HRXrKpYu3vMw" /><link href="assets/images/favicon.png" rel="icon" /><title>FastAdmin | Documentation</title><meta name="description" content="FastAdmin is an easy-to-use Admin Dashboard App for FastAPI/Django/Flask inspired by Django Admin." /><meta name="author" content="vsdudakov@gmail.com" /><link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.3/css/bootstrap.min.css" /><link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github-dark.min.css" /><link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css" /><link rel="stylesheet" type="text/css" href="assets/css/stylesheet.css" /></head><body data-spy="scroll" data-target=".app-navigation" data-offset="125"><div class="preloader"><div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div></div><div id="main-wrapper"><header id="header" class="sticky-top"><nav class="primary-menu navbar navbar-expand-lg navbar-dropdown-dark"><div class="container-fluid"><button id="sidebarCollapse" class="navbar-toggler d-block d-md-none" type="button" ><span></span><span class="w-75"></span><span class="w-50"></span></button><a class="logo ml-md-3" href="index.html" title="FastAdmin"><img src="assets/images/header-logo.svg" alt="FastAdmin" width="50" /><h4 class="title">FastAdmin</h4></a><ul class="social-icons social-icons-sm ml-lg-2 mr-2"><li class="social-icons-github"><a href="https://pypi.org/project/fastadmin/" target="_blank" title="" data-original-title="github" ><i class="fa-solid fa-house"></i ></a></li><li class="social-icons-github"><a href="https://github.com/vsdudakov/fastadmin" target="_blank" title="" data-original-title="github" ><i class="fab fa-github"></i ></a></li><!-- <li class="social-icons-twitter"><a data-toggle="tooltip" href="" target="_blank" title="" data-original-title="Twitter"><i class="fab fa-twitter"></i></a></li><li class="social-icons-facebook"><a data-toggle="tooltip" href="" target="_blank" title="" data-original-title="Facebook"><i class="fab fa-facebook-f"></i></a></li><li class="social-icons-dribbble"><a data-toggle="tooltip" href="" target="_blank" title="" data-original-title="Dribbble"><i class="fab fa-dribbble"></i></a></li> --></ul></div></nav></header><div id="content" role="main"><div class="app-navigation bg-light"><ul class="nav flex-column"><li class="nav-item"><a class="nav-link" href="#introduction">Introduction</a></li><li class="nav-item"><a class="nav-link" href="#getting_started">Getting Started</a><ul class="nav flex-column"><li class="nav-item"><a class="nav-link" href="#installation">Installation</a></li><li class="nav-item"><a class="nav-link" href="#quick_tutorial">Quick Tutorial</a></li></ul></li><li class="nav-item"><a class="nav-link" href="#settings">Settings</a></li><li class="nav-item"><a class="nav-link" href="#dashboard-widget-admins">Dashboard Widget Admins</a><ul class="nav flex-column"><li class="nav-item"><a class="nav-link" href="#registering-widgets">Registering Widgets</a></li><li class="nav-item"><a class="nav-link" href="#widget-methods-and-attributes">Methods and Attributes</a></li><li class="nav-item"><a class="nav-link" href="#widget-chart-types">Chart Types</a></li></ul></li><li class="nav-item"><a class="nav-link" href="#model-admins">Model Admins</a><ul class="nav flex-column"><li class="nav-item"><a class="nav-link" href="#registering-models">Registering Models</a></li><li class="nav-item"><a class="nav-link" href="#authentication">Authentication</a></li><li class="nav-item"><a class="nav-link" href="#model-methods-and-attributes">Methods and Attributes</a></li><li class="nav-item"><a class="nav-link" href="#model-form-field-types">Form Field Types</a></li></ul></li><li class="nav-item"><a class="nav-link" href="#inline-admins">Inline Model Admins</a><ul class="nav flex-column"><li class="nav-item"><a class="nav-link" href="#registering-inlines">Registering Inlines</a></li><li class="nav-item"><a class="nav-link" href="#inline-methods-and-attributes">Methods and Attributes</a></li></ul></li><li class="nav-item"><a class="nav-link" href="#changelog">Changelog</a><ul class="nav flex-column"><li class="nav-item"><a class="nav-link" href="#v0_2_20">v0.2.20</a></li><li class="nav-item"><a class="nav-link" href="#v0_2_19">v0.2.19</a></li><li class="nav-item"><a class="nav-link" href="#v0_2_18">v0.2.18</a></li><li class="nav-item"><a class="nav-link" href="#v0_2_17">v0.2.17</a></li><li class="nav-item"><a class="nav-link" href="#v0_2_16">v0.2.16</a></li><li class="nav-item"><a class="nav-link" href="#v0_2_15">v0.2.15</a></li><li class="nav-item"><a class="nav-link" href="#v0_2_14">v0.2.14</a></li><li class="nav-item"><a class="nav-link" href="#v0_2_13">v0.2.13</a></li><li class="nav-item"><a class="nav-link" href="#v0_2_12">v0.2.12</a></li><li class="nav-item"><a class="nav-link" href="#v0_2_11">v0.2.11</a></li><li class="nav-item"><a class="nav-link" href="#v0_2_10">v0.2.10</a></li><li class="nav-item"><a class="nav-link" href="#v0_2_9">v0.2.9</a></li><li class="nav-item"><a class="nav-link" href="#v0_2_8">v0.2.8</a></li><li class="nav-item"><a class="nav-link" href="#v0_2_7">v0.2.7</a></li><li class="nav-item"><a class="nav-link" href="#v0_2_6">v0.2.6</a></li><li class="nav-item"><a class="nav-link" href="#v0_2_5">v0.2.5</a></li><li class="nav-item"><a class="nav-link" href="#v0_2_4">v0.2.4</a></li><li class="nav-item"><a class="nav-link" href="#v0_2_3">v0.2.3</a></li><li class="nav-item"><a class="nav-link" href="#v0_2_2">v0.2.2</a></li><li class="nav-item"><a class="nav-link" href="#v0_2_1">v0.2.1</a></li><li class="nav-item"><a class="nav-link" href="#v0_2_0">v0.2.0</a></li></ul></li></ul></div><div class="app-content"><div class="container"><section id="index"><h1>FastAdmin | Documentation</h1><div class="row"><div class="col-sm-6 col-lg-4"><ul class="list-unstyled"><li><strong>Version:</strong> 0.2.20</li><li><strong>Author:</strong><a href="mailto:vsdudakov@gmail.com" target="_blank"> Seva D. </a></li></ul></div><div class="col-sm-6 col-lg-4"><ul class="list-unstyled"><li><strong class="font-weight-700">Created:</strong> 7 March 2023 </li><li><strong>Updated:</strong> 15 April 2025 </li></ul></div></div></section><hr class="divider" /><section id="introduction"><h2>Introduction</h2><p class="text-4"><a href='https://github.com/vsdudakov/fastadmin' target='_blank'>FastAdmin</a> is an easy-to-use Admin Dashboard App for FastAPI/Django/Flask inspired by Django Admin. </p><p class="text-4"> FastAdmin was built with relations in mind and admiration for the excellent and popular Django Admin. It's engraved in its design that you may configure your admin dashboard for FastAPI/Django/Flask easiest way. </p><p class="text-4"> FastAdmin is designed to be minimalistic, functional and yet familiar. </p></section><hr class="small-divider" /><section id="getting_started"><h2>Getting Started</h2><p class="alert alert-info"> If you have any questions that are beyond the scope of the documentation, Please feel free to email <a href='mailto:vsdudakov@gmail.com' target='_blank'>us</a>. </p><section id="installation"><h3>Installation</h3><p class="lead"> Follow the steps below to setup FastAdmin: </p><p class="text-4"> Install the package using pip: </p><p class="alert alert-info"> Note: For zsh and macos use: <code>pip install fastadmin[fastapi,django]</code></p><pre><code class="language-bash"> pip install fastadmin[fastapi,django] # for fastapi with django orm pip install fastadmin[fastapi,tortoise-orm] # for fastapi with tortoise orm pip install fastadmin[fastapi,pony] # for fastapi with pony orm pip install fastadmin[fastapi,sqlalchemy] # for fastapi with sqlalchemy orm pip install fastadmin[django] # for django with django orm pip install fastadmin[django,pony] # for django with pony orm pip install fastadmin[flask,sqlalchemy] # for flask with sqlalchemy </code></pre><p class="text-4"> Install the package using poetry: </p><pre><code class="language-bash"> poetry add 'fastadmin[fastapi,django]' # for fastapi with django orm poetry add 'fastadmin[fastapi,tortoise-orm]' # for fastapi with tortoise orm poetry add 'fastadmin[fastapi,pony]' # for fastapi with pony orm poetry add 'fastadmin[fastapi,sqlalchemy]' # for fastapi with sqlalchemy orm poetry add 'fastadmin[django]' # for django with django orm poetry add 'fastadmin[django,pony]' # for django with pony orm poetry add 'fastadmin[flask,sqlalchemy]' # for flask with sqlalchemy </code></pre><p class="text-4"> Configure required settings using virtual environment variables: </p><p class="alert alert-info"> Note: You can add these variables to .env and use python-dotenv to load them. See all settings <a href='https://vsdudakov.github.io/fastadmin#settings'>here</a></p><pre><code class="language-bash"> export ADMIN_USER_MODEL=User export ADMIN_USER_MODEL_USERNAME_FIELD=username export ADMIN_SECRET_KEY=secret_key </code></pre></section><section id="quick_tutorial"><h3>Quick Tutorial</h3><p class="lead"> Setup FastAdmin for a framework </p><ul class="nav nav-tabs" id="setup_framework" role="tablist"><li class="nav-item" role="presentation"><button class="nav-link active" id="fastapi" data-bs-toggle="tab" data-bs-target="#fastapi-pane" type="button" role="tab" aria-controls="fastapi" aria-selected="true">FastAPI</button></li><li class="nav-item" role="presentation"><button class="nav-link " id="django" data-bs-toggle="tab" data-bs-target="#django-pane" type="button" role="tab" aria-controls="django" aria-selected="false">Django</button></li><li class="nav-item" role="presentation"><button class="nav-link " id="flask" data-bs-toggle="tab" data-bs-target="#flask-pane" type="button" role="tab" aria-controls="flask" aria-selected="false">Flask</button></li></ul><div class="tab-content" id="setup_framework-content"><div class="tab-pane fade show active" id="fastapi-pane" role="tabpanel" aria-labelledby="fastapi" tabindex="0"><pre><code class="language-python"> from fastapi import FastAPI from fastadmin import fastapi_app as admin_app app = FastAPI() app.mount("/admin", admin_app) </code></pre></div><div class="tab-pane fade " id="django-pane" role="tabpanel" aria-labelledby="django" tabindex="0"><pre><code class="language-python"> from django.urls import path from fastadmin import get_django_admin_urls as get_admin_urls from fastadmin.settings import settings urlpatterns = [ path(f"{settings.ADMIN_PREFIX}/", get_admin_urls()), ] </code></pre></div><div class="tab-pane fade " id="flask-pane" role="tabpanel" aria-labelledby="flask" tabindex="0"><pre><code class="language-python"> from flask import Flask from fastadmin import flask_app as admin_app app = Flask(__name__) app.register_blueprint(admin_app, url_prefix="/admin") </code></pre></div></div><p class="lead"> Register ORM models </p><ul class="nav nav-tabs" id="register_orm_models" role="tablist"><li class="nav-item" role="presentation"><button class="nav-link active" id="tortoise_orm" data-bs-toggle="tab" data-bs-target="#tortoise_orm-pane" type="button" role="tab" aria-controls="tortoise_orm" aria-selected="true">Tortoise ORM</button></li><li class="nav-item" role="presentation"><button class="nav-link " id="django_orm" data-bs-toggle="tab" data-bs-target="#django_orm-pane" type="button" role="tab" aria-controls="django_orm" aria-selected="false">Django ORM</button></li><li class="nav-item" role="presentation"><button class="nav-link " id="sql_alchemy" data-bs-toggle="tab" data-bs-target="#sql_alchemy-pane" type="button" role="tab" aria-controls="sql_alchemy" aria-selected="false">SQL Alchemy</button></li><li class="nav-item" role="presentation"><button class="nav-link " id="pony_orm" data-bs-toggle="tab" data-bs-target="#pony_orm-pane" type="button" role="tab" aria-controls="pony_orm" aria-selected="false">Pony ORM</button></li></ul><div class="tab-content" id="register_orm_models-content"><div class="tab-pane fade show active" id="tortoise_orm-pane" role="tabpanel" aria-labelledby="tortoise_orm" tabindex="0"><pre><code class="language-python"> from uuid import UUID import bcrypt from tortoise import fields from tortoise.models import Model from fastadmin import TortoiseModelAdmin, register class User(Model): username = fields.CharField(max_length=255, unique=True) hash_password = fields.CharField(max_length=255) is_superuser = fields.BooleanField(default=False) is_active = fields.BooleanField(default=False) def __str__(self): return self.username @register(User) class UserAdmin(TortoiseModelAdmin): exclude = ("hash_password",) list_display = ("id", "username", "is_superuser", "is_active") list_display_links = ("id", "username") list_filter = ("id", "username", "is_superuser", "is_active") search_fields = ("username",) async def authenticate(self, username: str, password: str) -> UUID | int | None: user = await User.filter(username=username, is_superuser=True).first() if not user: return None if not bcrypt.checkpw(password.encode(), user.hash_password.encode()): return None return user.id </code></pre></div><div class="tab-pane fade " id="django_orm-pane" role="tabpanel" aria-labelledby="django_orm" tabindex="0"><pre><code class="language-python"> from django.db import models from fastadmin import DjangoModelAdmin, register class User(models.Model): username = models.CharField(max_length=255, unique=True) hash_password = models.CharField(max_length=255) is_superuser = models.BooleanField(default=False) is_active = models.BooleanField(default=False) def __str__(self): return self.username @register(User) class UserAdmin(DjangoModelAdmin): exclude = ("hash_password",) list_display = ("id", "username", "is_superuser", "is_active") list_display_links = ("id", "username") list_filter = ("id", "username", "is_superuser", "is_active") search_fields = ("username",) def authenticate(self, username, password): obj = User.objects.filter(username=username, is_superuser=True).first() if not obj: return None if not obj.check_password(password): return None return obj.id </code></pre></div><div class="tab-pane fade " id="sql_alchemy-pane" role="tabpanel" aria-labelledby="sql_alchemy" tabindex="0"><pre><code class="language-python"> import bcrypt from sqlalchemy import Boolean, Integer, String, select from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from fastadmin import SqlAlchemyModelAdmin, register sqlalchemy_engine = create_async_engine( "sqlite+aiosqlite:///:memory:", echo=True, ) sqlalchemy_sessionmaker = async_sessionmaker(sqlalchemy_engine, expire_on_commit=False) class Base(DeclarativeBase): pass class User(Base): __tablename__ = "user" id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False) username: Mapped[str] = mapped_column(String(length=255), nullable=False) hash_password: Mapped[str] = mapped_column(String(length=255), nullable=False) is_superuser: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) is_active: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) def __str__(self): return self.username @register(User, sqlalchemy_sessionmaker=sqlalchemy_sessionmaker) class UserAdmin(SqlAlchemyModelAdmin): exclude = ("hash_password",) list_display = ("id", "username", "is_superuser", "is_active") list_display_links = ("id", "username") list_filter = ("id", "username", "is_superuser", "is_active") search_fields = ("username",) async def authenticate(self, username, password): sessionmaker = self.get_sessionmaker() async with sessionmaker() as session: query = select(User).filter_by(username=username, password=password, is_superuser=True) result = await session.scalars(query) user = result.first() if not user: return None if not bcrypt.checkpw(password.encode(), user.hash_password.encode()): return None return user.id </code></pre></div><div class="tab-pane fade " id="pony_orm-pane" role="tabpanel" aria-labelledby="pony_orm" tabindex="0"><pre><code class="language-python"> import bcrypt from pony.orm import Database, PrimaryKey, Required, db_session from fastadmin import PonyORMModelAdmin, register db = Database() db.bind(provider="sqlite", filename=":memory:", create_db=True) class User(db.Entity): # type: ignore [name-defined] _table_ = "user" id = PrimaryKey(int, auto=True) username = Required(str) hash_password = Required(str) is_superuser = Required(bool, default=False) is_active = Required(bool, default=False) def __str__(self): return self.username @register(User) class UserAdmin(PonyORMModelAdmin): exclude = ("hash_password",) list_display = ("id", "username", "is_superuser", "is_active") list_display_links = ("id", "username") list_filter = ("id", "username", "is_superuser", "is_active") search_fields = ("username",) @db_session def authenticate(self, username, password): user = next((f for f in self.model_cls.select(username=username, password=password, is_superuser=True)), None) if not user: return None if not bcrypt.checkpw(password.encode(), user.hash_password.encode()): return None return user.id </code></pre></div></div></section></section><hr class="small-divider" /><section id="settings"><h2>Settings</h2><p class="text-4"> There are settings with default values: </p><p class="alert alert-info"> Note: Export virtual environment variables or create <code>.env</code> file with variables and use <code>python-dotenv</code> package. </p><pre><code class="language-python"> class Settings: """Settings""" # This value is the prefix you used for mounting FastAdmin app for FastAPI. ADMIN_PREFIX: str = os.getenv("ADMIN_PREFIX", "admin") # This value is the site name on sign-in page and on header. ADMIN_SITE_NAME: str = os.getenv("ADMIN_SITE_NAME", "FastAdmin") # This value is the logo path on sign-in page. ADMIN_SITE_SIGN_IN_LOGO: str = os.getenv("ADMIN_SITE_SIGN_IN_LOGO", "/admin/static/images/sign-in-logo.svg") # This value is the logo path on header. ADMIN_SITE_HEADER_LOGO: str = os.getenv("ADMIN_SITE_HEADER_LOGO", "/admin/static/images/header-logo.svg") # This value is the favicon path. ADMIN_SITE_FAVICON: str = os.getenv("ADMIN_SITE_FAVICON", "/admin/static/images/favicon.png") # This value is the primary color for FastAdmin. ADMIN_PRIMARY_COLOR: str = os.getenv("ADMIN_PRIMARY_COLOR", "#009485") # This value is the session id key to store session id in http only cookies. ADMIN_SESSION_ID_KEY: str = os.getenv("ADMIN_SESSION_ID_KEY", "admin_session_id") # This value is the expired_at period (in sec) for session id. ADMIN_SESSION_EXPIRED_AT: int = os.getenv("ADMIN_SESSION_EXPIRED_AT", 144000) # in sec # This value is the date format for JS widgets. ADMIN_DATE_FORMAT: str = os.getenv("ADMIN_DATE_FORMAT", "YYYY-MM-DD") # This value is the datetime format for JS widgets. ADMIN_DATETIME_FORMAT: str = os.getenv("ADMIN_DATETIME_FORMAT", "YYYY-MM-DD HH:mm") # This value is the time format for JS widgets. ADMIN_TIME_FORMAT: str = os.getenv("ADMIN_TIME_FORMAT", "HH:mm:ss") # This value is the name for User db/orm model class for authentication. ADMIN_USER_MODEL: str = os.getenv("ADMIN_USER_MODEL") # This value is the username field for User db/orm model for for authentication. ADMIN_USER_MODEL_USERNAME_FIELD: str = os.getenv("ADMIN_USER_MODEL_USERNAME_FIELD") # This value is the key to securing signed data - it is vital you keep this secure, # or attackers could use it to generate their own signed values. ADMIN_SECRET_KEY: str = os.getenv("ADMIN_SECRET_KEY") # This value disables the crop image feature in FastAdmin. ADMIN_DISABLE_CROP_IMAGE: bool = os.getenv("ADMIN_DISABLE_CROP_IMAGE", False) </code></pre><p class="alert alert-warning"> Note: Settings without default values are required. </p></section><hr class="small-divider" /><section id="dashboard-widget-admins"><h2>Dashboard Widget Admins</h2><section id="registering-widgets"><h3>Registering Widgets</h3><p class="lead"> Register Dashboard widgets </p><ul class="nav nav-tabs" id="register_dashboard_widgets" role="tablist"><li class="nav-item" role="presentation"><button class="nav-link active" id="dashboard_tortoise_orm" data-bs-toggle="tab" data-bs-target="#dashboard_tortoise_orm-pane" type="button" role="tab" aria-controls="dashboard_tortoise_orm" aria-selected="true">Tortoise ORM</button></li><li class="nav-item" role="presentation"><button class="nav-link " id="dashboard_django_orm" data-bs-toggle="tab" data-bs-target="#dashboard_django_orm-pane" type="button" role="tab" aria-controls="dashboard_django_orm" aria-selected="false">Django ORM</button></li><li class="nav-item" role="presentation"><button class="nav-link " id="dashboard_sql_alchemy" data-bs-toggle="tab" data-bs-target="#dashboard_sql_alchemy-pane" type="button" role="tab" aria-controls="dashboard_sql_alchemy" aria-selected="false">SQL Alchemy</button></li><li class="nav-item" role="presentation"><button class="nav-link " id="dashboard_pony_orm" data-bs-toggle="tab" data-bs-target="#dashboard_pony_orm-pane" type="button" role="tab" aria-controls="dashboard_pony_orm" aria-selected="false">Pony ORM</button></li></ul><div class="tab-content" id="register_dashboard_widgets-content"><div class="tab-pane fade show active" id="dashboard_tortoise_orm-pane" role="tabpanel" aria-labelledby="dashboard_tortoise_orm" tabindex="0"><pre><code class="language-python"> import datetime from tortoise import Tortoise, fields from tortoise.models import Model from fastadmin import DashboardWidgetAdmin, DashboardWidgetType, WidgetType, register_widget class DashboardUser(Model): username = fields.CharField(max_length=255, unique=True) hash_password = fields.CharField(max_length=255) is_superuser = fields.BooleanField(default=False) is_active = fields.BooleanField(default=False) def __str__(self): return self.username @register_widget class UsersDashboardWidgetAdmin(DashboardWidgetAdmin): title = "Users" dashboard_widget_type = DashboardWidgetType.ChartLine x_field = "date" x_field_filter_widget_type = WidgetType.DatePicker x_field_filter_widget_props: dict[str, str] = {"picker": "month"} # noqa: RUF012 x_field_periods = ["day", "week", "month", "year"] # noqa: RUF012 y_field = "count" async def get_data( self, min_x_field: str | None = None, max_x_field: str | None = None, period_x_field: str | None = None, ) -> dict: conn = Tortoise.get_connection("default") if not min_x_field: min_x_field_date = datetime.datetime.now(tz=datetime.UTC) - datetime.timedelta(days=360) else: min_x_field_date = datetime.datetime.fromisoformat(min_x_field) if not max_x_field: max_x_field_date = datetime.datetime.now(tz=datetime.UTC) + datetime.timedelta(days=1) else: max_x_field_date = datetime.datetime.fromisoformat(max_x_field) if not period_x_field or period_x_field not in (self.x_field_periods or []): period_x_field = "month" results = await conn.execute_query_dict( """ SELECT to_char(date_trunc($1, "user"."created_at")::date, 'dd/mm/yyyy') "date", COUNT("user"."id") "count" FROM "user" WHERE "user"."created_at" >= $2 AND "user"."created_at" <= $3 GROUP BY "date" ORDER BY "date" """, [period_x_field, min_x_field_date, max_x_field_date], ) return { "results": results, "min_x_field": min_x_field_date.isoformat(), "max_x_field": max_x_field_date.isoformat(), "period_x_field": period_x_field, } </code></pre></div><div class="tab-pane fade " id="dashboard_django_orm-pane" role="tabpanel" aria-labelledby="dashboard_django_orm" tabindex="0"><pre><code class="language-python"> import datetime from django.db import connection, models from fastadmin import DashboardWidgetAdmin, DashboardWidgetType, WidgetType, register_widget class DashboardUser(models.Model): username = models.CharField(max_length=255, unique=True) hash_password = models.CharField(max_length=255) is_superuser = models.BooleanField(default=False) is_active = models.BooleanField(default=False) def __str__(self): return self.username @register_widget class UsersDashboardWidgetAdmin(DashboardWidgetAdmin): title = "Users" dashboard_widget_type = DashboardWidgetType.ChartLine x_field = "date" x_field_filter_widget_type = WidgetType.DatePicker x_field_filter_widget_props: dict[str, str] = {"picker": "month"} # noqa: RUF012 x_field_periods = ["day", "week", "month", "year"] # noqa: RUF012 y_field = "count" def get_data( # type: ignore [override] self, min_x_field: str | None = None, max_x_field: str | None = None, period_x_field: str | None = None, ) -> dict: def dictfetchall(cursor): columns = [col[0] for col in cursor.description] return [dict(zip(columns, row, strict=True)) for row in cursor.fetchall()] with connection.cursor() as c: if not min_x_field: min_x_field_date = datetime.datetime.now(tz=datetime.UTC) - datetime.timedelta(days=360) else: min_x_field_date = datetime.datetime.fromisoformat(min_x_field) if not max_x_field: max_x_field_date = datetime.datetime.now(tz=datetime.UTC) + datetime.timedelta(days=1) else: max_x_field_date = datetime.datetime.fromisoformat(max_x_field) if not period_x_field or period_x_field not in (self.x_field_periods or []): period_x_field = "month" c.execute( """ SELECT to_char(date_trunc($1, "user"."created_at")::date, 'dd/mm/yyyy') "date", COUNT("user"."id") "count" FROM "user" WHERE "user"."created_at" >= $2 AND "user"."created_at" <= $3 GROUP BY "date" ORDER BY "date" """, [period_x_field, min_x_field_date, max_x_field_date], ) results = dictfetchall(c) return { "results": results, "min_x_field": min_x_field_date.isoformat(), "max_x_field": max_x_field_date.isoformat(), "period_x_field": period_x_field, } </code></pre></div><div class="tab-pane fade " id="dashboard_sql_alchemy-pane" role="tabpanel" aria-labelledby="dashboard_sql_alchemy" tabindex="0"><pre><code class="language-python"> See example for Tortoise ORM </code></pre></div><div class="tab-pane fade " id="dashboard_pony_orm-pane" role="tabpanel" aria-labelledby="dashboard_pony_orm" tabindex="0"><pre><code class="language-python"> See example for Tortoise ORM </code></pre></div></div></section><section id="widget-methods-and-attributes"><h3>Methods and Attributes</h3><p class="text-4"> There are methods and attributes for Dashboard Widget Admin: </p><pre><code class="language-python"> class DashboardWidgetAdmin: title: str dashboard_widget_type: DashboardWidgetType x_field: str y_field: str | None = None series_field: str | None = None x_field_filter_widget_type: WidgetType | None = None x_field_filter_widget_props: dict[str, Any] | None = None x_field_periods: list[str] | None = None async def get_data( self, min_x_field: str | None = None, max_x_field: str | None = None, period_x_field: str | None = None, ) -> dict[str, Any]: """This method is used to get data for dashboard widget. :params min_x_field: A minimum value for x_field. :params max_x_field: A maximum value for x_field. :params period_x_field: A period value for x_field. :return: A dict with data. """ raise NotImplementedError </code></pre><p class="alert alert-warning"> Note: Please see <a href='https://ant-design-charts.antgroup.com/en/examples' target='_blank'>antd charts</a> for <code>x_field_filter_widget_props</code>. </p></section><section id="widget-chart-types"><h3>Chart Types</h3><p class="text-4"> There are widget types which fastadmin dashboard supports: </p><pre><code class="language-python"> class DashboardWidgetType(str, Enum): """Dashboard Widget type""" ChartLine = "ChartLine" ChartArea = "ChartArea" ChartColumn = "ChartColumn" ChartBar = "ChartBar" ChartPie = "ChartPie" </code></pre><p class="alert alert-warning"> Note: Please see <a href='=https://ant-design-charts.antgroup.com/en/examples' target='_blank'>antd charts</a> for more details (e.g. to see how they look like). </p></section></section><hr class="small-divider" /><section id="model-admins"><h2>Model Admins</h2><section id="registering-models"><h3>Registering Models</h3><ul class="nav nav-tabs" id="register_models" role="tablist"><li class="nav-item" role="presentation"><button class="nav-link active" id="models_tortoise_orm" data-bs-toggle="tab" data-bs-target="#models_tortoise_orm-pane" type="button" role="tab" aria-controls="models_tortoise_orm" aria-selected="true">Tortoise ORM</button></li><li class="nav-item" role="presentation"><button class="nav-link " id="models_django_orm" data-bs-toggle="tab" data-bs-target="#models_django_orm-pane" type="button" role="tab" aria-controls="models_django_orm" aria-selected="false">Django ORM</button></li><li class="nav-item" role="presentation"><button class="nav-link " id="models_sql_alchemy" data-bs-toggle="tab" data-bs-target="#models_sql_alchemy-pane" type="button" role="tab" aria-controls="models_sql_alchemy" aria-selected="false">SQL Alchemy</button></li><li class="nav-item" role="presentation"><button class="nav-link " id="models_pony_orm" data-bs-toggle="tab" data-bs-target="#models_pony_orm-pane" type="button" role="tab" aria-controls="models_pony_orm" aria-selected="false">Pony ORM</button></li></ul><div class="tab-content" id="register_models-content"><div class="tab-pane fade show active" id="models_tortoise_orm-pane" role="tabpanel" aria-labelledby="models_tortoise_orm" tabindex="0"><pre><code class="language-python"> from uuid import UUID import bcrypt from tortoise import fields from tortoise.models import Model from fastadmin import TortoiseModelAdmin, WidgetType, action, register class ModelUser(Model): username = fields.CharField(max_length=255, unique=True) hash_password = fields.CharField(max_length=255) is_superuser = fields.BooleanField(default=False) is_active = fields.BooleanField(default=False) def __str__(self): return self.username @register(ModelUser) class UserAdmin(TortoiseModelAdmin): list_display = ("username", "is_superuser", "is_active") list_display_links = ("username",) list_filter = ( "username", "is_superuser", "is_active", ) search_fields = ( "id", "username", ) fieldsets = ( (None, {"fields": ("username", "hash_password")}), ("Permissions", {"fields": ("is_active", "is_superuser")}), ) formfield_overrides = { # noqa: RUF012 "username": (WidgetType.SlugInput, {"required": True}), "password": (WidgetType.PasswordInput, {"passwordModalForm": True}), } actions = ( *TortoiseModelAdmin.actions, "activate", "deactivate", ) async def authenticate(self, username: str, password: str) -> int | None: user = await self.model_cls.filter(phone=username, is_superuser=True).first() if not user: return None if not bcrypt.checkpw(password.encode(), user.hash_password.encode()): return None return user.id async def change_password(self, id: UUID | int, password: str) -> None: user = await self.model_cls.filter(id=id).first() if not user: return user.hash_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() await user.save(update_fields=("hash_password",)) @action(description="Set as active") async def activate(self, ids: list[int]) -> None: await self.model_cls.filter(id__in=ids).update(is_active=True) @action(description="Deactivate") async def deactivate(self, ids: list[int]) -> None: await self.model_cls.filter(id__in=ids).update(is_active=False) </code></pre></div><div class="tab-pane fade " id="models_django_orm-pane" role="tabpanel" aria-labelledby="models_django_orm" tabindex="0"><p class="alert alert-info"> See example for Tortoise ORM </p></div><div class="tab-pane fade " id="models_sql_alchemy-pane" role="tabpanel" aria-labelledby="models_sql_alchemy" tabindex="0"><p class="alert alert-info"> See example for Tortoise ORM </p></div><div class="tab-pane fade " id="models_pony_orm-pane" role="tabpanel" aria-labelledby="models_pony_orm" tabindex="0"><p class="alert alert-info"> See example for Tortoise ORM </p></div></div></section><section id="authentication"><h3>Authentication</h3><p class="alert alert-info"> You have to implement methods authenticate and change_password in Modal Admin for User model. See example above. </p></section><section id="model-methods-and-attributes"><h3>Methods and Attributes</h3><p class="text-4"> There are methods and attributes for Model Admin: </p><pre><code class="language-python"> class BaseModelAdmin: """Base class for model admin""" # Use it only if you use several orms in your project. model_name_prefix: str | None = None # A list of actions to make available on the change list page. # You have to implement methods with names like action_name in your ModelAdmin class and decorate them with @action decorator. # Example of usage: # # actions = ("make_published",) # @action( # description="Mark selected stories as published", # ) # async def make_published(self, objs: list[Any]) -> None: # ... actions: Sequence[str] = () # Controls where on the page the actions bar appears. # By default, the admin changelist displays actions at the top of the page (actions_on_top = False; actions_on_bottom = True). # Example of usage: actions_on_top = True actions_on_top: bool = False # Controls where on the page the actions bar appears. # By default, the admin changelist displays actions at the top of the page (actions_on_top = False; actions_on_bottom = True). # Example of usage: actions_on_bottom = False actions_on_bottom: bool = True # Controls whether a selection counter is displayed next to the action dropdown. By default, the admin changelist will display it # Example of usage: actions_selection_counter = False actions_selection_counter: bool = True # Not supported setting # date_hierarchy # This attribute overrides the default display value for record's fields that are empty (None, empty string, etc.). The default value is - (a dash). # Example of usage: empty_value_display = "N/A" empty_value_display: str = "-" # This attribute, if given, should be a list of field names to exclude from the form. # Example of usage: exclude = ("password", "otp") exclude: Sequence[str] = () # Use the fields option to make simple layout changes in the forms on the “add” and “change” pages # such as showing only a subset of available fields, modifying their order, or grouping them into rows. # For more complex layout needs, see the fieldsets option. # Example of usage: fields = ("id", "mobile_number", "email", "is_superuser", "is_active", "created_at") fields: Sequence[str] = () # Set fieldsets to control the layout of admin “add” and “change” pages. # fieldsets is a list of two-tuples, in which each two-tuple represents a fieldset on the admin form page. (A fieldset is a “section” of the form.) fieldsets: Sequence[tuple[str | None, dict[str, Sequence[str]]]] = () # By default, a ManyToManyField is displayed in the admin dashboard with a select multiple. # However, multiple-select boxes can be difficult to use when selecting many items. # Adding a ManyToManyField to this list will instead use a nifty unobtrusive JavaScript “filter” interface that allows searching within the options. # The unselected and selected options appear in two boxes side by side. See filter_vertical to use a vertical interface. # Example of usage: filter_horizontal = ("groups", "user_permissions") filter_horizontal: Sequence[str] = () # Same as filter_horizontal, but uses a vertical display of the filter interface with the box of unselected options appearing above the box of selected options. # Example of usage: filter_vertical = ("groups", "user_permissions") filter_vertical: Sequence[str] = () # Not supported setting # form # This provides a quick-and-dirty way to override some of the Field options for use in the admin. # formfield_overrides is a dictionary mapping a field class to a dict # of arguments to pass to the field at construction time. # Example of usage: # formfield_overrides = { # "description": (WidgetType.RichTextArea, {}) # } formfield_overrides: dict[str, tuple[WidgetType, dict]] = {} # noqa: RUF012 # Set list_display to control which fields are displayed on the list page of the admin. # If you don't set list_display, the admin site will display a single column that displays the __str__() representation of each object # Example of usage: list_display = ("id", "mobile_number", "email", "is_superuser", "is_active", "created_at") list_display: Sequence[str] = () # Use list_display_links to control if and which fields in list_display should be linked to the “change” page for an object. # Example of usage: list_display_links = ("id", "mobile_number", "email") list_display_links: Sequence[str] = () # A dictionary containing the field names and the corresponding widget type and # column widths (px, %) for the list view. # Example of usage: # list_display_widths = { # "id": "100px", # } list_display_widths: dict[str, str] = {} # noqa: RUF012 # Set list_filter to activate filters in the tabel columns of the list page of the admin. # Example of usage: list_filter = ("is_superuser", "is_active", "created_at") list_filter: Sequence[str] = () # By default, applied filters are preserved on the list view after creating, editing, or deleting an object. # You can have filters cleared by setting this attribute to False. # Example of usage: preserve_filters = False preserve_filters: bool = True # Set list_max_show_all to control how many items can appear on a “Show all” admin change list page. # The admin will display a “Show all” link on the change list only if the total result count is less than or equal to this setting. By default, this is set to 200. # Example of usage: list_max_show_all = 100 list_max_show_all: int = 200 # Set list_per_page to control how many items appear on each paginated admin list page. By default, this is set to 10. # Example of usage: list_per_page = 50 list_per_page = 10 # Set list_select_related to tell ORM to use select_related() in retrieving the list of objects on the admin list page. # This can save you a bunch of database queries. # Example of usage: list_select_related = ("user",) list_select_related: Sequence[str] = () # Set ordering to specify how lists of objects should be ordered in the admin views. # This should be a list or tuple in the same format as a model's ordering parameter. # Example of usage: ordering = ("-created_at",) ordering: Sequence[str] = () # Not supported setting # paginator # When set, the given fields will use a bit of JavaScript to populate from the fields assigned. # The main use for this functionality is # to automatically generate the value for SlugField fields from one or more other fields. # The generated value is produced by concatenating the values of the source fields, # and then by transforming that result into a valid slug # (e.g. substituting dashes for spaces and lowercasing ASCII letters). # prepopulated_fields: dict[str, Sequence[str]] = {} # By default, FastAPI admin uses a select-box interface (select) for fields that are ForeignKey or have choices set. # If a field is present in radio_fields, FastAPI admin will use a radio-button interface instead. # Example of usage: radio_fields = ("user",) radio_fields: Sequence[str] = () # Not supported setting (all fk, m2m uses select js widget as default) # autocomplete_fields # By default, FastAPI admin uses a select-box interface (select) for fields that are ForeignKey. # Sometimes you don't want to incur the overhead of having to select all the related instances to display in the drop-down. # raw_id_fields is a list of fields you would like to change into an Input widget for either a ForeignKey or ManyToManyField. # Example of usage: raw_id_fields = ("user",) raw_id_fields: Sequence[str] = () # By default the admin shows all fields as editable. # Any fields in this option (which should be a list or tuple) will display its data as-is and non-editable. # Example of usage: readonly_fields = ("created_at",) readonly_fields: Sequence[str] = () # Set search_fields to enable a search box on the admin list page. # This should be set to a list of field names that will be searched whenever somebody submits a search query in that text box. # Example of usage: search_fields = ("mobile_number", "email") search_fields: Sequence[str] = () # Set search_help_text to specify a descriptive text for the search box which will be displayed below it. # Example of usage: search_help_text = "Search by mobile number or email" search_help_text: str = "" # Set show_full_result_count to control whether the full count of objects should be displayed # on a filtered admin page (e.g. 99 results (103 total)). # If this option is set to False, a text like 99 results (Show all) is displayed instead. # Example of usage: show_full_result_count = True show_full_result_count: bool = False # By default, the list page allows sorting by all model fields # If you want to disable sorting for some columns, set sortable_by to a collection (e.g. list, tuple, or set) # of the subset of list_display that you want to be sortable. # An empty collection disables sorting for all columns. # Example of usage: sortable_by = ("mobile_number", "email") sortable_by: Sequence[str] = () # An override to the verbose_name from the model's inner Meta class. verbose_name: str | None = None # An override to the verbose_name_plural from the model's inner Meta class. verbose_name_plural: str | None = None def __init__(self, model_cls: Any): """This method is used to initialize admin class. :params model_cls: an orm/db model class. """ self.model_cls = model_cls @staticmethod def get_model_pk_name(orm_model_cls: Any) -> str: """This method is used to get model pk name. :return: A str. """ raise NotImplementedError def get_model_fields_with_widget_types( self, with_m2m: bool | None = None, with_upload: bool | None = None, ) -> list[ModelFieldWidgetSchema]: """This method is used to get model fields with widget types. :params with_m2m: a flag to include m2m fields. :params with_upload: a flag to include upload fields. :return: A list of ModelFieldWidgetSchema. """ raise NotImplementedError async def orm_get_list( self, offset: int | None = None, limit: int | None = None, search: str | None = None, sort_by: str | None = None, filters: dict | None = None, ) -> tuple[list[Any], int]: """This method is used to get list of orm/db model objects. :params offset: an offset for pagination. :params limit: a limit for pagination. :params search: a search query. :params sort_by: a sort by field name. :params filters: a dict of filters. :return: A tuple of list of objects and total count. """ raise NotImplementedError async def orm_get_obj(self, id: UUID | int) -> Any | None: """This method is used to get orm/db model object. :params id: an id of object. :return: An object or None. """ raise NotImplementedError async def orm_save_obj(self, id: UUID | Any | None, payload: dict) -> Any: """This method is used to save orm/db model object. :params id: an id of object. :params payload: a dict of payload. :return: An object. """ raise NotImplementedError async def orm_delete_obj(self, id: UUID | int) -> None: """This method is used to delete orm/db model object. :params id: an id of object. :return: None. """ raise NotImplementedError async def orm_get_m2m_ids(self, obj: Any, field: str) -> list[int | UUID]: """This method is used to get m2m ids. :params obj: an object. :params field: a m2m field name. :return: A list of ids. """ raise NotImplementedError async def orm_save_m2m_ids(self, obj: Any, field: str, ids: list[int | UUID]) -> None: """This method is used to get m2m ids. :params obj: an object. :params field: a m2m field name. :params ids: a list of ids. :return: A list of ids. """ raise NotImplementedError async def orm_save_upload_field(self, obj: Any, field: str, base64: str) -> None: """This method is used to save upload field. :params obj: an object. :params field: a m2m field name. :params base64: a base64 string. :return: A list of ids. """ raise NotImplementedError @classmethod def get_sessionmaker(cls) -> Any: """This method is used to get db session maker for sqlalchemy. :return: A db session maker. """ return cls.db_session_maker @classmethod def set_sessionmaker(cls, db_session_maker: Any) -> None: """This method is used to set db session maker for sqlalchemy. :params db_session: a db session maker. :return: None. """ cls.db_session_maker = db_session_maker def get_fields_for_serialize(self) -> set[str]: """This method is used to get fields for serialize. :return: A set of fields. """ fields = self.get_model_fields_with_widget_types() fields_for_serialize = {field.name for field in fields} if self.fields: fields_for_serialize &= set(self.fields) if self.exclude: fields_for_serialize -= set(self.exclude) if self.list_display: fields_for_serialize |= set(self.list_display) return fields_for_serialize async def serialize_obj_attributes( self, obj: Any, attributes_to_serizalize: list[ModelFieldWidgetSchema] ) -> dict[str, Any]: """Serialize orm model obj attribute to dict. :params obj: an object. :params attributes_to_serizalize: a list of attributes to serialize. :return: A dict of serialized attributes. """ serialized_dict = {field.name: getattr(obj, field.column_name) for field in attributes_to_serizalize} if inspect.iscoroutinefunction(obj.__str__): str_fn = obj.__str__ else: str_fn = sync_to_async(obj.__str__) serialized_dict["__str__"] = await str_fn() return serialized_dict async def serialize_obj(self, obj: Any, list_view: bool = False) -> dict: """Serialize orm model obj to dict. :params obj: an object. :params exclude_fields: a list of fields to exclude. :return: A dict. """ fields = self.get_model_fields_with_widget_types() fields_for_serialize = self.get_fields_for_serialize() obj_dict = {} attributes_to_serizalize = [] for field in fields: if field.name not in fields_for_serialize: continue if field.is_m2m and list_view: continue if field.is_m2m: obj_dict[field.name] = await self.orm_get_m2m_ids(obj, field.column_name) else: attributes_to_serizalize.append(field) obj_dict.update(await self.serialize_obj_attributes(obj, attributes_to_serizalize)) for field_name in fields_for_serialize: display_field_function = getattr(self, field_name, None) if not display_field_function or not hasattr(display_field_function, "is_display"): continue if inspect.iscoroutinefunction(display_field_function): display_field_function_fn = display_field_function else: display_field_function_fn = sync_to_async(display_field_function) obj_dict[field_name] = await display_field_function_fn(obj) return obj_dict def deserialize_value(self, field: ModelFieldWidgetSchema, value: Any) -> Any: if not value: return value match field.form_widget_type: case WidgetType.TimePicker: return datetime.datetime.fromisoformat(value).time() case WidgetType.DatePicker | WidgetType.DateTimePicker: return datetime.datetime.fromisoformat(value) case _: return value async def get_list( self, offset: int | None = None, limit: int | None = None, search: str | None = None, sort_by: str | None = None, filters: dict | None = None, ) -> tuple[list[dict], int]: """This method is used to get list of seriaized objects. :params offset: an offset for pagination. :params limit: a limit for pagination. :params search: a search query. :params sort_by: a sort by field name. :params filters: a dict of filters. :return: A tuple of list of dict and total count. """ objs, total = await self.orm_get_list( offset=offset, limit=limit, search=search, sort_by=sort_by, filters=filters, ) serialized_objs = [] for obj in objs: serialized_objs.append(await self.serialize_obj(obj, list_view=True)) return serialized_objs, total async def get_obj(self, id: UUID | int) -> dict | None: """This method is used to get serialized object by id. :params id: an id of object. :return: A dict or None. """ obj = await self.orm_get_obj(id) if not obj: return None return await self.serialize_obj(obj) async def save_model(self, id: UUID | int | None, payload: dict) -> dict | None: """This method is used to save orm/db model object. :params id: an id of object. :params payload: a payload from request. :return: A saved object or None. """ fields = self.get_model_fields_with_widget_types(with_m2m=False, with_upload=False) m2m_fields = self.get_model_fields_with_widget_types(with_m2m=True) upload_fields = self.get_model_fields_with_widget_types(with_upload=True) fields_payload = { field.column_name: self.deserialize_value(field, payload[field.name]) for field in fields if field.name in payload } obj = await self.orm_save_obj(id, fields_payload) if not obj: return None for upload_field in upload_fields: if upload_field.name in payload and is_valid_base64(payload[upload_field.name]): await self.orm_save_upload_field(obj, upload_field.column_name, payload[upload_field.name]) for m2m_field in m2m_fields: if m2m_field.name in payload: await self.orm_save_m2m_ids(obj, m2m_field.column_name, payload[m2m_field.name]) return await self.serialize_obj(obj) async def delete_model(self, id: UUID | int) -> None: """This method is used to delete orm/db model object. :params id: an id of object. :return: None. """ await self.orm_delete_obj(id) async def get_export( self, export_format: ExportFormat | None, offset: int | None = None, limit: int | None = None, search: str | None = None, sort_by: str | None = None, filters: dict | None = None, ) -> StringIO | BytesIO | None: """This method is used to get export data (str or bytes stream). :params export_format: a n export format (CSV at default). :params offset: an offset for pagination. :params limit: a limit for pagination. :params search: a search query. :params sort_by: a sort by field name. :params filters: a dict of filters. :return: A StringIO or BytesIO object. """ objs, _ = await self.orm_get_list(offset=offset, limit=limit, search=search, sort_by=sort_by, filters=filters) fields = self.get_model_fields_with_widget_types(with_m2m=False) export_fields = [f.name for f in fields] match export_format: case ExportFormat.CSV: output = StringIO() writer = csv.DictWriter(output, fieldnames=export_fields) writer.writeheader() for obj in objs: obj_dict = await self.serialize_obj(obj, list_view=True) obj_dict = {k: v for k, v in obj_dict.items() if k in export_fields} writer.writerow(obj_dict) output.seek(0) return output case ExportFormat.JSON: class JSONEncoder(json.JSONEncoder): def default(self, obj): try: return super().default(obj) except TypeError: return str(obj) output = StringIO() json.dump([await self.serialize_obj(obj, list_view=True) for obj in objs], output, cls=JSONEncoder) output.seek(0) return output case _: return None async def has_add_permission(self, user_id: UUID | int | None = None) -> bool: """This method is used to check if user has permission to add new model instance. :param user_id: The user id. :return: A boolean value. """ return True async def has_change_permission(self, user_id: UUID | int | None = None) -> bool: """This method is used to check if user has permission to change model instance. :param user_id: The user id. :return: A boolean value. """ return True async def has_delete_permission(self, user_id: UUID | int | None = None) -> bool: """This method is used to check if user has permission to delete model instance. :param user_id: The user id. :return: A boolean value. """ return True async def has_export_permission(self, user_id: UUID | int | None = None) -> bool: """This method is used to check if user has permission to export model instance. :param user_id: The user id. :return: A boolean value. """ return True </code></pre><p class="text-4"> Specific methods and attributes for Model Admin: </p><pre><code class="language-python"> class ModelAdmin(BaseModelAdmin): """This class is used to create admin model class.""" # Normally, objects have three save options: “Save”, “Save and continue editing”, and “Save and add another”. # If save_as is True, “Save and add another” will be replaced # by a “Save as new” button that creates a new object (with a new ID) rather than updating the existing object. # Example of usage: save_as = True save_as: bool = False # When save_as_continue=True, the default redirect after saving the new object is to the change view for that object. # If you set save_as_continue=False, the redirect will be to the changelist view. # Example of usage: save_as_continue = False save_as_continue: bool = False # Normally, the save buttons appear only at the bottom of the forms. # If you set save_on_top, the buttons will appear both on the top and the bottom. # Example of usage: save_on_top = True save_on_top: bool = False # Set view_on_site to control whether or not to display the “View on site” link. # This link should bring you to a URL where you can display the saved object. # Example of usage: view_on_site = "http://example.com" view_on_site: str | None = None # Inlines inlines: Sequence[type[InlineModelAdmin]] = () async def authenticate(self, username: str, password: str) -> UUID | int | None: """This method is used to implement authentication for settings.ADMIN_USER_MODEL orm/db model. :params username: a value for user model settings.ADMIN_USER_MODEL_USERNAME_FIELD field. :params password: a password. :return: An user id or None. """ raise NotImplementedError async def change_password(self, id: UUID | int, password: str) -> None: """This method is used to change user password. :params id: An user id. :params password: A new password. """ raise NotImplementedError async def save_model(self, id: UUID | int | None, payload: dict) -> dict | None: """This method is used to save orm/db model object. :params id: an id of object. :params payload: a payload from request. :return: A saved object or None. """ obj = await super().save_model(id, payload) fields = self.get_model_fields_with_widget_types(with_m2m=False, with_upload=False) password_fields = [field.name for field in fields if field.form_widget_type == WidgetType.PasswordInput] if obj and id is None and password_fields: # save hashed password for create pk_name = self.get_model_pk_name(self.model_cls) pk = obj[pk_name] password_values = [payload[field] for field in password_fields if field in payload] if password_values: await self.change_password(pk, password_values[0]) return obj </code></pre></section><section id="model-form-field-types"><h3>Form Field Types</h3><p class="text-4"> There are form field types for model admin: </p><pre><code class="language-python"> class WidgetType(str, Enum): """Widget type""" Input = "Input" InputNumber = "InputNumber" SlugInput = "SlugInput" EmailInput = "EmailInput" PhoneInput = "PhoneInput" UrlInput = "UrlInput" PasswordInput = "PasswordInput" TextArea = "TextArea" RichTextArea = "RichTextArea" JsonTextArea = "JsonTextArea" Select = "Select" AsyncSelect = "AsyncSelect" AsyncTransfer = "AsyncTransfer" Switch = "Switch" Checkbox = "Checkbox" TimePicker = "TimePicker" DatePicker = "DatePicker" DateTimePicker = "DateTimePicker" RangePicker = "RangePicker" RadioGroup = "RadioGroup" CheckboxGroup = "CheckboxGroup" Upload = "Upload" </code></pre><p class="alert alert-warning"> Note: Please see <a href='https://ant.design/components/overview' target='_blank'>antd components</a> for more details (e.g. to see how they look like). </p></section></section><hr class="small-divider" /><section id="inline-admins"><h2>Inline Model Admins</h2><section id="registering-inlines"><h3>Registering Inlines</h3><ul class="nav nav-tabs" id="register_inlines" role="tablist"><li class="nav-item" role="presentation"><button class="nav-link active" id="inlines_tortoise_orm" data-bs-toggle="tab" data-bs-target="#inlines_tortoise_orm-pane" type="button" role="tab" aria-controls="inlines_tortoise_orm" aria-selected="true">Tortoise ORM</button></li><li class="nav-item" role="presentation"><button class="nav-link " id="inlines_django_orm" data-bs-toggle="tab" data-bs-target="#inlines_django_orm-pane" type="button" role="tab" aria-controls="inlines_django_orm" aria-selected="false">Django ORM</button></li><li class="nav-item" role="presentation"><button class="nav-link " id="inlines_sql_alchemy" data-bs-toggle="tab" data-bs-target="#inlines_sql_alchemy-pane" type="button" role="tab" aria-controls="inlines_sql_alchemy" aria-selected="false">SQL Alchemy</button></li><li class="nav-item" role="presentation"><button class="nav-link " id="inlines_pony_orm" data-bs-toggle="tab" data-bs-target="#inlines_pony_orm-pane" type="button" role="tab" aria-controls="inlines_pony_orm" aria-selected="false">Pony ORM</button></li></ul><div class="tab-content" id="register_inlines-content"><div class="tab-pane fade show active" id="inlines_tortoise_orm-pane" role="tabpanel" aria-labelledby="inlines_tortoise_orm" tabindex="0"><pre><code class="language-python"> from uuid import UUID import bcrypt from tortoise import fields from tortoise.models import Model from fastadmin import TortoiseInlineModelAdmin, TortoiseModelAdmin, WidgetType, action, register class InlineUser(Model): username = fields.CharField(max_length=255, unique=True) hash_password = fields.CharField(max_length=255) is_superuser = fields.BooleanField(default=False) is_active = fields.BooleanField(default=False) def __str__(self): return self.username class InlineUserMessage(Model): user = fields.ForeignKeyField("models.InlineUser", related_name="messages") message = fields.TextField() def __str__(self): return self.message class UserMessageAdminInline(TortoiseInlineModelAdmin): model = InlineUserMessage list_display = ("user", "message") list_display_links = ("user", "message") list_filter = ("user", "message") search_fields = ("user", "message") @register(InlineUser) class UserAdmin(TortoiseModelAdmin): list_display = ("username", "is_superuser", "is_active") list_display_links = ("username",) list_filter = ( "username", "is_superuser", "is_active", ) search_fields = ( "id", "username", ) fieldsets = ( (None, {"fields": ("username", "hash_password")}), ("Permissions", {"fields": ("is_active", "is_superuser")}), ) formfield_overrides = { # noqa: RUF012 "username": (WidgetType.SlugInput, {"required": True}), "password": (WidgetType.PasswordInput, {"passwordModalForm": True}), } actions = ( *TortoiseModelAdmin.actions, "activate", "deactivate", ) inlines = (UserMessageAdminInline,) async def authenticate(self, username: str, password: str) -> int | None: user = await self.model_cls.filter(phone=username, is_superuser=True).first() if not user: return None if not bcrypt.checkpw(password.encode(), user.hash_password.encode()): return None return user.id async def change_password(self, id: UUID | int, password: str) -> None: user = await self.model_cls.filter(id=id).first() if not user: return user.hash_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() await user.save(update_fields=("hash_password",)) @action(description="Set as active") async def activate(self, ids: list[int]) -> None: await self.model_cls.filter(id__in=ids).update(is_active=True) @action(description="Deactivate") async def deactivate(self, ids: list[int]) -> None: await self.model_cls.filter(id__in=ids).update(is_active=False) </code></pre></div><div class="tab-pane fade " id="inlines_django_orm-pane" role="tabpanel" aria-labelledby="inlines_django_orm" tabindex="0"><p class="alert alert-info"> See example for Tortoise ORM </p></div><div class="tab-pane fade " id="inlines_sql_alchemy-pane" role="tabpanel" aria-labelledby="inlines_sql_alchemy" tabindex="0"><p class="alert alert-info"> See example for Tortoise ORM </p></div><div class="tab-pane fade " id="inlines_pony_orm-pane" role="tabpanel" aria-labelledby="inlines_pony_orm" tabindex="0"><p class="alert alert-info"> See example for Tortoise ORM </p></div></div></section><section id="inline-methods-and-attributes"><h3>Methods and Attributes</h3><p class="text-4"> There are methods and attributes for Inline Model Admin: </p><p class="alert alert-info"> See BaseModelAdmin class methods and attributes in model admin section. </p><p class="text-4"> Specific methods and attributes for Inline Model Admin: </p><pre><code class="language-python"> class InlineModelAdmin(BaseModelAdmin): """This class is used to create admin inline model class.""" # The model class which the inline is using. This is required. model: Any # The name of the foreign key on the model. # In most cases this will be dealt with automatically, but fk_name must be specified explicitly # if there are more than one foreign key to the same parent model. fk_name: str | None = None # This controls the maximum number of forms to show in the inline. # This doesn't directly correlate to the number of objects, but can if the value is small enough. # See Limiting the number of editable objects for more information. max_num: int = 10 # This controls the minimum number of forms to show in the inline. min_num: int = 1 </code></pre></section></section><hr class="small-divider" /><section id="changelog"><h2>Changelog</h2><p class="alert alert-info"> See what's new added, changed, fixed, improved or updated in the latest versions. </p><section id="v0_2_20"><h3>v0.2.20</h3><p class="text-4"> Fix for _id fields. Bump packages for backend and frontend. </p></section><section id="v0_2_19"><h3>v0.2.19</h3><p class="text-4"> Fix for is_pk for tortoise orm. </p></section><section id="v0_2_18"><h3>v0.2.18</h3><p class="text-4"> Fixes for m2m fk's sqlalchemy postgres. Convert str to int for them. </p></section><section id="v0_2_17"><h3>v0.2.17</h3><p class="text-4"> Fixes for fk sqlalchemy postgres. Convert str to int for them. </p></section><section id="v0_2_16"><h3>v0.2.16</h3><p class="text-4"> Added new setting ADMIN_DISABLE_CROP_IMAGE. So,we can configure crop images on upload. </p></section><section id="v0_2_15"><h3>v0.2.15</h3><p class="text-4"> Fix password logic for user. </p></section><section id="v0_2_14"><h3>v0.2.14</h3><p class="text-4"> Make permissions functions awaitable. Bump frontend/backend packages. </p></section><section id="v0_2_13"><h3>v0.2.13</h3><p class="text-4"> Fix edit page frontend issue for Date field. </p></section><section id="v0_2_12"><h3>v0.2.12</h3><p class="text-4"> Remove python-dotenv dep. Bump django. Add django example. </p></section><section id="v0_2_11"><h3>v0.2.11</h3><p class="text-4"> Fixes for examples. Fixes for Pony ORM (delete, update m2m). Allow sorting by custom columns. Fix for list_display ordering. </p></section><section id="v0_2_10"><h3>v0.2.10</h3><p class="text-4"> Fix issue empty m2m. Optimisation on unit tests. Fix for pony orm. Optimisation on search for tortoise orm. </p></section><section id="v0_2_9"><h3>v0.2.9</h3><p class="text-4"> Fix issue with modal inline dialogs. Fix issue with m2m multiple select. </p></section><section id="v0_2_8"><h3>v0.2.8</h3><p class="text-4"> Fix sqlalchemy delete functionality. Add more examples. </p></section><section id="v0_2_7"><h3>v0.2.7</h3><p class="text-4"> Fix helpers function. Add regexps. </p></section><section id="v0_2_6"><h3>v0.2.6</h3><p class="text-4"> Add edit btn for async select. </p></section><section id="v0_2_5"><h3>v0.2.5</h3><p class="text-4"> Fix for async select in inlines. </p></section><section id="v0_2_4"><h3>v0.2.4</h3><p class="text-4"> Fix dashboard widgets and auto register inlines. </p></section><section id="v0_2_3"><h3>v0.2.3</h3><p class="text-4"> Fix filters issue on lists. Remove jinja from dependencies. </p></section><section id="v0_2_2"><h3>v0.2.2</h3><p class="text-4"> Fix bugs with datetime. </p></section><section id="v0_2_1"><h3>v0.2.1</h3><p class="text-4"> Update packages. Fix linters and tests in vite frontend. Removed pydantic from dependencies. </p></section><section id="v0_2_0"><h3>v0.2.0</h3><p class="text-4"> Update packages. Use vite instead obsolete react-scripts. </p></section></section><hr class="small-divider" /></div></div></div><footer id="footer" class="section bg-dark footer-text-light"><div class="container"><!-- <ul class="social-icons social-icons-lg social-icons-muted justify-content-center mb-3"><li><a data-toggle="tooltip" href="https://twitter.com/harnishdesign/" target="_blank" title="" data-original-title="Twitter"><i class="fab fa-twitter"></i></a></li><li><a data-toggle="tooltip" href="http://www.facebook.com/harnishdesign/" target="_blank" title="" data-original-title="Facebook"><i class="fab fa-facebook-f"></i></a></li><li><a data-toggle="tooltip" href="http://www.dribbble.com/harnishdesign/" target="_blank" title="" data-original-title="Dribbble"><i class="fab fa-dribbble"></i></a></li><li><a data-toggle="tooltip" href="http://www.github.com/" target="_blank" title="" data-original-title="GitHub"><i class="fab fa-github"></i></a></li></ul> --><p class="text-center"> Copyright © 2025 FastAdmin. All Rights Reserved. </p></div></footer></div><a id="back-to-top" data-toggle="tooltip" title="Back to Top" href="#"> ^ </a><script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.3/jquery.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.3/js/bootstrap.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script><script src="assets/js/theme.js"></script></body></html>