Reference¶
The classes you use most, with their signatures and docstrings.
The admin¶
adminsite.Admin ¶
Admin(
source: Database | SessionSource,
*,
title: str = "Admin",
banner: str = "",
views: Sequence[
ModelView[Any] | type[ModelView[Any]]
] = (),
inspector: SQLAlchemyInspector | None = None,
field_registry: FieldRegistry | None = None,
template_dirs: Sequence[str | Path] = (),
auth: AuthProvider | None = None,
secret_key: str = "",
audit: AuditStore | bool = False,
saved_views: SavedViews | bool = False,
session_cookie: str | None = None,
pages: Sequence[AdminPage | type[AdminPage]] = (),
plugins: Sequence[Plugin] = (),
dashboard: Sequence[Widget] | None = None,
api: bool = False,
session_https_only: bool = False,
session_max_age: int | None = 14 * 24 * 3600,
language: str = "en",
languages: Sequence[str] = (),
translations: Translations | None = None,
timezone: str = "UTC",
timezones: Sequence[str] = (),
database_timezone: str = "UTC",
)
The admin itself: a small ASGI app you mount into your own.
admin = Admin(engine, title="Acme")
admin.add_view(OrderView)
app.mount("/admin", admin)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Database | SessionSource
|
Where the records live: an engine, an async engine, or a session factory for either. Sync and async behave the same. |
required |
title
|
str
|
The name in the sidebar, the browser tab and the sign in page. It names the session cookie too. |
'Admin'
|
banner
|
str
|
One line above every page and the sign in page, such as
"Staging: changes here are not real". Escaped, unless given as
|
''
|
views
|
Sequence[ModelView[Any] | type[ModelView[Any]]]
|
The views to show, each a class such as |
()
|
inspector
|
SQLAlchemyInspector | None
|
Reads the models' columns and relationships. Rarely needed. |
None
|
field_registry
|
FieldRegistry | None
|
Picks the field for each type of column. Left out,
|
None
|
template_dirs
|
Sequence[str | Path]
|
Folders searched for templates before the built in ones, to override one of the admin's or to add your own. |
()
|
auth
|
AuthProvider | None
|
Decides who may sign in. Left out, the admin is open to anyone who can reach it. |
None
|
secret_key
|
str
|
Signs the session cookie. Required with |
''
|
audit
|
AuditStore | bool
|
True to write down who changed what in a SQLite file of its
own, or an |
False
|
saved_views
|
SavedViews | bool
|
True to let people save lists they come back to, in a
SQLite file of their own, or a |
False
|
session_cookie
|
str | None
|
The session cookie's name. Left out, one made from the title, so two admins in one app keep separate sessions. |
None
|
pages
|
Sequence[AdminPage | type[AdminPage]]
|
Pages of your own, each an |
()
|
plugins
|
Sequence[Plugin]
|
Plugins, each set up as the admin is built. |
()
|
dashboard
|
Sequence[Widget] | None
|
The cards on the overview page. Left out, how many records each view holds; an empty list, no cards. |
None
|
api
|
bool
|
True to answer JSON under /-/api, through the same views and permissions as the pages. |
False
|
session_https_only
|
bool
|
True to send the session cookie over HTTPS only. Switch it on wherever the admin is served over HTTPS. |
False
|
session_max_age
|
int | None
|
How long the session cookie lasts, in seconds; two weeks unless you say. None keeps it for the browser session. |
14 * 24 * 3600
|
language
|
str
|
The language the admin speaks, such as "fa", and the
default for anyone whose browser asks for none of |
'en'
|
languages
|
Sequence[str]
|
More languages people may switch to, in the account menu at the foot of the sidebar. The first visit follows the browser. |
()
|
translations
|
Translations | None
|
Your own wording, by language and then by the English
text, such as |
None
|
timezone
|
str
|
The time zone times are shown in, such as "Asia/Tehran", until a person's browser says its own, and for an API token. |
'UTC'
|
timezones
|
Sequence[str]
|
More time zones people may switch to, in the account menu at the foot of the sidebar. Until they do, times follow the browser. |
()
|
database_timezone
|
str
|
The time zone the database keeps times in, for a
column that keeps none of its own as |
'UTC'
|
Raises:
| Type | Description |
|---|---|
AdminSiteError
|
When |
add_view ¶
add_view(
view: ModelView[Any] | type[ModelView[Any]],
) -> ModelView[Any]
add_page ¶
add_page(page: AdminPage | type[AdminPage]) -> AdminPage
Add a page of your own, served at /-/ and its name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
page
|
AdminPage | type[AdminPage]
|
The page, as an |
required |
Returns:
| Type | Description |
|---|---|
AdminPage
|
The page as the admin built it. |
Raises:
| Type | Description |
|---|---|
AdminSiteError
|
When a page of that name is already there, or the admin keeps the name for itself. |
use ¶
use(plugin: Plugin) -> Plugin
add_route ¶
add_route(
path: str,
endpoint: Endpoint,
*,
methods: Sequence[str] = ("GET",),
name: str | None = None,
guarded: bool = True,
) -> None
Answer one more path, called as endpoint(admin, request).
The path has to start with /-/, which no model name can take, and
the route sits behind the admin's sign in unless guarded is off.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
The path under the admin, such as "/-/reports/export". |
required |
endpoint
|
Endpoint
|
An async function given the admin and the request, which answers with a response. |
required |
methods
|
Sequence[str]
|
The HTTP methods it answers. |
('GET',)
|
name
|
str | None
|
The route's name, for building its URL. |
None
|
guarded
|
bool
|
False to answer people who have not signed in. |
True
|
Raises:
| Type | Description |
|---|---|
AdminSiteError
|
When the path is not under /-/, or the admin has already answered its first request. |
add_static ¶
add_static(name: str, directory: str | Path) -> None
Serve a folder of files at /-/static/ and the name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The folder's name in the URL. |
required |
directory
|
str | Path
|
The folder on disk. |
required |
Raises:
| Type | Description |
|---|---|
AdminSiteError
|
When the admin has already answered its first request. |
add_template_dir ¶
add_template_dir(directory: str | Path) -> None
Look for templates in one more folder, before the built in ones.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
directory
|
str | Path
|
The folder on disk. |
required |
add_stylesheet ¶
add_stylesheet(href: str) -> None
Load a stylesheet on every page.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
href
|
str
|
The stylesheet's address. A relative one starts at the admin, such as "-/static/reports/reports.css". |
required |
add_script ¶
add_script(src: str) -> None
Load a script on every page.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
src
|
str
|
The script's address. A relative one starts at the admin. |
required |
render_template
async
¶
render_template(
name: str,
request: Request,
context: dict[str, Any] | None = None,
status_code: int = 200,
) -> Response
Render a template of your own, found in the admin's template dirs.
The template gets what every page of the admin has, such as the
sidebar, as well as context.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The template's path inside one of the template folders. |
required |
request
|
Request
|
The request being answered. |
required |
context
|
dict[str, Any] | None
|
More values for the template. |
None
|
status_code
|
int
|
The response's status. |
200
|
Returns:
| Type | Description |
|---|---|
Response
|
The page, as an HTML response. |
adminsite.database.Database ¶
Database(source: SessionSource)
Hands out sessions, hiding whether the engine is async or not.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
SessionSource
|
An engine, an async engine, or a session factory for either,
as |
required |
session
async
¶
session() -> AsyncIterator[SessionAdapter]
Open a session and close it when the block ends.
Returns:
| Type | Description |
|---|---|
AsyncIterator[SessionAdapter]
|
A context manager that hands over the session: |
adminsite.database.SessionAdapter ¶
SessionAdapter()
Bases: ABC
One way to talk to the database, whether the session is async or not.
execute
abstractmethod
async
¶
execute(statement: Executable) -> Result
Run a statement and return its result.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
statement
|
Executable
|
A select, an insert, an update or a delete. |
required |
Returns:
| Type | Description |
|---|---|
Result
|
SQLAlchemy's result, rows read as they come. |
scalar
abstractmethod
async
¶
scalar(statement: Executable) -> Any
Run a statement and return a single value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
statement
|
Executable
|
A select of one value, such as a count. |
required |
Returns:
| Type | Description |
|---|---|
Any
|
The first column of the first row, or None without one. |
scalars
abstractmethod
async
¶
scalars(statement: Executable) -> ScalarResult[Any]
Run a statement and return the first column of every row.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
statement
|
Executable
|
A select, usually of whole records. |
required |
Returns:
| Type | Description |
|---|---|
ScalarResult[Any]
|
The values, such as the records a |
get
abstractmethod
async
¶
get(model: type[T], key: Any) -> T | None
Load one record by primary key, or nothing if it is missing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
type[T]
|
The model to load. |
required |
key
|
Any
|
The primary key, a tuple for a composite one. |
required |
Returns:
| Type | Description |
|---|---|
T | None
|
The record, or None. |
add
abstractmethod
async
¶
add(record: Any) -> None
Put a new record into the session.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
record
|
Any
|
The record, inserted at the next flush or commit. |
required |
delete
abstractmethod
async
¶
delete(record: Any) -> None
Mark a record for deletion.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
record
|
Any
|
The record, deleted at the next flush or commit. |
required |
flush
abstractmethod
async
¶
flush() -> None
Send pending changes to the database without committing.
commit
async
¶
commit() -> None
Commit the open transaction, then run the work waiting on it.
If the work before it or the commit itself fails, everything is rolled back, so what waits on a rollback runs. A change the database refuses raises adminsite's IntegrityError.
run
abstractmethod
async
¶
run(work: Callable[[Session], T]) -> T
Run a function against the underlying sync session.
For SQLAlchemy calls the adapter has no method for: through
run_sync with an async engine, and on the session's own thread
with a sync one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
work
|
Callable[[Session], T]
|
A function given SQLAlchemy's sync |
required |
Returns:
| Type | Description |
|---|---|
T
|
What the function returns. |
adminsite.inspector.SQLAlchemyInspector ¶
SQLAlchemyInspector()
Describes SQLAlchemy models: their columns, relationships and keys.
Views¶
adminsite.ModelView ¶
ModelView(
inspector: SQLAlchemyInspector | None = None,
registry: FieldRegistry | None = None,
)
Bases: Generic[M]
How one model appears in the admin: class OrderView(ModelView[Order]).
class OrderView(ModelView[Order]):
fields = [Order.id, Order.customer, Order.status, Order.total]
searchable_fields = [Order.id, Link(Order.customer, Customer.name)]
fields_default_sort = [Descending(Order.created_at)]
fields lists every field once, in order, for every page. The other
class attributes describe the list and the forms, and the get_
methods and can_access_field answer per request, for when the answer
depends on who is asking. A view with no settings needs no class:
register ModelView[Tag].
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
inspector
|
SQLAlchemyInspector | None
|
Reads the model's columns and relationships. The admin passes its own when it builds the view from its class. |
None
|
registry
|
FieldRegistry | None
|
Picks the field for each type of column. The admin
passes its |
None
|
name
class-attribute
instance-attribute
¶
name: str = ''
The view's name in its URLs, such as orders in /admin/orders.
Left empty, the model's name, plural and in snake case: order_items.
Two views of one model each need a name of their own.
label
class-attribute
instance-attribute
¶
label: str = ''
What one record is called in headings and buttons, such as "Order".
Left empty, the model's name in words: OrderItem reads "Order item".
label_plural
class-attribute
instance-attribute
¶
label_plural: str = ''
What the records are called in the sidebar and the list's heading.
Left empty, the model's name in words, made plural: "Order items".
group
class-attribute
instance-attribute
¶
group: str = ''
The sidebar section the view sits under, such as "Sales".
Views with no group sit together, under no heading.
icon
class-attribute
instance-attribute
¶
icon: str = ''
The view's icon in the sidebar: inline SVG markup, or a picture's address.
Markup goes into the page as it is, so keep it to icons you control. A
relative address starts at the admin, so a plugin's add_static folder
works.
record_title
class-attribute
instance-attribute
¶
record_title: str = ''
How a record is named in headings, links to it and the history.
A template of the model's columns, such as "Order #{id}" or
"{name} ({email})", each name checked when the admin starts. Left
empty, the model's own __str__, or else the view's label and the
record's key, "Order #12". Links from other models name the record the
same way. get_record_title names one no template can.
fields
class-attribute
instance-attribute
¶
fields: Sequence[
ColumnReference | Field[Any] | ComputedField[M, Any]
] = ()
Every field of the view, once, in the order each page shows them.
A column is named by its attribute, Order.total, by a Link for a
column of a related model, or by its name as a string, "total" or
"customer.email". A field, such as Field(Order.total,
read_only=True), gives the column options. Left empty, the view shows
every column of the model, a foreign key as its relationship.
exclude_fields_from_list
class-attribute
instance-attribute
¶
exclude_fields_from_list: Sequence[ColumnReference] = ()
Fields left off the list, as exclude_from_list on a field does.
exclude_fields_from_detail
class-attribute
instance-attribute
¶
exclude_fields_from_detail: Sequence[ColumnReference] = ()
Fields left off the record page, as exclude_from_detail on a field does.
exclude_fields_from_create
class-attribute
instance-attribute
¶
exclude_fields_from_create: Sequence[ColumnReference] = ()
Fields left off the form for a new record, as exclude_from_create does.
exclude_fields_from_edit
class-attribute
instance-attribute
¶
exclude_fields_from_edit: Sequence[ColumnReference] = ()
Fields left off the edit form, as exclude_from_edit on a field does.
exclude_fields_from_export
class-attribute
instance-attribute
¶
exclude_fields_from_export: Sequence[ColumnReference] = ()
Fields left out of the CSV export, as exclude_from_export on a field does.
searchable_fields
class-attribute
instance-attribute
¶
searchable_fields: Sequence[ColumnReference] = ()
The columns the search box looks in.
Text matches anywhere in the value, a number only exactly. Left empty, the list has no search box, and the command palette skips the view.
sortable_fields
class-attribute
instance-attribute
¶
sortable_fields: Sequence[ColumnReference] | None = None
The columns people may sort the list by.
Left out, every column the list shows, but no relationship and no column reached through one holding many records. An empty list, none.
fields_default_sort
class-attribute
instance-attribute
¶
fields_default_sort: Sequence[
ColumnReference | Descending
] = ()
The order the list starts in.
Descending(Order.created_at), or "-created_at", sorts newest first.
Left empty, the list goes by the record's key.
list_filters
class-attribute
instance-attribute
¶
list_filters: Sequence[ColumnReference | SQLFilter[M]] = ()
The filters beside the list.
A column gets the filter that suits its type; a SQLFilter of your own
decides for itself.
inline_editable_fields
class-attribute
instance-attribute
¶
inline_editable_fields: Sequence[ColumnReference] = ()
The fields whose values can be changed straight from the list.
A cell of each opens a small editor under it, which saves that one value
as the edit form does: allows, can_access_field,
get_readonly_fields, the save hooks and the audit log all apply, and a
cell this user may not change stays plain text. The list's first column
opens the record, so it cannot be one of them.
page_size
class-attribute
instance-attribute
¶
page_size: int = 25
How many rows a page of the list holds.
page_size_options
class-attribute
instance-attribute
¶
page_size_options: Sequence[int] = ()
The page sizes people may switch between, such as [25, 100, 500].
Left empty, the size stays at page_size.
count_mode
class-attribute
instance-attribute
¶
count_mode: CountMode = CountMode.EXACT
How hard the list works to say how many records match.
The rows never wait for it: the total follows them in a request of its
own. CountMode.ESTIMATED suits a table of millions of rows on Postgres
or MySQL, and CountMode.NONE skips the count. Either also leaves the
counts off the filters' options, unless a filter is given
show_counts=True.
global_search
class-attribute
instance-attribute
¶
global_search: bool = True
Whether the command palette looks through this view's records.
pagination
class-attribute
instance-attribute
¶
pagination: Pagination = Pagination.OFFSET
How the list moves between pages: page numbers, or a keyset for big tables.
list_refresh_seconds
class-attribute
instance-attribute
¶
list_refresh_seconds: int | None = None
How often an open list reads its rows again, in seconds, such as 30.
New and changed records then show up without a reload, under the search, the filters, the sort and the page in use. The list waits while someone is at work on it, with a row ticked, a menu, a dialog or a value's editor open, the focus in the table or its text selected, and while its tab is hidden, which asks the database nothing; shown again, it reads them at once. Left None, a list changes only when someone asks it to.
deferred_fields
class-attribute
instance-attribute
¶
deferred_fields: Sequence[ColumnReference] = ()
Columns the list leaves out of its query, such as a large JSON payload.
The record page, the form and the API load them as usual. A column the
list shows is loaded whatever this says, as is anything record_title
reads.
inlines
class-attribute
instance-attribute
¶
inlines: Sequence[Inline] = ()
Child records edited inside this model's form, such as order lines.
form_layout
class-attribute
instance-attribute
¶
form_layout: Sequence[LayoutEntry] = ()
How the forms and the record page arrange the fields.
Panels, fieldsets, rows and tabs, built from PanelWidget,
FieldsetWidget, RowWidget and TabsWidget; a tuple is a row and a
list a column. Left empty, one field under another, in the order of
fields. A field the layout leaves out comes last.
can_create
class-attribute
instance-attribute
¶
can_create: bool = True
Whether records can be added. allows decides per user.
can_view_detail
class-attribute
instance-attribute
¶
can_view_detail: bool = True
Whether records have a page of their own. Without it, rows open the form.
can_export
class-attribute
instance-attribute
¶
can_export: bool = True
Whether the list can be downloaded as CSV.
can_delete
class-attribute
instance-attribute
¶
can_delete: bool = True
Whether records can be deleted.
can_delete_selected
class-attribute
instance-attribute
¶
can_delete_selected: bool = True
Whether the ticked rows can be deleted together.
The Delete action then sits in the bar that rises when rows are ticked.
can_delete has to allow it too.
can_import
class-attribute
instance-attribute
¶
can_import: bool = False
Whether records can be imported from a CSV or Excel file.
Off until you switch it on, since it writes many records at once.
import_limit
class-attribute
instance-attribute
¶
import_limit: int = 10000
The most rows one imported file may hold.
in_sidebar
class-attribute
instance-attribute
¶
in_sidebar: bool = True
Whether the view is listed in the sidebar.
False leaves it out of the sidebar, the command palette's pages and the overview's counts, for a view whose records are only opened from other records. Its pages, links and pickers stay as they are.
can_access_field ¶
can_access_field(
request: Request,
field: BaseField,
action: RequestAction,
) -> bool
Whether this user sees the field on this page.
Every field is shown to everyone by default. Answer False to keep one from someone, such as a cost price from staff who are not managers: it leaves the page, the export and the API, and no form reads it back.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
The request, with the signed in user on
|
required |
field
|
BaseField
|
The field, whose |
required |
action
|
RequestAction
|
The page asking, such as |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True to show the field, False to keep it from this user. |
get_searchable_fields ¶
get_searchable_fields(
request: Request,
) -> Sequence[ColumnReference]
The columns the search box looks in, for this user.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
The request, with the signed in user on
|
required |
Returns:
| Type | Description |
|---|---|
Sequence[ColumnReference]
|
Columns, as |
search_condition ¶
search_condition(
term: str, *, request: Request
) -> ColumnElement[bool] | None
The condition the search box matches with, or None for the usual one.
The usual one looks for the term inside every search field, which a large table cannot answer from an index. Return a condition of your own, such as an exact match on a normalised phone number, and the list, its count, the export, "select all matching", the command palette and pickers all use it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
term
|
str
|
What was typed into the search box. |
required |
request
|
Request
|
The request, with the signed in user on
|
required |
Returns:
| Type | Description |
|---|---|
ColumnElement[bool] | None
|
A condition on the view's model, or None for the usual search. |
get_list_filters ¶
get_list_filters(
request: Request,
) -> Sequence[ColumnReference | SQLFilter[M]]
The filters offered beside the list, for this user.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
The request, with the signed in user on
|
required |
Returns:
| Type | Description |
|---|---|
Sequence[ColumnReference | SQLFilter[M]]
|
Columns and filters, as |
get_fields_default_sort ¶
get_fields_default_sort(
request: Request,
) -> Sequence[ColumnReference | Descending]
The order the list starts in, for this user.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
The request, with the signed in user on
|
required |
Returns:
| Type | Description |
|---|---|
Sequence[ColumnReference | Descending]
|
Columns, any of them |
get_deferred_fields ¶
get_deferred_fields(
request: Request,
) -> Sequence[ColumnReference]
The columns the list leaves out of its query, for this user.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
The request, with the signed in user on
|
required |
Returns:
| Type | Description |
|---|---|
Sequence[ColumnReference]
|
Columns, as |
get_readonly_fields ¶
get_readonly_fields(
request: Request, record: M | None
) -> Sequence[ColumnReference]
Fields shown on this record's form but not editable there.
A field with read_only=True is never editable. Answer with more
for one record or one user, such as the customer of a shipped order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
The request, with the signed in user on
|
required |
record
|
M | None
|
The record on the form, or None on the form for a new record. |
required |
Returns:
| Type | Description |
|---|---|
Sequence[ColumnReference]
|
The fields to lock as well, named as in |
get_inlines ¶
get_inlines(
request: Request, record: M | None
) -> Sequence[Inline]
The child records edited inside the form, for this user and record.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
The request, with the signed in user on
|
required |
record
|
M | None
|
The record on the form, or None on the form for a new record. |
required |
Returns:
| Type | Description |
|---|---|
Sequence[Inline]
|
Inlines from |
form_only_values
async
¶
form_only_values(
session: SessionAdapter,
record: M | None,
*,
request: Request,
) -> Mapping[str, Any]
The values form-only fields start from, by name.
Nothing by default, so they start empty. Answer with, say, settings kept as rows of another table, to edit them as one value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
SessionAdapter
|
The session the form is read in. |
required |
record
|
M | None
|
The record on the form, or None on the form for a new record. |
required |
request
|
Request
|
The request, with the signed in user on
|
required |
Returns:
| Type | Description |
|---|---|
Mapping[str, Any]
|
Each form-only field's starting value, by the field's name. |
get_record_title ¶
get_record_title(record: M) -> str
Name a record, for a heading, a link to it and the history.
record_title first, then the model's own __str__. A model with
neither is named by the view's label and the record's key, "Order
12", rather than by where it sits in memory.¶
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
record
|
M
|
The record, with its columns loaded but not its links. It is passed by position, so the parameter may be named after the model. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The record's name. |
get_actions ¶
get_actions(request: Request) -> Sequence[Action]
The actions this view offers this user, in the order they appear.
Delete comes last, where the view allows deleting several at once, unless the view has an action of its own by that name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
The request, with the signed in user on
|
required |
Returns:
| Type | Description |
|---|---|
Sequence[Action]
|
The view's actions, each a method marked with |
allows
async
¶
allows(
action: Permission | str,
*,
request: Request,
record: M | None,
) -> bool
Whether the current user may do this, to this record.
By default it answers from the can_ settings, such as can_edit,
and then from the grants the auth provider's grants_for gave the
user, if it gave any. Everything else is allowed. An override that
calls super().allows(...) keeps both.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
action
|
Permission | str
|
What the user wants to do: a |
required |
request
|
Request
|
The request, with the signed in user on
|
required |
record
|
M | None
|
The record the question is about, or None when it is about the view as a whole, such as whether its list may be exported. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True when the user may. |
scope_query ¶
scope_query(
statement: Statement, *, request: Request
) -> Statement
Narrow every read to the rows this user may see.
This runs on the list, the count, a single record, an export, a
bulk action and the history on the Activity page, so a row can never
leak through a path that forgot to check. A record it leaves out
reads as Hidden where another view links to it. To keep records off
the list but still reachable, such as deleted ones, use list_query.
Hand the statement back unchanged for a user it leaves nothing out for, such as a superuser. The Activity page then shows them the history of deleted records too, which no scope can be checked against.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
statement
|
Statement
|
A select of the view's model, about to run. |
required |
request
|
Request
|
The request, with the signed in user on
|
required |
Returns:
| Type | Description |
|---|---|
Statement
|
The statement, narrowed, such as with a |
list_query ¶
list_query(
statement: Statement, *, request: Request
) -> Statement
Narrow the list to the rows it shows, such as the ones not deleted.
Unlike scope_query, this leaves every record reachable. It runs on
the list, its count and filters, the export, "select all matching",
the command palette, the dashboard and the pickers on other forms,
but not on a record's own page or where another record links to it.
A record it leaves out still opens from a link, and is named there.
scope_query runs first, so this only ever narrows what the user
may see.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
statement
|
Statement
|
A select of the view's model, about to run. |
required |
request
|
Request
|
The request, with the signed in user on
|
required |
Returns:
| Type | Description |
|---|---|
Statement
|
The statement, narrowed, such as with a |
before_save
async
¶
before_save(context: SaveContext[M]) -> None
Runs before the values are written.
context.values[Order.slug].set(...) stores something other than
what was submitted.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
context
|
SaveContext[M]
|
The record, the values about to be written, the session and the request. |
required |
Raises:
| Type | Description |
|---|---|
RefusedError
|
To refuse the save, with a message for the user. Name a field to put the message beside its input. |
save_record
async
¶
save_record(context: SaveContext[M]) -> None
Writes the values onto the record and stores it, on a create or an edit.
Override it to store the record another way, such as through your
own code: context.values[Order.total].get() reads each value.
Call super().save_record(context) to keep the usual write and add
to it. It runs between before_save and after_save, in the same
transaction. The rows of the record's inline tables are on
context.record already, and are stored with it.
It stores context.record, which the page opens next, so a new
record has to end up in context.session.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
context
|
SaveContext[M]
|
The record, the values to write, the session and the
request. |
required |
Raises:
| Type | Description |
|---|---|
RefusedError
|
To refuse the save, with a message for the user. |
after_save
async
¶
after_save(context: SaveContext[M]) -> None
Runs after the flush, while the transaction is still open.
The record has its primary key by now, and the values the database set in its own columns.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
context
|
SaveContext[M]
|
The record, the values written, the session and the request. |
required |
Raises:
| Type | Description |
|---|---|
RefusedError
|
To roll the save back, with a message for the user. |
after_save_committed
async
¶
after_save_committed(context: SaveContext[M]) -> None
Runs once the save has committed, such as to send an email.
The change is stored by now, so nothing here can undo it: an error is written to the server's log, and the save still succeeds. The transaction is over, so write through a session of your own.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
context
|
SaveContext[M]
|
The record, the values written and the request. |
required |
before_delete
async
¶
before_delete(context: DeleteContext[M]) -> None
Runs before a record is deleted.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
context
|
DeleteContext[M]
|
The record, the session and the request. |
required |
Raises:
| Type | Description |
|---|---|
RefusedError
|
To refuse the delete, with a message for the user. |
delete_record
async
¶
delete_record(context: DeleteContext[M]) -> None
Removes the record from the database.
Override it to delete another way, such as by keeping the row and
filling in its deleted_at. Everything else stays as it is: the
Delete buttons, the permission, the hooks and the audit log. It runs
between before_delete and after_delete, in the same transaction,
for each record a Delete covers: from its page, from the list and
through the API.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
context
|
DeleteContext[M]
|
The record, the session and the request. |
required |
Raises:
| Type | Description |
|---|---|
RefusedError
|
To refuse the delete, with a message for the user. |
after_delete
async
¶
after_delete(context: DeleteContext[M]) -> None
Runs after the delete, while the transaction is still open.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
context
|
DeleteContext[M]
|
The record, the session and the request. |
required |
Raises:
| Type | Description |
|---|---|
RefusedError
|
To roll the delete back, with a message for the user. |
after_delete_committed
async
¶
after_delete_committed(context: DeleteContext[M]) -> None
Runs once the delete has committed.
As with after_save_committed, an error here is logged and the
delete still stands.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
context
|
DeleteContext[M]
|
The record and the request. |
required |
adminsite.RequestAction ¶
Bases: StrEnum
The page a field is asked for, as can_access_field is told it.
adminsite.Permission ¶
Bases: StrEnum
The things a user can be allowed to do with a view.
VIEW
class-attribute
instance-attribute
¶
VIEW = 'view'
Opening the view's list, and seeing it in the sidebar.
EDIT
class-attribute
instance-attribute
¶
EDIT = 'edit'
Changing a record. The permission an action asks for, unless it says.
DELETE
class-attribute
instance-attribute
¶
DELETE = 'delete'
Deleting a record, alone or with others.
VIEW_DETAIL
class-attribute
instance-attribute
¶
VIEW_DETAIL = 'detail'
Opening one record's page.
A view whose list says everything can switch it off.
HISTORY
class-attribute
instance-attribute
¶
HISTORY = 'history'
Reading the audit log.
A record's History tab, and the view's entries on the Activity page.
adminsite.views.ViewRegistry ¶
ViewRegistry()
Holds the views of one admin, in the order they were added.
adminsite.Link
dataclass
¶
Link(
relation: QueryableAttribute[Any],
column: QueryableAttribute[V_co] | Link[V_co],
)
Bases: Generic[V_co]
A column of a related model, such as Link(Order.customer, Customer.email).
A longer way nests links:
Link(Order.customer, Link(Customer.address, Address.city)).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
relation
|
QueryableAttribute[Any]
|
The relationship that leads to the model owning the
column, such as |
required |
column
|
QueryableAttribute[V_co] | Link[V_co]
|
The column on that model, or another |
required |
adminsite.ColumnReference
module-attribute
¶
ColumnReference: TypeAlias = (
str | QueryableAttribute[Any] | Link[Any]
)
A column named by its attribute, by a Link, or by its name as a string.
Such as Order.total, Link(Order.customer, Customer.email) or "total".
adminsite.Descending
dataclass
¶
Descending(column: ColumnReference)
A column sorted from the highest value down, such as newest first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
column
|
ColumnReference
|
The column, as a setting names it. The string
|
required |
adminsite.Inline
dataclass
¶
Inline(
relation: str | QueryableAttribute[Any],
fields: Sequence[ColumnReference | Field[Any]] = (),
label: str = "",
blank_rows: int = 1,
can_delete: bool = True,
record_title: str = "",
)
Child records edited inside their parent's form, such as order lines.
class OrderView(ModelView[Order]):
inlines = [
Inline(
Order.items,
fields=[OrderItem.product, OrderItem.quantity, OrderItem.unit_price],
)
]
Each child shows as a row of inputs, with a button to remove it, and new rows can be added in the form. Everything is saved in one transaction with the parent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
relation
|
str | QueryableAttribute[Any]
|
The relationship on the parent that holds the children,
by attribute or by name, such as |
required |
fields
|
Sequence[ColumnReference | Field[Any]]
|
The child's fields, in order, written as a view's are, so
|
()
|
label
|
str
|
The heading above the rows. Left empty, the relationship's name. |
''
|
blank_rows
|
int
|
How many blank rows the table starts with while it has no rows yet. |
1
|
can_delete
|
bool
|
Whether rows can be removed, which deletes the child. |
True
|
record_title
|
str
|
How a child is named, as a view's |
''
|
adminsite.PanelWidget
dataclass
¶
PanelWidget(
title: str,
children: Sequence[LayoutEntry] = (),
description: str = "",
collapsible: bool = False,
collapsed: bool = False,
)
Fields under a titled card, on the forms and the record page.
class ProductView(ModelView[Product]):
form_layout = [
PanelWidget("Product", [Product.name, Product.description]),
PanelWidget("Price", [(Product.price, Product.cost)]),
]
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
title
|
str
|
The card's title. A panel holding one field leaves that field's label to it. |
required |
children
|
Sequence[LayoutEntry]
|
The fields and widgets inside, in order: a tuple of them is a row and a list a column, as everywhere in a layout. |
()
|
description
|
str
|
A line under the title, saying what the fields are for. |
''
|
collapsible
|
bool
|
Whether the panel can be folded away. |
False
|
collapsed
|
bool
|
Whether it starts folded. A folded panel opens when it holds a mistake. |
False
|
adminsite.FieldsetWidget
dataclass
¶
FieldsetWidget(
legend: str, children: Sequence[LayoutEntry] = ()
)
Fields in a bordered group with a caption, inside a panel.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
legend
|
str
|
The caption on the border. |
required |
children
|
Sequence[LayoutEntry]
|
The fields and widgets inside, as a panel takes them. |
()
|
adminsite.RowWidget
dataclass
¶
RowWidget(children: Sequence[LayoutEntry] = ())
Fields side by side on a wide screen, one under the other on a phone.
A tuple of fields is a row too: (Product.price, Product.stock).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
children
|
Sequence[LayoutEntry]
|
The fields and widgets side by side, in order. |
()
|
adminsite.TabsWidget
dataclass
¶
TabsWidget(tabs: Sequence[tuple[str, LayoutEntry]] = ())
Fields split into tabs, as (label, fields) pairs, one tab shown at a time.
The form opens on the first tab holding a mistake, and marks each tab that holds one. The record page shows each tab as a panel of its own.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tabs
|
Sequence[tuple[str, LayoutEntry]]
|
Each tab as a pair of its label and what it holds: a field, a widget, or a list of them. |
()
|
adminsite.CountMode ¶
Bases: StrEnum
How hard to work to report how many records match.
ESTIMATED
class-attribute
instance-attribute
¶
ESTIMATED = 'estimated'
The row count the database keeps, where that is cheap and close enough.
Postgres and MySQL keep one in their statistics. It is used when nothing narrows the list and the table holds more than 10,000 rows, shown as "about 2,500,000". A search, a filter or a scope is counted exactly. SQLite keeps no estimate, so it counts exactly.
NONE
class-attribute
instance-attribute
¶
NONE = 'none'
No count at all.
The pager learns whether there is a next page by reading one row more, so a page is a single query.
adminsite.Pagination ¶
Bases: StrEnum
How the list moves from one page to the next.
OFFSET
class-attribute
instance-attribute
¶
OFFSET = 'offset'
Page numbers.
Simple, but the database still walks every row before the page, so deep pages of a big table get slow.
KEYSET
class-attribute
instance-attribute
¶
KEYSET = 'keyset'
Continue after the last row seen, so every page costs the same.
There is no jumping to page 40, only Previous and Next. A sort by a column that can be empty, or through a relationship, falls back to page numbers.
adminsite.SaveContext
dataclass
¶
SaveContext(
session: SessionAdapter,
record: M,
values: SaveValues,
created: bool,
request: Request,
)
Bases: Generic[M]
What a save hook is given: SaveContext[Order] for an order's save.
before_save, save_record and after_save run inside the save's
transaction, after_save_committed once it has committed. In before_save the
values have not been written onto the record yet, so that is where to
change them: context.values[Order.slug].set(...). Setting an
attribute on context.record there would be overwritten a moment
later by the value from the form.
session
instance-attribute
¶
session: SessionAdapter
The session the save runs in, for reading and writing other tables.
values
instance-attribute
¶
values: SaveValues
The values the save stores, each read and changed by its column.
created
instance-attribute
¶
created: bool
Whether the save adds a new record rather than changing one.
adminsite.views.SaveValues ¶
SaveValues(
record: object,
stored: Mapping[str, Any],
*,
form_only: Callable[[str], bool] = lambda path: False,
)
The values a save stores, each named by its column: values[Order.note].
A string names a column too, or a value the form asks for that is no
column, such as a password to hash: values["password"]. in tells
whether the save stores a value for a column at all. adminsite builds it
for each save and hands it to the hooks as context.values.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
record
|
object
|
The record being saved. |
required |
stored
|
Mapping[str, Any]
|
The values the save stores, by path. |
required |
form_only
|
Callable[[str], bool]
|
Says whether a name is one of the view's form-only fields. |
lambda path: False
|
adminsite.views.SaveValue ¶
SaveValue(values: SaveValues, path: str)
Bases: Generic[T]
One column's value in a save, which a hook reads and can change.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
values
|
SaveValues
|
The save's values. |
required |
path
|
str
|
The column's name, or a dotted path such as "customer.email". |
required |
get ¶
get() -> T
The value the save stores in the column.
When the save leaves the column as it is, such as a read-only
field, that is the record's own value. On a new record, such a
column is None until the insert fills in its default, as the
record's own attribute is; read it in after_save.
Returns:
| Type | Description |
|---|---|
T
|
The value, of the column's type. |
Raises:
| Type | Description |
|---|---|
AdminSiteError
|
When the record does not hold the value yet, such
as a link it was loaded without, since reading it would need
a query. Put it in the view's fields, or read it with
|
set ¶
set(value: T) -> None
Store this value instead of the one given.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
T
|
The value to store, of the column's type. |
required |
adminsite.DeleteContext
dataclass
¶
DeleteContext(
session: SessionAdapter, record: M, request: Request
)
Bases: Generic[M]
What a delete hook is given: DeleteContext[Order] for an order's delete.
before_delete, delete_record and after_delete run inside the
delete's transaction, after_delete_committed once it has committed.
adminsite.Statement
module-attribute
¶
Statement = TypeVar('Statement', bound=Select[Any])
The select a view's scope_query is given, and hands back narrowed.
A type variable, so the hook returns the same kind of select it was given:
statement.where(...) does, and a new select(Order) is a type error.
Actions¶
adminsite.actions.action ¶
action(
label: str = "",
*,
name: str = "",
confirm: str = "",
permission: str = Permission.EDIT,
dangerous: bool = False,
icon: str = "",
inputs: Sequence[BaseField] = (),
on: ActionTarget = ON_SELECTION,
audit_answer: bool = True,
) -> Callable[[Handler], Handler]
Mark a method as an action.
from typing import Literal
class OrderView(ModelView[Order]):
@action("Mark as shipped", confirm="Mark the chosen orders as shipped?")
async def ship(
self, selection: Selection[Order], *, carrier: Literal["DHL", "UPS"]
) -> str:
orders = await selection.records()
for order in orders:
order.status = OrderStatus.SHIPPED
return f"{len(orders)} orders sent with {carrier}."
Each parameter the method does not get handed is asked for in a
dialog before the action runs, checked like a form field, and passed to
the method by name. Its type picks the input, and
Annotated[str, Input(label="Reason")] words it.
on says what it acts on:
"selection", the default: the rows the user ticked, handed to a parameter typedSelection[Order]."record": one record, from its row in the list or from its page, handed to a parameter typed with the view's model."view": nothing in particular. For work about the whole table, such as fetching from another system.
A parameter typed Request, AsyncSession or SessionAdapter is
handed the request or the session the action runs in.
A method returns the message to show, a Message for more than a line
of text, or a response to send instead, such as a file to download.
Raise RefusedError to stop it with a message; everything it did is
rolled back.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
label
|
str
|
The button's text. Left empty, the method's name in words:
|
''
|
name
|
str
|
The action's name in its URL and the audit log. Left empty, the method's name. |
''
|
confirm
|
str
|
A question asked in a dialog before it runs. |
''
|
permission
|
str
|
What the user needs to run it, as |
EDIT
|
dangerous
|
bool
|
Whether the button is drawn in red. A menu of actions lists these last, under a line. |
False
|
icon
|
str
|
An icon drawn before the label wherever the action is
offered: inline SVG markup, or a picture's address, as a view's
|
''
|
inputs
|
Sequence[BaseField]
|
Fields to ask for as they are, each passed to the parameter of its name. The parameters' types usually say enough. |
()
|
on
|
ActionTarget
|
What it acts on: "selection", "record" or "view", as above. |
ON_SELECTION
|
audit_answer
|
bool
|
Whether the audit log keeps the answer. False for one that holds something shown only once, such as a new API key; the entry still says who ran it, on what and when. |
True
|
Returns:
| Type | Description |
|---|---|
Callable[[Handler], Handler]
|
A decorator that marks the method and hands it back as it was. |
Raises:
| Type | Description |
|---|---|
AdminSiteError
|
When |
adminsite.actions.Selection
dataclass
¶
Selection(
view: ModelView[M],
session: SessionAdapter,
spec: QuerySpec,
request: Request,
keys: Sequence[str] = (),
everything: bool = False,
changes: dict[str, dict[str, Change]] = dict(),
)
Bases: Generic[M]
The rows an action runs over.
Either the rows that were ticked, or every row the current search and
filters match. The second kind is never loaded into memory, so an
action over a large table stays one statement. Selection[Order] holds
orders, so records() returns them as orders.
session
instance-attribute
¶
session: SessionAdapter
The session the action runs in, committed when it succeeds.
keys
class-attribute
instance-attribute
¶
keys: Sequence[str] = ()
The keys of the ticked rows, as the URLs write them.
everything
class-attribute
instance-attribute
¶
everything: bool = False
Whether it covers every row the search and filters match, not the keys.
update
async
¶
update(**values: Any) -> int
Change every row this covers, in one statement.
This does not run the save hooks, because it never loads the
records. Use records when the hooks matter.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**values
|
Any
|
The new values, by column name, such as
|
{}
|
Returns:
| Type | Description |
|---|---|
int
|
How many rows changed. |
delete
async
¶
delete() -> int
Delete every row this covers, in one statement.
Like update, it skips the hooks, and relies on the database for
cascades.
Returns:
| Type | Description |
|---|---|
int
|
How many rows were deleted. |
count
async
¶
count() -> int
How many rows this covers.
Returns:
| Type | Description |
|---|---|
int
|
The number of rows. |
records
async
¶
records(
*, paths: Sequence[ColumnReference] = ()
) -> list[M]
Load the records, for work that needs each one in turn.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
paths
|
Sequence[ColumnReference]
|
Links to load with them, such as |
()
|
Returns:
| Type | Description |
|---|---|
list[M]
|
The records, as the view's model. |
statement ¶
statement() -> Select[Any]
A statement selecting the primary keys this covers.
Returns:
| Type | Description |
|---|---|
Select[Any]
|
A |
covered_keys
async
¶
covered_keys() -> list[str]
The keys of the records this covers, written as the URLs write them.
Returns:
| Type | Description |
|---|---|
list[str]
|
The keys, a composite one with its parts joined by commas. |
adminsite.actions.Input
dataclass
¶
Input(
*,
label: str = "",
help_text: str = "",
multiline: bool = False,
accept: str = "",
max_size: int | None = None,
secret: bool | None = None,
)
How an action asks for one value, written in the parameter's annotation.
from typing import Annotated
from adminsite.actions import Input
reason: Annotated[str, Input(label="Reason", multiline=True)]
An option the type has no use for, such as multiline on an int,
stops the admin when it starts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
label
|
str
|
The text above the input. Left empty, the parameter's name
in words: |
''
|
help_text
|
str
|
A line under the input. |
''
|
multiline
|
bool
|
A box of several lines, for a |
False
|
accept
|
str
|
For an |
''
|
max_size
|
int | None
|
For an |
None
|
secret
|
bool | None
|
Whether the audit log keeps |
None
|
adminsite.Message
dataclass
¶
Message(
text: str,
link: str = "",
link_text: str = "",
copy: str = "",
)
What an action answers, when a line of text is not enough.
Message("The new key is ready.", copy="sk_live_k3j9x2")
Message("The export is on its way.", link="/admin/exports", link_text="Exports")
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
What happened. It is escaped, unless given as |
required |
link
|
str
|
An address to follow, written after the text. |
''
|
link_text
|
str
|
The words of the link. Left empty, "Open". |
''
|
copy
|
str
|
A value shown with a button that copies it, such as a new API key. The audit log, which keeps the text, never keeps it. |
''
|
Filters¶
adminsite.filters.SQLFilter ¶
SQLFilter(
name: str,
*,
path: str | None = None,
label: str | None = None,
)
Bases: Filter, Generic[M]
A filter that narrows a SQLAlchemy statement: SQLFilter[Product].
Write a custom filter by subclassing this, offering its choices from
options and returning a condition from condition. Override apply
instead when the filter needs to change the statement itself, for
example to add a join.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The filter's name in the URL, such as "delivery" in
|
required |
path
|
str | None
|
The column it reads, such as "customer.region". Left out, the name. |
None
|
label
|
str | None
|
The filter's label in the list's toolbar. Left out, the name in words. |
None
|
multiple
class-attribute
instance-attribute
¶
multiple = False
Whether several options can be picked at once, all in value.values.
template
class-attribute
instance-attribute
¶
template = 'choice'
The control the list draws: "choice", "range", "relation" or "text".
show_counts
class-attribute
instance-attribute
¶
show_counts: bool | None = None
Whether each option says how many records it matches.
Counting reads every record the list may show, so None, the default,
leaves it to the view: the options are counted where its count_mode is
CountMode.EXACT, and not on a view too big to count.
options
async
¶
options(context: FilterContext) -> Sequence[FilterOption]
List the choices to offer. Empty when the filter is free text.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
context
|
FilterContext
|
Asks the database for counts and values, within the
view's scope, such as |
required |
Returns:
| Type | Description |
|---|---|
Sequence[FilterOption]
|
The choices, in the order they are offered. |
condition ¶
condition(
value: FilterValue, repository: SQLAlchemyRepository[M]
) -> ColumnElement[bool] | None
The condition this filter adds, or nothing to leave the list be.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
FilterValue
|
What the user picked: |
required |
repository
|
SQLAlchemyRepository[M]
|
The repository of the view's model. |
required |
Returns:
| Type | Description |
|---|---|
ColumnElement[bool] | None
|
A condition on the view's model, or None to leave the list be. |
apply ¶
apply(
statement: Select[Any],
value: FilterValue,
repository: SQLAlchemyRepository[M],
) -> Select[Any]
Narrow the statement with this filter's condition.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
statement
|
Select[Any]
|
The select the list is about to run. |
required |
value
|
FilterValue
|
What the user picked. |
required |
repository
|
SQLAlchemyRepository[M]
|
The repository of the view's model. |
required |
Returns:
| Type | Description |
|---|---|
Select[Any]
|
The statement, narrowed. |
adminsite.filters.FilterOption
dataclass
¶
FilterOption(
value: str, label: str, count: int | None = None
)
One choice a filter offers, with how many records it would match.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
str
|
What the URL carries when it is picked, such as "late". |
required |
label
|
str
|
What the list shows for it, such as "Overdue". |
required |
count
|
int | None
|
How many records it would match, shown beside it. None shows no count. |
None
|
adminsite.filters.FilterValue
dataclass
¶
FilterValue(
name: str, values: tuple[str, ...], source: Any = None
)
What the user picked for one filter.
values
instance-attribute
¶
values: tuple[str, ...]
Every value picked, as text, in the order the URL carries them.
source
class-attribute
instance-attribute
¶
source: Any = field(default=None, compare=False, repr=False)
The filter these were read for.
So a filter that get_list_filters adds for a single request is applied
by the query that request makes.
adminsite.filters.ChoiceFilter ¶
ChoiceFilter(
name: str,
*,
choices: Sequence[tuple[str, str]] = (),
show_counts: bool | None = None,
**options: Any,
)
Bases: SQLFilter[Any]
Pick one or more values from a fixed list, such as a status.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The filter's name in the URL. |
required |
choices
|
Sequence[tuple[str, str]]
|
The options as (value, label) pairs. |
()
|
show_counts
|
bool | None
|
Whether each option says how many records it matches. Left out, they are counted where the view counts its records exactly. |
None
|
**options
|
Any
|
|
{}
|
adminsite.filters.BooleanFilter ¶
BooleanFilter(
name: str,
*,
yes_label: str = "Yes",
no_label: str = "No",
show_counts: bool | None = None,
**options: Any,
)
Bases: SQLFilter[Any]
A yes or no column.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The filter's name in the URL. |
required |
yes_label
|
str
|
What the option for yes says. |
'Yes'
|
no_label
|
str
|
What the option for no says. |
'No'
|
show_counts
|
bool | None
|
Whether each option says how many records it matches. Left out, they are counted where the view counts its records exactly. |
None
|
**options
|
Any
|
|
{}
|
adminsite.filters.NumberRangeFilter ¶
NumberRangeFilter(
name: str,
*,
presets: Sequence[tuple[str, str]] = (),
**options: Any,
)
Bases: SQLFilter[Any]
A range of numbers, written as min,max with either side empty.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The filter's name in the URL. |
required |
presets
|
Sequence[tuple[str, str]]
|
Ranges offered as options, as (value, label) pairs, each
value written as |
()
|
**options
|
Any
|
|
{}
|
adminsite.filters.DateRangeFilter ¶
DateRangeFilter(
name: str,
*,
now: datetime | None = None,
**options: Any,
)
Bases: SQLFilter[Any]
A period, either one of the shortcuts or from,to as dates.
The dates are days on the clock of the person filtering: a day starts at their midnight, wherever the database keeps its times.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The filter's name in the URL. |
required |
now
|
datetime | None
|
The moment the shortcuts, such as the last 7 days, count from, in the database's time zone unless it has one of its own. Left out, the time each request is read. |
None
|
**options
|
Any
|
|
{}
|
adminsite.filters.RelationFilter ¶
RelationFilter(
name: str, *, key: str = "id", **options: Any
)
Bases: SQLFilter[Any]
Records linked to one of the chosen related records.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The filter's name in the URL. |
required |
key
|
str
|
The related model's column the chosen values are matched against. |
'id'
|
**options
|
Any
|
|
{}
|
adminsite.filters.TextFilter ¶
TextFilter(
name: str,
*,
path: str | None = None,
label: str | None = None,
)
Fields¶
adminsite.Field
dataclass
¶
Field(
column: QueryableAttribute[V] | Link[V] | str,
*,
label: str = "",
help_text: str = "",
empty_text: str = "",
required: bool | None = None,
read_only: bool = False,
max_length: int | None = None,
default: V | None = None,
format: str | None = None,
secret: bool | None = None,
form_only: bool = False,
exclude_from_list: bool = False,
exclude_from_detail: bool = False,
exclude_from_create: bool = False,
exclude_from_edit: bool = False,
exclude_from_export: bool = False,
hidden_in_list: bool = False,
)
Bases: BaseField, Generic[V]
A column's field: Field(Order.created_at, label="Placed").
On its own it is the field adminsite picks for the column, with the
options given. The kinds, such as DecimalField or TextAreaField,
are built on it and choose the field themselves. Either way, an option
left out comes from the column: its label, its length, whether it may
be empty.
Every option of BaseField applies as well.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
column
|
QueryableAttribute[V] | Link[V] | str
|
The column, by its attribute, by a |
required |
default
|
V | None
|
What a new record's input starts with, of the column's type of value. |
None
|
column_types
class-attribute
¶
column_types: tuple[type[Any], ...] = ()
The types of value this kind's column may hold, as V says them.
A view checks a column named by a string against them when it starts. Empty for a kind that takes any column.
adminsite.BaseField
dataclass
¶
BaseField(
*,
label: str = "",
help_text: str = "",
empty_text: str = "",
required: bool | None = None,
read_only: bool = False,
max_length: int | None = None,
default: Any = None,
format: str | None = None,
secret: bool | None = None,
form_only: bool = False,
exclude_from_list: bool = False,
exclude_from_detail: bool = False,
exclude_from_create: bool = False,
exclude_from_edit: bool = False,
exclude_from_export: bool = False,
hidden_in_list: bool = False,
)
What every field has: a label, the pages it is left off, how it reads.
Field and the kinds built on it show a column; ComputedField shows a
value worked out from the record. Every kind takes the options below,
and refuses one it never reads when the admin starts, such as
max_length on a date. A field of your own subclasses Field and says
how to show and read its value with the methods below.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
label
|
str
|
The field's label. Left empty, the column's label, or the
name written as words: |
''
|
help_text
|
str
|
A line under the input, saying how to fill it in. |
''
|
empty_text
|
str
|
What an empty value means, such as "Every region" for a multi-select left empty. The list and the record page show it faint, like a placeholder, where they would show a hyphen, so it never reads as a value someone set, and the export writes it into the empty cell. The form is unchanged. |
''
|
required
|
bool | None
|
Whether the form refuses the field left empty. Left as None, the column decides: required where it cannot be null and has no default. A default only fills in a new record, so on a record that exists, a field whose column cannot be null is required, unless it stores empty text. |
None
|
read_only
|
bool
|
Shown on the form and never read back from it, even if someone adds the input back by hand. |
False
|
max_length
|
int | None
|
The most characters the input takes. Left as None, the
column's length, as |
None
|
default
|
Any
|
What a new record's input starts with, and what an action's dialog opens with. A stored value always wins over it. |
None
|
format
|
str | None
|
How the value is written wherever it is shown, as
|
None
|
secret
|
bool | None
|
Whether the audit log keeps |
None
|
form_only
|
bool
|
Whether the field is an input that is no column. It is
on the form under a name of its own, never read from the record
or written to it, and its value reaches |
False
|
exclude_from_list
|
bool
|
Leaves the field off the list. |
False
|
exclude_from_detail
|
bool
|
Leaves the field off the record page. |
False
|
exclude_from_create
|
bool
|
Leaves the field off the form for a new record. |
False
|
exclude_from_edit
|
bool
|
Leaves the field off the edit form. |
False
|
exclude_from_export
|
bool
|
Leaves the field out of the CSV export. |
False
|
hidden_in_list
|
bool
|
Offers the field in the list's Columns menu, and keeps it off the list until someone turns it on. |
False
|
name
class-attribute
instance-attribute
¶
name: str = field(init=False)
The path the field shows, such as "total" or "customer.email".
widget
class-attribute
instance-attribute
¶
widget = 'text'
The name of the input the form draws, such as "text" or "number".
It picks the template adminsite/widgets/<widget>.html, or a plain
input where there is none. A project can override one or add its own.
python_type
class-attribute
¶
python_type: type[Any] = str
The type to_python checks submitted text against, with Pydantic.
error_message
class-attribute
instance-attribute
¶
error_message = 'Enter a valid value.'
What the form says when the text is not a value of python_type.
stored
class-attribute
instance-attribute
¶
stored = True
Whether the value is stored on the record.
A computed field is not, so it is never loaded, written, sorted or filtered.
keeps_value_when_blank
class-attribute
instance-attribute
¶
keeps_value_when_blank = False
Whether an input left empty on a record that exists keeps its value.
True for a password, so a form saved without one keeps the old.
stores_empty_text
class-attribute
instance-attribute
¶
stores_empty_text = False
Whether an input left empty stores "" where the column cannot be null.
True for text, and for a file, whose column keeps its key as text. Only a record that exists stores it: a new one is left to the column's default.
unused_options
class-attribute
¶
unused_options: frozenset[str] = frozenset()
The options this kind never reads, refused when the admin starts.
So a date field given max_length stops the admin starting instead of
refusing every date typed in.
display ¶
display(value: Any) -> str
Format the value for reading. An empty value shows as nothing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
Any
|
The value, as the record holds it. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The text the list, the record page and the export show. |
text_for ¶
text_for(record: Any, value: Any) -> str
The text shown for this value, given the record it belongs to.
Everything that shows a value goes through here: the list, the
record page and the export. Override it where the text depends on
another column, such as an amount that reads differently per
currency; override display where the value alone is enough.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
record
|
Any
|
The record the value belongs to. |
required |
value
|
Any
|
The value, as the record holds it. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The text shown, in the field's |
hint ¶
hint() -> str
How to fill the input in, shown under it when there is no help text.
A method, so it is worded in the language of each request.
Returns:
| Type | Description |
|---|---|
str
|
The hint, or nothing. |
serialize ¶
serialize(value: Any) -> str
Format the value for a form input.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
Any
|
The value, as the record holds it. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The text the input starts with, which |
parse ¶
parse(raw: str | None) -> Any
Turn submitted text into a Python value, or raise.
Empty text is None, or refused where the field is required. Anything
else goes to to_python, stripped.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raw
|
str | None
|
The text submitted for the field, or None when it is missing. |
required |
Returns:
| Type | Description |
|---|---|
Any
|
The value to store. |
Raises:
| Type | Description |
|---|---|
FieldValidationError
|
When the text is not a value the field takes, with a message for the form. |
to_python ¶
to_python(text: str) -> Any
Convert non empty text, assuming it is present and stripped.
Override it to read a value of your own, and call it through
super() to keep the checks on length and type.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The submitted text, stripped and not empty. |
required |
Returns:
| Type | Description |
|---|---|
Any
|
The value to store. |
Raises:
| Type | Description |
|---|---|
FieldValidationError
|
When the text is too long or not of
|
check_options ¶
check_options() -> None
Refuse options that do not fit together, once the column filled its part.
The view calls it for every field it is given, when it is built, so a mistake stops the admin starting rather than the page that shows it.
Raises:
| Type | Description |
|---|---|
AdminSiteError
|
When two options contradict each other, such as a tone given for a value the field does not have. |
adminsite.fields.ComputedField
dataclass
¶
ComputedField(
name: str,
getter: Callable[[M], V] | None = None,
*,
label: str = "",
help_text: str = "",
empty_text: str = "",
required: bool | None = None,
read_only: bool = False,
max_length: int | None = None,
default: V | None = None,
format: str | None = None,
secret: bool | None = None,
form_only: bool = False,
exclude_from_list: bool = False,
exclude_from_detail: bool = False,
exclude_from_create: bool = False,
exclude_from_edit: bool = False,
exclude_from_export: bool = False,
hidden_in_list: bool = False,
needs: Sequence[ColumnReference] = (),
load: Loader[M, V] | None = None,
)
Bases: BaseField, Generic[M, V]
A value the view works out from a record, rather than a column.
def capacity(product: Product) -> str:
return f"{len(product.slots)}/{product.limit}"
class ProductView(ModelView[Product]):
fields = [
Product.name,
ComputedField("capacity", capacity, needs=[Product.slots]),
]
It shows in the list, on the record page and in the export, and is never written, sorted or filtered. The function takes the view's model, so one written for another model is a type error.
A value that takes a query, such as a count of related rows, comes from
load instead, which runs once for the page, however many rows it
holds:
from collections.abc import Sequence
async def order_counts(
session: SessionAdapter, customers: Sequence[Customer]
) -> dict[int, int]:
rows = await session.execute(
select(Order.customer_id, func.count())
.where(Order.customer_id.in_([customer.id for customer in customers]))
.group_by(Order.customer_id)
)
return {customer_id: count for customer_id, count in rows.all()}
ComputedField("orders", load=order_counts, default=0)
It takes label, help_text, empty_text, format, hidden_in_list
and the flags that leave it off the list, the record page or the export,
as BaseField describes them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The field's name, such as "capacity". Its label is made
from it, unless |
required |
getter
|
Callable[[M], V] | None
|
A function given the record, which answers with the value. |
None
|
needs
|
Sequence[ColumnReference]
|
What the function reads, such as |
()
|
load
|
Loader[M, V] | None
|
An async function given the session and every record on the
page, which answers with each record's value by its primary
key, a tuple for a composite key. Give it or |
None
|
default
|
V | None
|
The value of a record |
None
|
Raises:
| Type | Description |
|---|---|
AdminSiteError
|
When it is given both |
adminsite.fields.StringField
dataclass
¶
StringField(
column: QueryableAttribute[V] | Link[V] | str,
*,
label: str = "",
help_text: str = "",
empty_text: str = "",
required: bool | None = None,
read_only: bool = False,
max_length: int | None = None,
default: V | None = None,
format: str | None = None,
secret: bool | None = None,
form_only: bool = False,
exclude_from_list: bool = False,
exclude_from_detail: bool = False,
exclude_from_create: bool = False,
exclude_from_edit: bool = False,
exclude_from_export: bool = False,
hidden_in_list: bool = False,
)
adminsite.fields.TextAreaField
dataclass
¶
TextAreaField(
column: QueryableAttribute[V] | Link[V] | str,
*,
label: str = "",
help_text: str = "",
empty_text: str = "",
required: bool | None = None,
read_only: bool = False,
max_length: int | None = None,
default: V | None = None,
format: str | None = None,
secret: bool | None = None,
form_only: bool = False,
exclude_from_list: bool = False,
exclude_from_detail: bool = False,
exclude_from_create: bool = False,
exclude_from_edit: bool = False,
exclude_from_export: bool = False,
hidden_in_list: bool = False,
)
adminsite.fields.EmailField
dataclass
¶
EmailField(
column: QueryableAttribute[V] | Link[V] | str,
*,
label: str = "",
help_text: str = "",
empty_text: str = "",
required: bool | None = None,
read_only: bool = False,
max_length: int | None = None,
default: V | None = None,
format: str | None = None,
secret: bool | None = None,
form_only: bool = False,
exclude_from_list: bool = False,
exclude_from_detail: bool = False,
exclude_from_create: bool = False,
exclude_from_edit: bool = False,
exclude_from_export: bool = False,
hidden_in_list: bool = False,
)
adminsite.fields.IntegerField
dataclass
¶
IntegerField(
column: QueryableAttribute[V] | Link[V] | str,
*,
label: str = "",
help_text: str = "",
empty_text: str = "",
required: bool | None = None,
read_only: bool = False,
max_length: int | None = None,
default: V | None = None,
format: str | None = None,
secret: bool | None = None,
form_only: bool = False,
exclude_from_list: bool = False,
exclude_from_detail: bool = False,
exclude_from_create: bool = False,
exclude_from_edit: bool = False,
exclude_from_export: bool = False,
hidden_in_list: bool = False,
)
adminsite.fields.FloatField
dataclass
¶
FloatField(
column: QueryableAttribute[V] | Link[V] | str,
*,
label: str = "",
help_text: str = "",
empty_text: str = "",
required: bool | None = None,
read_only: bool = False,
max_length: int | None = None,
default: V | None = None,
format: str | None = None,
secret: bool | None = None,
form_only: bool = False,
exclude_from_list: bool = False,
exclude_from_detail: bool = False,
exclude_from_create: bool = False,
exclude_from_edit: bool = False,
exclude_from_export: bool = False,
hidden_in_list: bool = False,
)
adminsite.fields.DecimalField
dataclass
¶
DecimalField(
column: QueryableAttribute[V] | Link[V] | str,
*,
label: str = "",
help_text: str = "",
empty_text: str = "",
required: bool | None = None,
read_only: bool = False,
max_length: int | None = None,
default: V | None = None,
format: str | None = None,
secret: bool | None = None,
form_only: bool = False,
exclude_from_list: bool = False,
exclude_from_detail: bool = False,
exclude_from_create: bool = False,
exclude_from_edit: bool = False,
exclude_from_export: bool = False,
hidden_in_list: bool = False,
)
adminsite.fields.BooleanField
dataclass
¶
BooleanField(
column: QueryableAttribute[V] | Link[V] | str,
*,
label: str = "",
help_text: str = "",
empty_text: str = "",
required: bool | None = None,
read_only: bool = False,
max_length: int | None = None,
default: V | None = None,
format: str | None = None,
secret: bool | None = None,
form_only: bool = False,
exclude_from_list: bool = False,
exclude_from_detail: bool = False,
exclude_from_create: bool = False,
exclude_from_edit: bool = False,
exclude_from_export: bool = False,
hidden_in_list: bool = False,
tones: Tone | Mapping[bool, Tone | None] | None = None,
)
Bases: Field[bool | None]
A yes or no value.
Its badge is green for yes and grey for no, unless tones says: for a
flag whose yes is the bad case, {True: "rose", False: None} draws a
rose badge when it is set and nothing when it is not.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tones
|
Tone | Mapping[bool, Tone | None] | None
|
The colour of each value's badge, by name, or one name for both. None for a value draws it with no badge. |
None
|
adminsite.fields.UUIDField
dataclass
¶
UUIDField(
column: QueryableAttribute[V] | Link[V] | str,
*,
label: str = "",
help_text: str = "",
empty_text: str = "",
required: bool | None = None,
read_only: bool = False,
max_length: int | None = None,
default: V | None = None,
format: str | None = None,
secret: bool | None = None,
form_only: bool = False,
exclude_from_list: bool = False,
exclude_from_detail: bool = False,
exclude_from_create: bool = False,
exclude_from_edit: bool = False,
exclude_from_export: bool = False,
hidden_in_list: bool = False,
)
adminsite.fields.DateField
dataclass
¶
DateField(
column: QueryableAttribute[V] | Link[V] | str,
*,
label: str = "",
help_text: str = "",
empty_text: str = "",
required: bool | None = None,
read_only: bool = False,
max_length: int | None = None,
default: V | None = None,
format: str | None = None,
secret: bool | None = None,
form_only: bool = False,
exclude_from_list: bool = False,
exclude_from_detail: bool = False,
exclude_from_create: bool = False,
exclude_from_edit: bool = False,
exclude_from_export: bool = False,
hidden_in_list: bool = False,
)
adminsite.fields.DateTimeField
dataclass
¶
DateTimeField(
column: QueryableAttribute[V] | Link[V] | str,
*,
label: str = "",
help_text: str = "",
empty_text: str = "",
required: bool | None = None,
read_only: bool = False,
max_length: int | None = None,
default: V | None = None,
format: str | None = None,
secret: bool | None = None,
form_only: bool = False,
exclude_from_list: bool = False,
exclude_from_detail: bool = False,
exclude_from_create: bool = False,
exclude_from_edit: bool = False,
exclude_from_export: bool = False,
hidden_in_list: bool = False,
with_timezone: bool | None = None,
)
Bases: Field[datetime | None]
A date together with a time, on the clock of the person reading it.
A time the column keeps without a zone is taken to be in the admin's
database_timezone. What a person types is read on their own clock,
and saved the way the column keeps it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
with_timezone
|
bool | None
|
Whether the column keeps the time zone, as one
declared |
None
|
adminsite.fields.TimeField
dataclass
¶
TimeField(
column: QueryableAttribute[V] | Link[V] | str,
*,
label: str = "",
help_text: str = "",
empty_text: str = "",
required: bool | None = None,
read_only: bool = False,
max_length: int | None = None,
default: V | None = None,
format: str | None = None,
secret: bool | None = None,
form_only: bool = False,
exclude_from_list: bool = False,
exclude_from_detail: bool = False,
exclude_from_create: bool = False,
exclude_from_edit: bool = False,
exclude_from_export: bool = False,
hidden_in_list: bool = False,
)
adminsite.fields.ListField
dataclass
¶
ListField(
column: QueryableAttribute[V] | Link[V] | str,
*,
label: str = "",
help_text: str = "",
empty_text: str = "",
required: bool | None = None,
read_only: bool = False,
max_length: int | None = None,
default: V | None = None,
format: str | None = None,
secret: bool | None = None,
form_only: bool = False,
exclude_from_list: bool = False,
exclude_from_detail: bool = False,
exclude_from_create: bool = False,
exclude_from_edit: bool = False,
exclude_from_export: bool = False,
hidden_in_list: bool = False,
item: Field[Any] | None = None,
)
Bases: Field[Sequence[Any] | None]
A list of values, such as a Postgres ARRAY column: one value per line.
Chosen for an array column by itself, with each value read by the field
its type calls for, so ARRAY(Integer) takes whole numbers and
ARRAY(String(2)) two letters at most. Give item to choose it:
ListField(Product.sizes, item=IntegerField("sizes"))
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
item
|
Field[Any] | None
|
The field each value is read and shown by. Left out, the one the column's type of item calls for. |
None
|
adminsite.fields.PasswordField
dataclass
¶
PasswordField(
column: QueryableAttribute[V] | Link[V] | str,
*,
label: str = "",
help_text: str = "",
empty_text: str = "",
required: bool | None = None,
read_only: bool = False,
max_length: int | None = None,
default: V | None = None,
format: str | None = None,
secret: bool | None = True,
form_only: bool = True,
exclude_from_list: bool = False,
exclude_from_detail: bool = False,
exclude_from_create: bool = False,
exclude_from_edit: bool = False,
exclude_from_export: bool = False,
hidden_in_list: bool = False,
)
Bases: StringField
A password to set: typed in, never shown, changed only when typed.
class UserView(ModelView[User]):
fields = [
User.email,
PasswordField("password", required=True),
]
async def before_save(self, context: SaveContext[User]) -> None:
password = context.values["password"].get()
if password:
context.values[User.password_hash].set(hash_password(password))
It is form only: what is typed reaches the hooks, never the record, and
the input is never filled in. required holds for a new record; on an
existing one, leaving it empty keeps the password it has.
adminsite.fields.FieldRegistry ¶
FieldRegistry()
Decides which field type to use for a column.
register ¶
register(
python_type: type[Any], field_class: type[Field[Any]]
) -> None
Use this field type for columns holding this Python type.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
python_type
|
type[Any]
|
The type of value a column holds, such as |
required |
field_class
|
type[Field[Any]]
|
The field adminsite picks for such a column. |
required |
adminsite.fields.default_registry
module-attribute
¶
default_registry = build_default_registry()
The registry every admin uses unless it is given another.
Register a field here to have it picked for every column of a type:
default_registry.register(Percentage, PercentField).
adminsite.fields.EnumField
dataclass
¶
EnumField(
column: QueryableAttribute[V] | Link[V] | str,
*,
label: str = "",
help_text: str = "",
empty_text: str = "",
required: bool | None = None,
read_only: bool = False,
max_length: int | None = None,
default: V | None = None,
format: str | None = None,
secret: bool | None = None,
form_only: bool = False,
exclude_from_list: bool = False,
exclude_from_detail: bool = False,
exclude_from_create: bool = False,
exclude_from_edit: bool = False,
exclude_from_export: bool = False,
hidden_in_list: bool = False,
enum: type[Enum] | None = None,
choices: Sequence[tuple[str, str]] = (),
multiple: bool = False,
tones: Tones | None = None,
)
Bases: Field[Any]
A value picked from a fixed set, such as a status.
The set comes from the column, an Enum or a string column with its
values listed, or from enum or choices given here:
EnumField(
Order.status, tones={OrderStatus.PAID: "green", OrderStatus.FAILED: "rose"}
)
EnumField(Product.size, choices=[("S", "Small"), ("L", "Large")])
Every option of BaseField applies as well, but max_length.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
column
|
QueryableAttribute[V] | Link[V] | str
|
The column, as |
required |
enum
|
type[Enum] | None
|
The Enum whose members are the options, for a string column. The options are keyed by each member's value, which is what the column stores. |
None
|
choices
|
Sequence[tuple[str, str]]
|
The options as (value, label) pairs. On an Enum column they relabel or narrow its members, each named by its name or its value, and the field still hands over the member. |
()
|
multiple
|
bool
|
Whether it holds several options at once, in the order they were picked, which suits a JSON column and an action that asks for a few categories. |
False
|
tones
|
Tones | None
|
Which colour each value's badge is, by name, so a failed status is rose wherever it sits among the choices. A value left out is grey, and None draws it with no badge. One name, such as "grey", colours every value alike. Left out, a value takes the tone of its place. |
None
|
adminsite.fields.RelationField
dataclass
¶
RelationField(
column: QueryableAttribute[V] | Link[V] | str,
*,
label: str = "",
help_text: str = "",
empty_text: str = "",
required: bool | None = None,
read_only: bool = False,
max_length: int | None = None,
default: V | None = None,
format: str | None = None,
secret: bool | None = None,
form_only: bool = False,
exclude_from_list: bool = False,
exclude_from_detail: bool = False,
exclude_from_create: bool = False,
exclude_from_edit: bool = False,
exclude_from_export: bool = False,
hidden_in_list: bool = False,
target: type[Any] | None = None,
collection: bool = False,
record_title: str | None = None,
view: type[ModelView[Any]] | str | None = None,
ordered: bool = False,
)
Bases: Field[Any]
A link to one or many other records: RelationField(Order.customer).
The relationship says which model it links to and whether it holds many
records. Every option of BaseField applies as well, but max_length.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
column
|
QueryableAttribute[V] | Link[V] | str
|
The relationship, such as |
required |
target
|
type[Any] | None
|
The model of the linked records. The view fills it in from the relationship. |
None
|
collection
|
bool
|
Whether it links to many records. The view fills it in from the relationship. |
False
|
record_title
|
str | None
|
How each linked record is named here, such as
|
None
|
view
|
type[ModelView[Any]] | str | None
|
The view its links open and its picker lists from, as a class or by its name, for a model shown by more than one view. Left out, the first view registered for that model. |
None
|
ordered
|
bool
|
For a link to many whose order means something, such as servers tried in turn. The form can put its records in order, and saving writes the link again in that order when it changed. |
False
|
Raises:
| Type | Description |
|---|---|
AdminSiteError
|
When |
adminsite.Html ¶
Bases: Markup
Text written into the page as markup instead of being escaped.
A field that returns it can put a link, a badge or an icon in a cell:
def tracking(order: Order) -> Html:
return Html('<a href="../{}">Track</a>').format(order.tracking_url)
ComputedField("tracking", tracking, needs=[Order.tracking_url])
Everything interpolated with format or % is escaped, so a value from
the database cannot carry markup of its own into the page. Anything
written straight into the string is not, so keep that to markup you wrote.
The CSV export and the JSON API send the text without the tags, since markup belongs on the page and not in a spreadsheet.
adminsite.fields.JSONField
dataclass
¶
JSONField(
column: QueryableAttribute[V] | Link[V] | str,
*,
label: str = "",
help_text: str = "",
empty_text: str = "",
required: bool | None = None,
read_only: bool = False,
max_length: int | None = None,
default: V | None = None,
format: str | None = None,
secret: bool | None = None,
form_only: bool = False,
exclude_from_list: bool = False,
exclude_from_detail: bool = False,
exclude_from_create: bool = False,
exclude_from_edit: bool = False,
exclude_from_export: bool = False,
hidden_in_list: bool = False,
schema: Any = None,
partial: bool = False,
)
Bases: Field[Any]
A JSON column: an object, a list, or any other JSON document.
Chosen for JSON columns by itself. The form edits the document in a code box that colours it and says where a mistake is while it is typed; the record page lays it out, each object and list folding on its line.
Give it a schema, a Pydantic type or a JSON Schema dict, and the form
is built from it instead: a switch for a yes or no, a number input that
keeps to its limits, a picker for a few fixed values, rows for a list of
objects. The document is checked against the schema when it is saved,
by Pydantic where the schema is a Pydantic type, and the record page
names each value by its title.
JSONField(Shop.settings, schema=ShopSettings)
Every option of BaseField applies as well, but max_length.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
column
|
QueryableAttribute[V] | Link[V] | str
|
The JSON column, as |
required |
schema
|
Any
|
A Pydantic type or a JSON Schema dict to build the form from and check the document against. Or a function given the record that answers with either, for a table whose rows hold documents of different shapes; it may answer None where the record has none, and the form is then a code box. |
None
|
partial
|
bool
|
Whether each property may be left unset, and only those set are saved, as an override that changes a few settings keeps them. |
False
|
adminsite.fields.FileField
dataclass
¶
FileField(
column: QueryableAttribute[V] | Link[V] | str,
*,
label: str = "",
help_text: str = "",
empty_text: str = "",
required: bool | None = None,
read_only: bool = False,
max_length: int | None = None,
default: V | None = None,
format: str | None = None,
secret: bool | None = None,
form_only: bool = False,
exclude_from_list: bool = False,
exclude_from_detail: bool = False,
exclude_from_create: bool = False,
exclude_from_edit: bool = False,
exclude_from_export: bool = False,
hidden_in_list: bool = False,
storage: FileStorage,
accept: str = "",
max_size: int = 10 * MEGABYTE,
)
Bases: Field[str | None]
An uploaded file, kept in a storage, its key kept in a string column.
FileField(Product.datasheet, storage=LocalStorage("uploads"), accept=".pdf")
The form shows the current file with a box to remove it. A new upload
replaces the old file, which is deleted once the save has committed;
deleting the record keeps its files. Every option of BaseField
applies as well, but max_length.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
column
|
QueryableAttribute[V] | Link[V] | str
|
The string column that keeps the file's key. |
required |
storage
|
FileStorage
|
Where the files go, such as |
required |
accept
|
str
|
The types taken, as a browser's |
''
|
max_size
|
int
|
The largest file taken, in bytes: 10 MB unless you say. |
10 * MEGABYTE
|
adminsite.fields.ImageField
dataclass
¶
ImageField(
column: QueryableAttribute[V] | Link[V] | str,
*,
label: str = "",
help_text: str = "",
empty_text: str = "",
required: bool | None = None,
read_only: bool = False,
max_length: int | None = None,
default: V | None = None,
format: str | None = None,
secret: bool | None = None,
form_only: bool = False,
exclude_from_list: bool = False,
exclude_from_detail: bool = False,
exclude_from_create: bool = False,
exclude_from_edit: bool = False,
exclude_from_export: bool = False,
hidden_in_list: bool = False,
storage: FileStorage,
accept: str = "image/png,image/jpeg,image/gif,image/webp",
max_size: int = 5 * MEGABYTE,
)
Bases: FileField
An uploaded picture, shown as a thumbnail in the list and the form.
Takes PNG, JPEG, GIF and WebP, checked by the file's first bytes, not
just its name. SVG is left out on purpose: it can carry script. The
options are FileField's, with accept set to those four types and
max_size to 5 MB.
adminsite.files.FileStorage ¶
Where uploaded files go.
Subclass it to keep files somewhere else, such as S3: save returns the
key kept in the column, and response answers a request for the file,
for example with a redirect to a signed URL.
save
async
¶
save(upload: UploadFile) -> str
Store an upload and return the key to keep in the column.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
upload
|
UploadFile
|
The uploaded file, as Starlette reads it. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The key the column keeps, which |
delete
async
¶
delete(key: str) -> None
Remove a stored file. A missing file is not an error.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The key |
required |
url ¶
url(key: str) -> str
A public address for the file, or nothing to serve it through the admin.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The key |
required |
Returns:
| Type | Description |
|---|---|
str
|
The address, or an empty string to have the admin serve the
file through |
response
async
¶
response(key: str, content_type: str = '') -> Response
Answer a request for the file, made through the admin.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The key |
required |
content_type
|
str
|
The file's type, guessed from its name. |
''
|
Returns:
| Type | Description |
|---|---|
Response
|
The file, or a redirect to where it is, such as a signed URL. |
adminsite.files.LocalStorage ¶
LocalStorage(
directory: str | Path, *, url_prefix: str = ""
)
Bases: FileStorage
Keeps files in a folder on this machine.
uploads = LocalStorage("uploads")
Files are served through the admin, behind its sign in, unless you give
url_prefix for a place your application already serves them from.
Each file gets a key such as 2026/09/k3j9x2-invoice.pdf: the month,
a random part so names never clash, and the original name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
directory
|
str | Path
|
The folder the files go in. |
required |
url_prefix
|
str
|
Where your application already serves the folder from, such as "https://cdn.example.com/uploads". Left empty, the admin serves the files. |
''
|
url ¶
url(key: str) -> str
The public address, when the files are served elsewhere.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The key |
required |
Returns:
| Type | Description |
|---|---|
str
|
The address under |
path_for ¶
path_for(key: str) -> Path
Where a key lives on disk, refusing any key that leaves the folder.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The key |
required |
Returns:
| Type | Description |
|---|---|
Path
|
The file's path inside the folder. |
Raises:
| Type | Description |
|---|---|
HTTPException
|
A 404, for a key that would leave the folder. |
Pages, plugins and the dashboard¶
adminsite.AdminPage ¶
AdminPage()
A page of your own inside the admin, such as a report or a settings form.
from typing import Any
class SalesReport(AdminPage):
label = "Sales report"
group = "Reports"
template = "reports/sales.html"
async def get_context(self, request: Request) -> dict[str, Any]:
async with self.admin.database.session() as session:
total = await session.scalar(select(func.sum(Order.total)))
return {"total": total}
The template lives in one of the admin's template_dirs and usually
extends adminsite/base.html, so the page keeps the sidebar and header.
It is served at /-/sales_report and listed in the sidebar and the
command palette.
name
class-attribute
instance-attribute
¶
name: str = ''
The page's name in its URL, /-/ and the name.
Left empty, the class's name in snake case, less a _page ending:
SalesReport is served at /-/sales_report.
label
class-attribute
instance-attribute
¶
label: str = ''
The page's name in the sidebar. Left empty, the name in words.
group
class-attribute
instance-attribute
¶
group: str = ''
The sidebar section the page sits under, as a view's group.
icon
class-attribute
instance-attribute
¶
icon: str = ''
The page's icon in the sidebar, as a view's icon.
template
class-attribute
instance-attribute
¶
template: str = ''
The template get renders, from one of the admin's template_dirs.
allows
async
¶
allows(request: Request) -> bool
Whether this user may open the page. Everyone may, unless you say.
Where the auth provider's grants_for gave the user grants, the
page follows them, under "-/" and its name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
The request, with the signed in user on
|
required |
Returns:
| Type | Description |
|---|---|
bool
|
True when the user may open it. A page they may not is left out of the sidebar and answers 403. |
get_context
async
¶
get_context(request: Request) -> dict[str, Any]
The values the template needs, on top of what every page has.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
The request being answered. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The values, by the names the template reads them by. The page
itself is there as |
get
async
¶
get(request: Request) -> Response
Show the page. Override it to answer with anything else.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
The request being answered. |
required |
Returns:
| Type | Description |
|---|---|
Response
|
The |
Raises:
| Type | Description |
|---|---|
AdminSiteError
|
When the page has no |
post
async
¶
post(request: Request, form: dict[str, Any]) -> Response
Handle a form posted to the page, already checked for its token.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
The request being answered. |
required |
form
|
dict[str, Any]
|
The submitted values, by input name. |
required |
Returns:
| Type | Description |
|---|---|
Response
|
What to answer, such as a redirect back to the page. By default a 405, since a page takes no forms until it says so. |
adminsite.Plugin ¶
Something that adds to an admin, packaged so it can be shared.
A plugin gets the admin once, in setup, and adds what it brings with
the same methods a project uses:
from pathlib import Path
HERE = Path(__file__).parent
class Reports(Plugin):
name = "reports"
def setup(self, admin: Admin) -> None:
admin.add_template_dir(HERE / "templates")
admin.add_static("reports", HERE / "static")
admin.add_stylesheet("-/static/reports/reports.css")
admin.add_page(SalesReport)
admin.add_route("/-/reports/export", export_sales)
admin = Admin(engine, plugins=[Reports()])
name
class-attribute
instance-attribute
¶
name: str = ''
A short name for the plugin, such as "reports".
adminsite.Widget ¶
One card on the overview page.
Subclass it for a card of your own: set template to a template in
your template_dirs and return what it needs from load. The template
gets widget and data.
width
class-attribute
instance-attribute
¶
width: int = 1
How many of the four columns the card spans on a wide screen.
template
class-attribute
instance-attribute
¶
template: str = ''
The template that draws the card, given widget and data.
allows
async
¶
allows(admin: Admin, request: Request) -> bool
Whether this user sees the card. Everyone does, unless you say.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
admin
|
Admin
|
The admin the overview belongs to. |
required |
request
|
Request
|
The request, with the signed in user on
|
required |
Returns:
| Type | Description |
|---|---|
bool
|
True to show the card. |
load
async
¶
load(admin: Admin, request: Request) -> Any
Read what the card shows.
A card whose load fails says it could not be loaded, and the rest
of the overview still loads.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
admin
|
Admin
|
The admin the overview belongs to, whose
|
required |
request
|
Request
|
The request being answered. |
required |
Returns:
| Type | Description |
|---|---|
Any
|
What the template gets as |
adminsite.Stat ¶
Stat(
title: str,
value: Source,
*,
previous: Source | None = None,
format: str = "{:,}",
hint: str = "",
link: str = "",
width: int = 1,
)
Bases: Widget
A single number, such as revenue this month, with an optional change.
Stat(
"Revenue this month",
select(func.sum(Order.total)).where(Order.created_at >= month_start),
previous=select(func.sum(Order.total)).where(
Order.created_at >= last_month_start, Order.created_at < month_start
),
format="€{:,.2f}",
)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
title
|
str
|
The card's title. |
required |
value
|
Source
|
Where the number comes from: a |
required |
previous
|
Source | None
|
The same for the period before. The card then shows the change, such as "+12% on the period before", in green or red. |
None
|
format
|
str
|
How the number is written, as |
'{:,}'
|
hint
|
str
|
A line under the number, when there is no change to show. |
''
|
link
|
str
|
Where clicking the card goes. A relative link starts at the admin, so "orders?status=PAID" opens the filtered list. |
''
|
width
|
int
|
How many of the four columns the card spans. |
1
|
adminsite.Chart ¶
Chart(
title: str,
rows: Source,
*,
kind: str = "bar",
format: str = "{:,}",
width: int = 2,
)
Bases: Widget
Bars or a line over labelled values, drawn as plain SVG.
The source gives rows of a label and a value, such as orders per day:
day = func.date(Order.created_at)
Chart(
"Orders per day",
select(day, func.count()).group_by(day).order_by(day),
kind="line",
)
Hovering a bar shows its value, and screen readers get the same numbers as a table. Dates as labels are written short, such as "Sep 14".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
title
|
str
|
The card's title. |
required |
rows
|
Source
|
Where the rows come from: a |
required |
kind
|
str
|
"bar" or "line". |
'bar'
|
format
|
str
|
How each value is written, as |
'{:,}'
|
width
|
int
|
How many of the four columns the card spans. |
2
|
adminsite.RecentRecords ¶
RecentRecords(
title: str,
view: str,
*,
sort: str = "",
value: str = "",
limit: int = 5,
width: int = 2,
)
Bases: Widget
The latest records of a view, linking to each.
RecentRecords("Latest orders", "orders", sort="-created_at", value="total")
It reads through the view, so the view's permissions and scope apply.
A field the view's can_access_field keeps from the user on the list is
not shown as value, and not used as sort.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
title
|
str
|
The card's title. |
required |
view
|
str
|
The view's name, as in its URL, such as "orders". |
required |
sort
|
str
|
The order, such as "-created_at". Left empty, the view's
|
''
|
value
|
str
|
A path shown at the end of each record, such as "total". |
''
|
limit
|
int
|
How many records. |
5
|
width
|
int
|
How many of the four columns the card spans. |
2
|
adminsite.ModelCounts ¶
adminsite.messages.add_message ¶
add_message(
request: Request,
text: str | Message,
kind: str = "info",
) -> None
Leave a message for the page the user lands on next.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
The request being answered. |
required |
text
|
str | Message
|
What to say, escaped unless given as |
required |
kind
|
str
|
"error" draws it as a problem; anything else, such as the default "info", as good news. |
'info'
|
Saved views¶
adminsite.SavedViews ¶
SavedViews(
source: Database | SessionSource | str = DEFAULT_URL,
*,
create_table: bool | None = None,
)
Bases: Store
Where people keep the lists they come back to.
Like the audit log, the views go to a SQLite file of their own by
default. Pass your engine to keep them in your database; the table is
then adminsite_saved_views on saved_view_metadata.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Database | SessionSource | str
|
Where the views are kept: a SQLite URL for a file of their own, or your engine or session factory. |
DEFAULT_URL
|
create_table
|
bool | None
|
Whether to create the table on first use. Left as None, only in the SQLite file adminsite owns, and never uninvited in your own database, where your migrations add it. |
None
|
visible_to
async
¶
visible_to(view: str, owner: str | None) -> list[SavedView]
A person's own views of a list, and the ones others shared.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
view
|
str
|
The name of the view whose list it is, such as "orders". |
required |
owner
|
str | None
|
Who is asking, as |
required |
Returns:
| Type | Description |
|---|---|
list[SavedView]
|
The views, by name. |
save
async
¶
save(saved: SavedView) -> SavedView
Keep a view, and return it with its id.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
saved
|
SavedView
|
The view to keep. |
required |
Returns:
| Type | Description |
|---|---|
SavedView
|
The same view, with the id it was stored under. |
delete
async
¶
delete(key: int, owner: str | None) -> bool
Remove a view, if it belongs to this person.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
int
|
The view's id. |
required |
owner
|
str | None
|
Who is asking, as |
required |
Returns:
| Type | Description |
|---|---|
bool
|
Whether a view was removed. |
Translations¶
adminsite.i18n.gettext ¶
gettext(text: str, **values: Any) -> str
Translate a piece of English text into the current language.
A project's own translations win over the built in ones, English included, so a word of the admin's own can be changed without copying the template it is in. Text with no translation stays in English rather than disappearing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The English text, with any values as named placeholders, such as "{thing} saved.". |
required |
**values
|
Any
|
The values for the placeholders, filled in after the text is translated. |
{}
|
Returns:
| Type | Description |
|---|---|
str
|
The text in the language of the current request. |
adminsite.i18n.ngettext ¶
ngettext(
singular: str, plural: str, number: int, **values: Any
) -> str
Translate a text with a count into the form its language writes for it.
English has two forms, "1 row" and "2 rows". Russian has three, and
Arabic six, so a translation holds one text for each plural category its
language uses, such as {"one": ..., "few": ..., "many": ...}, keyed by
the English singular.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
singular
|
str
|
The English text for one, such as "{count} row". |
required |
plural
|
str
|
The English text for any other number, such as "{count} rows". |
required |
number
|
int
|
The number that picks the form. It also fills |
required |
**values
|
Any
|
The other placeholders. |
{}
|
Returns:
| Type | Description |
|---|---|
str
|
The text in the language of the current request. |
Signing in¶
adminsite.auth.AuthProvider ¶
Decides who may use the admin.
Subclass it and write verify. The session handling here is enough
for most projects, and load_user is where you turn the key kept in
the session back into whatever your application calls a user.
verify
async
¶
verify(username: str, password: str) -> Any | None
Return the user for these details, or nothing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
username
|
str
|
What was typed as the username. |
required |
password
|
str
|
What was typed as the password. |
required |
Returns:
| Type | Description |
|---|---|
Any | None
|
Whatever your application calls a user, or None when the details do not match. |
Raises:
| Type | Description |
|---|---|
SignInRefusedError
|
Instead of returning None, to have the audit
log say why the attempt failed, such as
|
load_user
async
¶
load_user(key: str) -> Any | None
Turn the key kept in the session back into a user.
The key itself by default. Override it to read the user from your own table, so a user switched off since is signed out at once.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
What |
required |
Returns:
| Type | Description |
|---|---|
Any | None
|
The user, or None to sign them out. |
grants_for
async
¶
grants_for(user: Any) -> Grants | None
What this user may do, view by view, or None to leave it to the views.
None by default, so each view answers from its own settings and its
allows. A provider that keeps roles answers with the Grants they
add up to, and each view and page then follows them, unless its own
allows says otherwise. It runs on every request, once the user is
loaded.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
user
|
Any
|
The user, as |
required |
Returns:
| Type | Description |
|---|---|
Grants | None
|
The grants, or None. |
change_password
async
¶
change_password(
request: Request, current: str, new: str
) -> None
Change the signed in user's own password.
The account menu offers Change password only where a provider writes
this, as AdminUsers does.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
The request, with the signed in user on
|
required |
current
|
str
|
The password they have now, typed again to prove it. |
required |
new
|
str
|
The password they want, already typed twice alike. |
required |
Raises:
| Type | Description |
|---|---|
RefusedError
|
When the current password is wrong or the new one
will not do, with a message for the form. |
authenticate_token
async
¶
authenticate_token(token: str) -> Any | None
Return the user an API token belongs to, or nothing.
The JSON API calls this for Authorization: Bearer <token>. Nobody
gets in this way until you write it, for example by looking the
token up in a table of API keys.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token
|
str
|
The token, without "Bearer ". |
required |
Returns:
| Type | Description |
|---|---|
Any | None
|
The user, or None to refuse the request. |
identity ¶
identity(user: Any) -> str
The key to keep in the session for this user.
The audit log keeps it too, as user_key, which does not change
when the user's name does.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
user
|
Any
|
What |
required |
Returns:
| Type | Description |
|---|---|
str
|
The key, such as the user's id as text. |
current_user
async
¶
current_user(request: Request) -> Any | None
Who is signed in, if anyone.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
The request being answered. |
required |
Returns:
| Type | Description |
|---|---|
Any | None
|
The user, as |
sign_in
async
¶
sign_in(
request: Request, username: str, password: str
) -> Any | None
Check the details and remember the user.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
The request being answered. |
required |
username
|
str
|
What was typed as the username. |
required |
password
|
str
|
What was typed as the password. |
required |
Returns:
| Type | Description |
|---|---|
Any | None
|
The user, now kept in the session, or None when |
sign_in_note
async
¶
sign_in_note(request: Request) -> str
A line above the sign in form, such as how to make the first account.
Nothing by default. AdminUsers says how to make the first superuser
while nobody can sign in.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
The request being answered. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The line, escaped unless given as |
sign_in_failed
async
¶
sign_in_failed(request: Request, username: str) -> str
Called when a sign in fails. Returns what to tell the person.
Override it to record the attempt, to make the next one wait, or to say something other than the default. Whatever it returns is shown above the form, so keep it vague: a message that says the username exists tells an attacker so too.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
The request being answered. |
required |
username
|
str
|
What was typed as the username. |
required |
Returns:
| Type | Description |
|---|---|
str
|
What to tell the person. |
sign_in_values
async
¶
sign_in_values(request: Request) -> Mapping[str, str]
What the sign in form starts with, by input name.
Nothing by default. A public demo can fill in its shared username and password, so visitors only press Sign in. Never put real credentials here: anyone who opens the page can read them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
The request being answered. |
required |
Returns:
| Type | Description |
|---|---|
Mapping[str, str]
|
Starting values by input name: "username" and "password". |
may_read_sign_ins
async
¶
may_read_sign_ins(
request: Request, *, reads_everything: bool
) -> bool
Whether this person sees who signed in, on the Activity page.
Signing in belongs to no model, so no view's permissions decide it.
By default only someone who may read the history of every record of
every model sees it, which reads_everything says. Override it to
let in, say, an auditor who reads less.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
The request being answered. |
required |
reads_everything
|
bool
|
Whether this person may read the history of
every view, with no |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True to show them the sign ins. |
sign_out
async
¶
sign_out(request: Request) -> None
Forget the user.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
The request being answered. |
required |
adminsite.Grants
dataclass
¶
Grants(
permissions: Mapping[str, Collection[str]] = dict(),
everything: bool = False,
)
What one person may do, view by view, as their roles add up.
An auth provider answers with these from AuthProvider.grants_for, and
each view and page then follows them, unless its own allows says
otherwise:
from adminsite import Grants
Grants({"orders": {"view", "edit", "refund"}, "-/sales_report": {"view"}})
A view is named as in its URL, such as "orders", and a page of your own by "-/" and its name. Any permission on a view lets the person see it, since there is nothing to do there otherwise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
permissions
|
Mapping[str, Collection[str]]
|
The permissions granted on each view and page, by name:
|
dict()
|
everything
|
bool
|
True to allow everything, as for a superuser. |
False
|
permissions
class-attribute
instance-attribute
¶
permissions: Mapping[str, Collection[str]] = field(
default_factory=dict
)
The permissions granted on each view and page, by name.
everything
class-attribute
instance-attribute
¶
everything: bool = False
Whether everything is allowed, as for a superuser.
allows ¶
allows(name: str, permission: Permission | str) -> bool
Whether the view or page with this name grants the permission.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The view's name, such as "orders", or a page's, such as "-/sales_report". |
required |
permission
|
Permission | str
|
What the person wants to do, such as
|
required |
Returns:
| Type | Description |
|---|---|
bool
|
True when it is granted. Seeing the view, and opening a record's page, come with any permission on it. |
adminsite.auth.PasswordAuth ¶
PasswordAuth(users: Mapping[str, str])
Bases: AuthProvider
Signs in against a fixed set of usernames and password hashes.
from adminsite.auth import PasswordAuth, hash_password
PasswordAuth({"nima": hash_password("letmein")})
Passwords have to be hashed with hash_password, so a plain one
never ends up in your settings or your repository. This suits a
small internal tool. Anything larger should subclass AuthProvider
and check its own user table.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
users
|
Mapping[str, str]
|
Each username with its password hash. |
required |
Raises:
| Type | Description |
|---|---|
AdminSiteError
|
When a password is not hashed. |
verify
async
¶
verify(username: str, password: str) -> Any | None
Check the password against the stored hash.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
username
|
str
|
What was typed as the username. |
required |
password
|
str
|
What was typed as the password. |
required |
Returns:
| Type | Description |
|---|---|
Any | None
|
The username, as the user. |
Raises:
| Type | Description |
|---|---|
SignInRefusedError
|
When there is no such username, or the password is wrong, so the audit log says which. |
adminsite.users.AdminUsers ¶
AdminUsers(*, create_table: bool = False)
Bases: AuthProvider, Plugin
Admin users and roles, kept in the admin's own database.
from adminsite import Admin
from adminsite.users import AdminUsers
admin = Admin(
engine,
views=[OrderView, CustomerView],
auth=AdminUsers(),
secret_key=settings.admin_secret_key,
)
Each person signs in with a username and a password, and may do what
their roles grant, or everything as a superuser. The tables are
adminsite_users, adminsite_roles and adminsite_user_roles, on
user_metadata. adminsite never creates them in your database uninvited,
so add user_metadata to your migrations, or pass create_table=True.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
create_table
|
bool
|
True to create the tables on first use, for a quick start. |
False
|
create_user
async
¶
create_user(
username: str,
password: str,
*,
superuser: bool = False,
roles: Sequence[str] = (),
) -> User
Add someone who may sign in, such as the first superuser.
await users.create_user("lena", "a long password", roles=["Support"])
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
username
|
str
|
What they sign in with, kept in small letters. |
required |
password
|
str
|
Their password, hashed before it is kept. |
required |
superuser
|
bool
|
True to let them do everything, managing users and roles included. |
False
|
roles
|
Sequence[str]
|
The names of the roles they hold, each made already. |
()
|
Returns:
| Type | Description |
|---|---|
User
|
The user, saved. |
Raises:
| Type | Description |
|---|---|
AdminSiteError
|
When the username is empty or taken, the password is too short, or no role has one of the names. |
create_role
async
¶
create_role(
name: str,
permissions: Mapping[str, Collection[str]],
*,
description: str | None = None,
) -> Role
Add a role, such as one a project starts with.
await users.create_role("Support", {"orders": ["view", "edit", "refund"]})
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
What the role is called, such as "Support". |
required |
permissions
|
Mapping[str, Collection[str]]
|
What it grants, by view, as |
required |
description
|
str | None
|
What the role is for, in a sentence. |
None
|
Returns:
| Type | Description |
|---|---|
Role
|
The role, saved. |
Raises:
| Type | Description |
|---|---|
AdminSiteError
|
When the name is empty, or a role has it already. |
adminsite.users.User ¶
Bases: _Base
Someone who may sign in to the admin.
id
class-attribute
instance-attribute
¶
id: Mapped[int] = mapped_column(primary_key=True)
The user's key, which the session and the audit log keep.
username
class-attribute
instance-attribute
¶
username: Mapped[str] = mapped_column(
String(150), unique=True
)
What they sign in with, kept in small letters.
password_hash
class-attribute
instance-attribute
¶
password_hash: Mapped[str] = mapped_column(String(200))
Their password, as hash_password made it.
is_active
class-attribute
instance-attribute
¶
is_active: Mapped[bool] = mapped_column(default=True)
Whether they may sign in. Switched off, they are signed out at once.
is_superuser
class-attribute
instance-attribute
¶
is_superuser: Mapped[bool] = mapped_column(default=False)
Whether they may do everything, managing users and roles included.
last_sign_in_at
class-attribute
instance-attribute
¶
last_sign_in_at: Mapped[datetime | None] = mapped_column(
default=None
)
When they last signed in, or None if they never have.
created_at
class-attribute
instance-attribute
¶
created_at: Mapped[datetime] = mapped_column(default=_now)
When the account was made.
roles
class-attribute
instance-attribute
¶
roles: Mapped[list[Role]] = relationship(
secondary=user_roles, back_populates="users"
)
The roles they hold.
has_role ¶
has_role(name: str) -> bool
Whether they hold the role with this name.
if request.state.user.has_role("Support"):
...
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The role's name, such as "Support". |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True when one of their roles is called that. |
adminsite.users.Role ¶
Bases: _Base
Permissions given together, such as everything a support agent needs.
A person may hold several roles, and may do whatever any of them grants.
id
class-attribute
instance-attribute
¶
id: Mapped[int] = mapped_column(primary_key=True)
The role's key.
name
class-attribute
instance-attribute
¶
name: Mapped[str] = mapped_column(String(100), unique=True)
What the role is called, such as "Support".
description
class-attribute
instance-attribute
¶
description: Mapped[str | None] = mapped_column(
Text, default=None
)
What the role is for, in a sentence.
permissions
class-attribute
instance-attribute
¶
permissions: Mapped[dict[str, list[str]]] = mapped_column(
JSON, default=dict
)
What the role grants, by view: {"orders": ["view", "edit"]}.
A page of your own is named by "-/" and its name, such as "-/sales_report".
users
class-attribute
instance-attribute
¶
users: Mapped[list[User]] = relationship(
secondary=user_roles, back_populates="roles"
)
The people who hold the role.
adminsite.auth.hash_password ¶
hash_password(
password: str, *, iterations: int = ITERATIONS
) -> str
Hash a password for storing.
from adminsite.auth import hash_password
hash_password("letmein")
Keep the result, not the password.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
password
|
str
|
The password. |
required |
iterations
|
int
|
How many rounds of PBKDF2 to run. More is slower to check, and slower to guess. |
ITERATIONS
|
Returns:
| Type | Description |
|---|---|
str
|
The hash, as |
adminsite.auth.verify_password ¶
verify_password(password: str, stored: str) -> bool
Check a password against a stored hash, in constant time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
password
|
str
|
The password typed. |
required |
stored
|
str
|
A hash |
required |
Returns:
| Type | Description |
|---|---|
bool
|
Whether they match. A hash in any other form never does. |
adminsite.auth.SignInRefusedError ¶
SignInRefusedError(reason: str, user: object = None)
Bases: AdminSiteError
Raise this from AuthProvider.verify to refuse a sign in, saying why.
The reason goes to the audit log, never to the person signing in, who
sees only what sign_in_failed returns. Pass the account when there is
one, such as an inactive user, so the attempt is filed under them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
reason
|
str
|
Why the sign in was refused, for the audit log. |
required |
user
|
object
|
The account the attempt was for, where there is one. |
None
|
Audit log¶
adminsite.audit.AuditLog ¶
AuditLog(
source: Database | SessionSource | str = DEFAULT_URL,
*,
create_table: bool | None = None,
)
Bases: Store
Where the admin writes down who changed what.
By default the entries go to a SQLite file of their own, which needs no
setup. Pass your engine to keep them in your own database instead; the
table is then adminsite_audit_log on audit_metadata, which you add to
your migrations.
Kept in the admin's own database, a change and its entries are saved in one transaction: both or neither. Kept anywhere else, entries are written once the change has committed, so a change that was rolled back never shows up, but one whose entries fail to write is not logged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Database | SessionSource | str
|
Where the entries are kept: a SQLite URL for a file of their own, or your engine or session factory. |
DEFAULT_URL
|
create_table
|
bool | None
|
Whether to create the table on first use. Left as None, only in the SQLite file adminsite owns, and never uninvited in your own database, where your migrations add it. |
None
|
find
async
¶
find(query: AuditQuery, *, limit: int) -> list[AuditEntry]
The entries that match, newest first, at most limit of them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
AuditQuery
|
Which entries to find. |
required |
limit
|
int
|
The most entries to return. |
required |
Returns:
| Type | Description |
|---|---|
list[AuditEntry]
|
The entries, newest first. |
history
async
¶
history(
view: str, record_key: str, *, limit: int = 100
) -> list[AuditEntry]
What happened to one record, newest first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
view
|
str
|
The view's name, such as "orders". |
required |
record_key
|
str
|
The record's key, as the URLs write it. |
required |
limit
|
int
|
The most entries to return. |
100
|
Returns:
| Type | Description |
|---|---|
list[AuditEntry]
|
The entries, newest first. |
recent
async
¶
recent(
*,
view: str | None = None,
views: Sequence[str] | None = None,
limit: int = 100,
) -> list[AuditEntry]
The latest entries, across the admin, for some views or for one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
view
|
str | None
|
Only this view's entries. |
None
|
views
|
Sequence[str] | None
|
Only these views' entries. |
None
|
limit
|
int
|
The most entries to return. |
100
|
Returns:
| Type | Description |
|---|---|
list[AuditEntry]
|
The entries, newest first. |
record
async
¶
record(entries: Sequence[AuditEntry]) -> None
Write entries down, a batch at a time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entries
|
Sequence[AuditEntry]
|
The entries to write. |
required |
adminsite.audit.AuditStore ¶
Bases: Protocol
Where the audit log is written and read back.
AuditLog is the one that ships. Write your own to keep the log in a
table of your application, or to send it on elsewhere; the History tab
and the Activity page read it through find either way.
A store that keeps its entries in the admin's own database can say so, and have each change and its entries saved in one transaction, with two more methods:
lives_in(database) -> bool: whether the entries are kept in thatDatabase.async record_within(session, entries): write the entries through that session, inside its transaction, without committing.
Without them, entries are written once the change has committed.
record
async
¶
record(entries: Sequence[AuditEntry]) -> None
Keep these entries.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entries
|
Sequence[AuditEntry]
|
The entries to keep, once the change has committed. |
required |
find
async
¶
find(query: AuditQuery, *, limit: int) -> list[AuditEntry]
The entries that match, newest first, at most limit of them.
Newest first means by occurred_at, then by id, both descending,
which is also the order older_than pages through.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
AuditQuery
|
Which entries to find. |
required |
limit
|
int
|
The most entries to return. |
required |
Returns:
| Type | Description |
|---|---|
list[AuditEntry]
|
The entries, newest first. |
adminsite.audit.AuditQuery
dataclass
¶
AuditQuery(
views: Sequence[str] | None = None,
view: str | None = None,
record_key: str | None = None,
user: str | None = None,
events: Sequence[AuditEvent] = (),
since: datetime | None = None,
until: datetime | None = None,
older_than: tuple[datetime, int] | None = None,
)
Which entries to find. Every field narrows the result; empty means all.
views
class-attribute
instance-attribute
¶
views: Sequence[str] | None = None
Only entries of these views, such as the ones a person may read.
An empty string among them stands for entries of no view, such as signing in.
record_key
class-attribute
instance-attribute
¶
record_key: str | None = None
Only entries of this record, of view.
user
class-attribute
instance-attribute
¶
user: str | None = None
Only one person's entries, matched against user_key and user.
events
class-attribute
instance-attribute
¶
events: Sequence[AuditEvent] = ()
Only these kinds of entry.
since
class-attribute
instance-attribute
¶
since: datetime | None = None
Only entries from this time on, in UTC.
until
class-attribute
instance-attribute
¶
until: datetime | None = None
Only entries before this time, in UTC.
older_than
class-attribute
instance-attribute
¶
older_than: tuple[datetime, int] | None = None
The (occurred_at, id) of the last entry shown, for the page after it.
adminsite.audit.AuditEntry
dataclass
¶
AuditEntry(
view: str,
record_key: str,
event: AuditEvent,
changes: Mapping[str, Change] = dict(),
record_title: str | None = None,
action: str | None = None,
batch: str | None = None,
user: str | None = None,
message: str | None = None,
occurred_at: datetime = (
lambda: datetime.now(UTC).replace(tzinfo=None)
)(),
id: int | None = None,
user_key: str | None = None,
ip: str | None = None,
user_agent: str | None = None,
error: str | None = None,
inputs: Mapping[str, Any] = dict(),
)
One thing that happened in the admin, who did it, and from where.
record_key
instance-attribute
¶
record_key: str
The record's key, as the URLs write it, or empty for none.
changes
class-attribute
instance-attribute
¶
changes: Mapping[str, Change] = field(default_factory=dict)
Each field that changed, by name, as a pair of its values before and after.
record_title
class-attribute
instance-attribute
¶
record_title: str | None = None
The record's name when it happened.
action
class-attribute
instance-attribute
¶
action: str | None = None
The name of the action that ran, for an action.
batch
class-attribute
instance-attribute
¶
batch: str | None = None
An id the entries of one bulk action share.
user
class-attribute
instance-attribute
¶
user: str | None = None
The name the admin shows for the person.
occurred_at
class-attribute
instance-attribute
¶
occurred_at: datetime = field(
default_factory=lambda: datetime.now(UTC).replace(
tzinfo=None
)
)
When it happened, in UTC.
user_key
class-attribute
instance-attribute
¶
user_key: str | None = None
What AuthProvider.identity returns for the person.
It does not change when their name does.
user_agent
class-attribute
instance-attribute
¶
user_agent: str | None = None
The browser or program that sent the request.
error
class-attribute
instance-attribute
¶
error: str | None = None
Why it failed, or None when it worked.
inputs
class-attribute
instance-attribute
¶
inputs: Mapping[str, Any] = field(default_factory=dict)
The values an action was run with, or an export's search and filters.
Secrets are kept as ***.
as_row ¶
as_row() -> dict[str, Any]
The entry as a row for the audit table.
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The row's values, by column name. |
from_row
classmethod
¶
from_row(row: Mapping[Any, Any]) -> AuditEntry
Read an entry back from a row of the audit table.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
row
|
Mapping[Any, Any]
|
The row, as the table's columns name its values. |
required |
Returns:
| Type | Description |
|---|---|
AuditEntry
|
The entry. |
Errors¶
adminsite.RefusedError ¶
RefusedError(message: str, *, field: ColumnReference = '')
Bases: AdminSiteError
Raise this from a hook to refuse a save or a delete.
The message is shown to the user on the form or above the list, and the transaction is rolled back. Any other exception is a fault, and is reported as one.
Name a field and the message appears next to that input instead of above the form:
raise RefusedError("Keep this above the check delay.", field=Check.validation_delay)
The field is one of the view's own, not one of an inline's rows.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
str
|
What to tell the user, such as "This customer's account is closed.". |
required |
field
|
ColumnReference
|
The field to put the message beside, as a setting names it. Left empty, the message goes above the form. |
''
|
adminsite.PermissionDeniedError ¶
PermissionDeniedError(action: str, subject: str = '')
Bases: AdminSiteError
Raised when the current user may not do this.
The admin answers it with a "Not allowed" page and a 403, or a 403 in JSON from the API.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
action
|
str
|
The permission refused, such as "edit". |
required |
subject
|
str
|
What it was refused on, such as "Orders". |
''
|
adminsite.RecordNotFoundError ¶
RecordNotFoundError(model: type[object], key: object)
Bases: AdminSiteError
Raised when a key does not match any record.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
type[object]
|
The model searched. |
required |
key
|
object
|
The key that matched nothing. |
required |
adminsite.exceptions.IntegrityError ¶
Bases: AdminSiteError
Raised when the database refuses a change.
A value that must be unique is already taken, a required one is missing,
or other records still refer to the one being deleted.
SessionAdapter.transaction() and commit() raise this in place of
SQLAlchemy's own error, which stays attached as the cause. Its message
is worded for people and holds none of the values. A save or a delete
through the view turns it into a RefusedError, which the form or the
list shows.
adminsite.exceptions.FieldValidationError ¶
FieldValidationError(field_name: str, message: str)
Bases: AdminSiteError
Raised when a submitted value cannot be stored in a field.
A field's parse or to_python raises it, and the form shows the
message under that field.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
field_name
|
str
|
The path of the field, such as "total". |
required |
message
|
str
|
What to tell the person, such as "Enter a whole number.". |
required |
adminsite.AdminSiteError ¶
Bases: Exception
Base class for every error adminsite raises.