Filters¶
Filters sit in a panel above the list. The ones in use show as chips, and the whole state lives in the URL, so a filtered list can be bookmarked or sent to someone.
Built in¶
Name a path in list_filter and adminsite picks the filter that fits the column:
class OrderView(ModelView, model=Order):
list_filter = ("status", "total", "created_at", "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, by the linked record's key |
?customer=3 |
| text | TextFilter, matching part of the value |
?email=fischer |
A dotted path such as customer.region filters through the link without adding a join, so rows
are never duplicated. Its name in the URL is customer__region.
Counts¶
Choice and boolean filters show how many records each option matches. The counts follow the search but not the other filters, so they stay steady while you pick. On a big table, turn them off:
from adminsite.backends.sqlalchemy import ChoiceFilter
class OrderView(ModelView, model=Order):
list_filter = (
ChoiceFilter(
"status",
choices=(("PAID", "Paid"), ("SHIPPED", "Shipped")),
show_counts=False,
),
)
Writing 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 func
from adminsite.backends.sqlalchemy import SQLFilter
from adminsite.filters import FilterOption
class DeliveryFilter(SQLFilter):
"""Orders past their delivery date, or due soon."""
async def options(self, context):
return [FilterOption("late", "Overdue"), FilterOption("soon", "Due in 2 days")]
def condition(self, value, repository):
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, model=Order):
list_filter = ("status", DeliveryFilter("delivery", label="Delivery"))
value.first is the chosen option, and value.values holds all of them when the filter allows
several (set multiple = True on the class).
When the filter needs to change the statement itself, for example to add a join, override apply
instead of condition:
from sqlalchemy import func, select
class BigSpenders(SQLFilter):
def apply(self, statement, value, repository):
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))
One place for every read¶
The list, its count, a CSV export and a bulk action all go through the same filters. "Select all 12,408 matching" always means exactly the rows the list is showing.