Filters¶
list_filters names what people can filter a list by. adminsite picks the filter that fits each
column, and the filters open in a drawer above the list. A filtered list keeps its filters in the
URL, so it can be bookmarked or shared, and the CSV export and bulk actions cover the same rows.
The examples on this page use the example models.
Built-in filters¶
from adminsite import Link
class OrderView(ModelView[Order]):
list_filters = [
Order.status,
Order.total,
Order.created_at,
Order.customer,
Link(Order.customer, Customer.region),
]
| Column | Filter | In the URL |
|---|---|---|
| enum | ChoiceFilter, several values, with counts |
?status=PAID&status=SHIPPED |
bool |
BooleanFilter |
?is_active=true |
| number | NumberRangeFilter, either end optional |
?total=10,50 or ?total=100, |
| date or datetime | DateRangeFilter, a shortcut or two dates |
?created_at=week or ?created_at=2026-09-01,2026-09-30 |
| relationship | RelationFilter, picking linked records by name |
?customer=3&customer=5 |
| text | TextFilter, matching part of the value |
?email=fischer |
A relation filter picks records through the linked model's view, so its scope and its permissions
apply. It matches the primary key, unless it is given another column, as in
RelationFilter("customer", key="code"). A related model's column, such as
Link(Order.customer, Customer.region), filters without a join, so rows never repeat.
Counts¶
Choice and yes or no filters show how many records each option matches. A view with a count_mode
of CountMode.ESTIMATED or CountMode.NONE shows no counts, since counting reads every record.
show_counts decides for one filter: True counts it whatever the view says, and False never
does.
from adminsite import CountMode
from adminsite.filters import BooleanFilter, ChoiceFilter
class OrderView(ModelView[Order]):
list_filters = [Order.status, BooleanFilter("high_risk", show_counts=False)]
class EventView(ModelView[Event]):
count_mode = CountMode.ESTIMATED
list_filters = [
ChoiceFilter(
"kind",
choices=[("signup", "Sign up"), ("order", "Order")],
show_counts=True,
),
Event.created_at,
]
Write your own¶
A filter is a class that returns a condition. Here orders are grouped by whether they are late:
from datetime import timedelta
from sqlalchemy import ColumnElement, func
from adminsite.filters import SQLAlchemyRepository, SQLFilter
from adminsite.filters import FilterContext, FilterOption, FilterValue
class DeliveryFilter(SQLFilter[Order]):
"""Orders past their delivery date, or due soon."""
async def options(self, context: FilterContext) -> list[FilterOption]:
return [FilterOption("late", "Overdue"), FilterOption("soon", "Due in 2 days")]
def condition(
self, value: FilterValue, repository: SQLAlchemyRepository[Order]
) -> ColumnElement[bool]:
if value.first == "late":
return Order.due_at < func.now()
return Order.due_at.between(func.now(), func.now() + timedelta(days=2))
class OrderView(ModelView[Order]):
list_filters = [Order.status, DeliveryFilter("delivery", label="Delivery")]
SQLFilter[Order] filters orders. value.first is the option picked, and value.values holds
every one picked when the class sets multiple = True. options can count too:
await context.count_by(self.path) counts the records by the value at that path, following the
rule for counts.
To change the statement itself, for example to add a join, override apply instead of
condition:
from typing import Any
from sqlalchemy import Select, func, select
class BigSpenders(SQLFilter[Customer]):
def apply(
self,
statement: Select[Any],
value: FilterValue,
repository: SQLAlchemyRepository[Customer],
) -> Select[Any]:
spent = (
select(Order.customer_id)
.group_by(Order.customer_id)
.having(func.sum(Order.total) > int(value.first))
)
return statement.where(Customer.id.in_(spent))
A filter that depends on who is asking comes from get_list_filters(request) instead of
list_filters. A filter on a field can_access_field keeps from the user is not offered to them,
as Permissions explains.