多个 Admin 实例
你构造的每个 Admin 实例都是一个自包含的 Starlette 子应用。可以按需挂载任意数量的实例,每个实例都拥有自己的 base_url、route_name、认证提供方和视图。
from sqlalchemy import create_engine
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.sessions import SessionMiddleware
from starlette.requests import Request
from starlette_admin.auth import AdminUser, AuthProvider, LoginFailed
from starlette_admin.contrib.sqla import Admin, ModelView
from myapp.models import Order, Post, User
engine = create_engine("sqlite:///app.sqlite")
app = Starlette()
STAFF = {"staff": "staffpass"}
SUPERADMINS = {"root": "rootpass"}
class StaffAuth(AuthProvider):
async def login(
self, username: str, password: str, remember_me: bool, request: Request
) -> None:
if STAFF.get(username) == password:
request.session["user"] = username
return
raise LoginFailed("Invalid username or password")
async def authenticate(self, request: Request) -> AdminUser | None:
username = request.session.get("user")
return AdminUser(username=username) if username in STAFF else None
async def logout(self, request: Request) -> None:
request.session.clear()
class SuperAdminAuth(AuthProvider):
async def login(
self, username: str, password: str, remember_me: bool, request: Request
) -> None:
if SUPERADMINS.get(username) == password:
request.session["user"] = username
return
raise LoginFailed("Invalid username or password")
async def authenticate(self, request: Request) -> AdminUser | None:
username = request.session.get("user")
return AdminUser(username=username) if username in SUPERADMINS else None
async def logout(self, request: Request) -> None:
request.session.clear()
staff_admin = Admin(
engine,
title="Staff Admin",
base_url="/staff",
route_name="staff_admin",
auth_provider=StaffAuth(),
secret_key="staff-secret-change-me",
middlewares=[Middleware(SessionMiddleware, secret_key="staff-secret-change-me")],
)
staff_admin.add_view(ModelView(Order))
staff_admin.add_view(ModelView(Post))
staff_admin.mount_to(app)
root_admin = Admin(
engine,
title="Super Admin",
base_url="/root",
route_name="root_admin",
auth_provider=SuperAdminAuth(),
secret_key="root-secret-change-me",
middlewares=[Middleware(SessionMiddleware, secret_key="root-secret-change-me")],
)
root_admin.add_view(ModelView(Order))
root_admin.add_view(ModelView(User))
root_admin.mount_to(app)
在此示例中,/staff 显示一个由 StaffAuth 支持的登录页面,/root 则显示另一个由 SuperAdminAuth 支持的独立登录页面。登录其中之一并不会获得对另一个的访问权限:每个 SessionMiddleware 都使用自己的 secret_key 对 Cookie 签名,因此每个 Admin 实例只会读取由自身认证提供方写入的会话数据。
base_url 与 route_name
base_url 和 route_name 是 starlette_admin/base.py 中 Admin 和 BaseAdmin 类的构造参数。它们分别默认为 /admin 和 "admin":
def __init__(
self,
title: str = "Admin",
base_url: str = "/admin",
route_name: str = "admin",
...
)
base_url设置管理后台挂载所在的路径前缀。它会直接传入内部的app.mount(self.base_url, app=admin_app, name=self.route_name)调用,因此在各实例之间必须唯一,否则一个挂载会遮蔽另一个。route_name是 Starlette 注册该挂载时使用的名称。管理后台生成的所有 URL——包括列表、详情、编辑、导出和静态资源——都来自request.url_for(route_name + ":list", ...),并且每个页面模板都会读取request.app.state.ROUTE_NAME来获取构建链接所需的正确前缀。
mount_to 会为每个 Admin 实例构建一个全新的 Starlette 子应用,因此中间件、路由和模板全局变量彼此隔离。Admin 并不是进程级单例:可以根据应用的需要构造任意多个独立实例。
Warning
为每个 Admin 设置不同的 route_name。Starlette 的路由通过匹配挂载名称来解析 url_for("admin:list", ...),因此两个共用同一 route_name 的管理后台会让父应用中存在两个同名挂载,url_for 会解析到 Starlette 先匹配到的那个。于是第二个管理后台中的所有内部链接——包括编辑链接、静态资源和导出端点——都会悄然指向第一个管理后台的 base_url。
共享视图与定义独立视图
add_view 接收一个视图实例,并在设置过程中对其进行修改。对于 BaseModelView,该设置过程会把内部回调绑定到它注册所在的 Admin,包括 HasOne 和 HasMany 字段如何解析关联记录的链接。
如果在两个管理后台上注册同一个视图实例,第二次 add_view 调用会覆盖这些回调,导致第一个管理后台页面上的关联链接按照第二个管理后台的视图和 URL 解析。
为避免这种情况,应给每个管理后台提供一个全新的 ModelView 类实例。类本身不持有任何与管理后台相关的状态,只有实例才会持有:
staff_admin.add_view(ModelView(Order))
root_admin.add_view(ModelView(Order)) # separate instance of the same class; this is safe
当两个管理后台需要不同的行为时,例如不同的可见性规则或 can_delete 权限,应为每个后台编写各自的子类,而不是在运行时修补共享实例:
class StaffOrderView(ModelView):
fields_default_sort = ["-created_at"]
def can_delete(self, request: Request) -> bool:
return False
class RootOrderView(ModelView):
fields_default_sort = ["-created_at"]
staff_admin.add_view(StaffOrderView(Order))
root_admin.add_view(RootOrderView(Order))