Getting started¶
Install¶
pip install adminsite==0.1.0a6
adminsite does not pull in a database driver. Add the one you use, for example asyncpg or
psycopg for Postgres, or aiosqlite for SQLite with an async engine.
Your first admin¶
Say you have these models:
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class Customer(Base):
__tablename__ = "customers"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(120))
email: Mapped[str] = mapped_column(String(255), unique=True)
def __str__(self) -> str:
return self.name
class Order(Base):
__tablename__ = "orders"
id: Mapped[int] = mapped_column(primary_key=True)
customer_id: Mapped[int] = mapped_column(ForeignKey("customers.id"))
note: Mapped[str | None] = mapped_column(String(500), default=None)
customer: Mapped[Customer] = relationship()
Describe how each one should appear, and mount the admin:
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import create_async_engine
from adminsite import Admin, ModelView
engine = create_async_engine("postgresql+asyncpg://localhost/shop")
class CustomerView(ModelView, model=Customer):
list_display = ("name", "email")
search_fields = ("name", "email")
class OrderView(ModelView, model=Order):
list_display = ("id", "customer.name", "note")
search_fields = ("id", "customer.name")
app = FastAPI()
admin = Admin(engine, title="Shop", views=[CustomerView, OrderView])
app.mount("/admin", admin)
Run the app and open /admin. You get a list for each model, with search, sorting and paging, and
pages to add, change and delete records. The order form offers a customer picker rather than a
number box, because customer_id is a foreign key.
A view with no settings at all still works: it shows every column. Settings only narrow and order what is shown.
Mounting elsewhere¶
The admin is an ASGI app, so it mounts wherever ASGI apps do.
from starlette.applications import Starlette
from starlette.routing import Mount
app = Starlette(routes=[Mount("/admin", app=admin)])
app.mount("/admin", admin)
from litestar import Litestar, asgi
@asgi("/admin", is_mount=True)
async def admin_app(scope, receive, send) -> None:
await admin(scope, receive, send)
app = Litestar(route_handlers=[admin_app])
Links inside the admin follow the path it is mounted on, so /admin, /backoffice or
/tools/admin all work without configuration.
Several admins in one app¶
Mount as many as you like, each with its own views, users and settings:
staff = Admin(
engine,
title="Staff",
views=[OrderView, CustomerView, ProductView],
auth=staff_auth,
secret_key=settings.staff_secret,
)
support = Admin(
engine,
title="Support",
views=[OrderView, CustomerView],
auth=support_auth,
secret_key=settings.support_secret,
)
app.mount("/staff", staff)
app.mount("/support", support)
Each admin keeps its own session, in a cookie named after its title (adminsite_staff,
adminsite_support), so signing in to one never signs you in to the other. Give two admins with the
same title different names with session_cookie=. The same view class can be used in both; each
admin gets its own instance.
Signing in¶
An admin reachable from the internet needs a login. The quickest one:
from adminsite.auth import PasswordAuth, hash_password
admin = Admin(
engine,
views=[CustomerView, OrderView],
auth=PasswordAuth({"nima": "pbkdf2_sha256$600000$..."}),
secret_key=settings.admin_secret_key,
)
Make the hash once with hash_password("your password") and keep the result in your settings.
See Signing in for checking your own user table instead.
Trying the example¶
The repository has a small shop with an admin already set up:
git clone https://github.com/nimaxin/adminsite
cd adminsite
uv run uvicorn examples.shop:app --reload
Open http://127.0.0.1:8000/admin and sign in as nima / letmein.