Überwachte maschinelle Übersetzung
Dieser Inhalt wurde maschinell übersetzt und basiert auf von Menschen kuratierten Glossaren und Styleguides. Da der Text nicht zeilenweise manuell überprüft wird, können gelegentlich Fehler oder unklare Formulierungen auftreten.
Bei etwaigen Abweichungen ist die ursprüngliche englische Version die maßgebliche Quelle.
Benutzerdefinierte Filter
Subklassen Sie BaseFilter, wenn Sie einen Operator benötigen, den der eingebaute Satz nicht abdeckt: eine domänenspezifische Prüfung wie „ist teilbar durch", eine berechnete Bedingung wie „in diesem Monat erstellt" oder Unterstützung für einen Feldtyp, den die Standard-Registry überspringt. Diese Seite erklärt, wie ein Filter intern funktioniert, und zeigt die beiden Möglichkeiten zur Registrierung: entweder durch Subclassing der FilterRegistry Ihres Backends, um alle passenden Feldtypen abzudecken, oder durch Übergeben des Filters an die filters=-Liste eines einzelnen Felds. Für die Details des täglichen Gebrauchs, einschließlich der Standardfilter pro Feldtyp, manueller Overrides und des URL-Formats, siehe den Filters-Leitfaden.
Die BaseFilter-Schnittstelle
Jeder Filter, ob eingebaut oder benutzerdefiniert, implementiert zwei Methoden:
from typing import Any
from starlette_admin.filters.base import BaseFilter, FilterApplyContext, FilterDataType
class MyFilter(BaseFilter):
name = "my_filter"
label = "My filter"
data_type = FilterDataType.STRING
def parse_value(self, raw: str) -> Any:
"""Convert the raw string from the URL into the value apply() expects.
Raise FilterValidationError if the value isn't acceptable.
"""
return raw
def apply(self, ctx: FilterApplyContext) -> Any:
"""Return a query fragment for this filter's condition."""
raise NotImplementedError()
parse_value(raw)wandelt den rohen URL-String in den Typ um, denapply()erwartet, etwa einDecimal, eindateoder eine Liste. Die Standardimplementierung gibt den String unverändert weiter, was fürSTRING- undENUM-Filter geeignet ist, jedoch nicht für numerische oder zeitbezogene Daten. Sie dient außerdem als Validierungs-Hook: Werfen SieFilterValidationErrorfür Werte, die zwar geparst werden können, aber dennoch unzulässig sind, etwa außerhalb des zulässigen Bereichs liegende oder fehlerhaft formatierte Eingaben.apply(ctx)ist die einzige abstrakte Methode. Sie erhält einenFilterApplyContext, derquery,field_name,value,value2,requestundviewenthält, und gibt ein Query-Fragment für Ihr Backend zurück.
Wie rohe URL-Werte geparst werden
Jeder URL-Parameter ist ein String, daher kommen sowohl price__gt=50 als auch created_at__eq=2026-01-01 als Rohtext an. Bevor apply() ausgeführt wird, wandelt parse_value() diesen String in ein Python-Objekt um, das zum data_type des Filters passt:
def _parse_number(raw: Any) -> int | float:
text = str(raw).strip()
try:
return int(text)
except ValueError:
pass
try:
return float(text)
except ValueError:
raise FilterValidationError(f"{raw!r} is not a valid number") from None
class GreaterThanFilter(BaseFilter):
name = "gt"
data_type = FilterDataType.NUMBER
def parse_value(self, raw: Any) -> int | float:
return _parse_number(raw)
So erreichen ?filter=price__gt=50 und ?filter=price__gt=50.5 GreaterThanFilter.apply() als Python-Zahlen (50 als int, 50.5 als float) und nicht als die Strings "50" und "50.5". apply() übergibt diesen geparsten Wert direkt an das Query-Objekt, und der Datenbanktreiber übernimmt die abschließende Umwandlung in den tatsächlichen Typ der Spalte, etwa Decimal oder Numeric.
data_type |
Beispielhafter roher URL-Wert | Geparster Python-Wert | Geparst von |
|---|---|---|---|
number |
50, -3, 50.5 |
int(50), int(-3), float(50.5) |
filters.numeric._parse_number (versucht int(), fällt auf float() zurück) |
date |
2026-01-01 |
date(2026, 1, 1) |
filters.date._parse_temporal mit date.fromisoformat() |
datetime |
2026-01-01T14:30:00 |
datetime(2026, 1, 1, 14, 30) |
filters.date._parse_temporal mit datetime.fromisoformat() |
time |
14:30:00 |
time(14, 30) |
filters.date._parse_temporal mit time.fromisoformat() |
array |
ACTIVE,OUT_OF_STOCK |
["ACTIVE", "OUT_OF_STOCK"] |
filters.array._parse_array (teilt an nicht in Anführungszeichen gesetzten Kommas) |
string, enum |
admin |
"admin" |
BaseFilter.parse_value Standard (unverändert durchgereicht) |
none |
(kein Wert in der URL vorhanden) | (wird nie aufgerufen) | N/A |
Wenn sich ein Wert nicht parsen lässt, etwa bei price__gt=abc oder created_at__eq=not-a-date, wirft parse_value() einen FilterValidationError. Der Request-Handler fängt ihn ab und gibt HTTP 400 zurück, bevor irgendeine Datenbankabfrage ausgeführt wird:
GET /admin/product/list?filter=price__gt=abc
Returns: 400 Bad Request: Invalid 'filter' parameter: 'abc' is not a valid number
Wertlose Filter – diejenigen mit data_type=none wie is_null, is_true oder in_past – überspringen diesen Schritt. Für sie läuft parse_value nie, weshalb field__is_null kein =value in der URL benötigt: Es gibt keinen Eingabe-String, der umgewandelt werden müsste.
Einen benutzerdefinierten Filter verfügbar machen
Sie können einen benutzerdefinierten Filter auf zwei Arten bei einer View registrieren. Wählen Sie die Variante, die dem gewünschten Geltungsbereich entspricht.
Pro Feldinstanz (schmaler Geltungsbereich)
Übergeben Sie den Filter an die filters=-Liste des Zielfelds, entweder zusätzlich zu den Standardfiltern oder an deren Stelle. Siehe Overriding filters for a specific field für dasselbe Muster mit eingebauten Filtern. Verwenden Sie dies, wenn der Filter nur für ein einzelnes Feld sinnvoll ist.
Registrierungsweit (jeder passende Feldtyp)
Jedes Backend liefert eine FilterRegistry-Subklasse mit: SqlaFilterRegistry für SQLAlchemy, BeanieFilterRegistry für Beanie, MongoEngineFilterRegistry für MongoEngine und TortoiseFilterRegistry für Tortoise ORM. Jede davon definiert die Standardfilter für einen unterstützten Feldtyp in einer Methode, die mit @filters(FieldType, ...) dekoriert ist:
# starlette_admin/contrib/sqla/filters.py
class SqlaFilterRegistry(FilterRegistry):
@filters(StringField)
def string_filters(self, field: BaseField) -> list[type[BaseFilter]]:
return [
ContainsFilter,
NotContainsFilter,
EqualFilter,
IsNullFilter,
IsNotNullFilter,
]
@filters(NumberField, FloatField)
def numeric_filters(self, field: BaseField) -> list[type[BaseFilter]]:
return [
NumericEqualFilter,
GreaterThanFilter,
LessThanFilter,
IsNullFilter,
IsNotNullFilter,
]
# ... one method per field type
Um die für einen Feldtyp verfügbaren Filter über eine gesamte View hinweg zu ändern, subklassen Sie die Registry des Backends, überschreiben bzw. ergänzen eine @filters-Methode und geben aus get_filter_registry() eine Instanz Ihrer Subklasse zurück:
class ProductFilterRegistry(SqlaFilterRegistry):
@filters(IntegerField)
def integer_filters(self, field: BaseField) -> list[type[BaseFilter]]:
return [*self.numeric_filters(field), DivisibleByFilter]
class ProductView(ModelView):
def get_filter_registry(self) -> FilterRegistry:
return ProductFilterRegistry()
Deklarieren Sie diese Methoden auf eine von zwei Arten, je nachdem, ob Sie die vorhandenen Filter ersetzen oder erweitern möchten:
- Override: Deklarieren Sie
@filters(StringField)in Ihrer Subklasse erneut und geben Sie genau die Klassen zurück, die Sie möchten. Dies ersetzt die Liste der Elternklasse, daher müssen Sie alle eingebauten Filter einschließen, die Sie behalten möchten. - Extend: Deklarieren Sie
@filters(IntegerField), wenn die Eltern-Registry nur den allgemeinerenNumberFieldregistriert. DaIntegerFieldeine Subklasse vonNumberFieldist, löst die Method Resolution Order (MRO)IntegerFieldauf Ihre neue Methode auf, währendDecimalField, eine weitereNumberField-Subklasse ohne eigene Registrierung, weiterhin unverändert dienumeric_filtersder Elternklasse erbt.
Dies ist eine gewöhnliche Python-Subklasse, daher verändert sie keinen globalen Zustand. Jeder Aufruf von ProductFilterRegistry() erzeugt eine unabhängige Registry, und Ihre Änderungen bleiben auf die Views beschränkt, die sie zurückgeben. Alle anderen Views behalten die Backend-Standards bei.
Vollständiges SQLAlchemy-Beispiel
Der folgende DivisibleByFilter nimmt einen Wert entgegen, den Divisor, gegen den die Spalte geprüft werden soll. Eine SqlaFilterRegistry-Subklasse wendet ihn auf jedes IntegerField in ProductView an, statt ihn an einzelne Felder zu hängen:
import uuid
from contextlib import asynccontextmanager
from datetime import datetime
from decimal import Decimal
from typing import Any
from fastapi import FastAPI
from sqlalchemy import Integer, Numeric, create_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from starlette.requests import Request
from starlette_admin import IntegerField
from starlette_admin.contrib.sqla import Admin, ModelView
from starlette_admin.contrib.sqla.filters import SqlaFilterRegistry
from starlette_admin.fields import BaseField
from starlette_admin.filters import (
BaseFilter,
FilterApplyContext,
FilterDataType,
FilterRegistry,
FilterValidationError,
filters,
)
engine = create_engine(
"sqlite:///product.db", connect_args={"check_same_thread": False}, echo=True
)
class Base(DeclarativeBase):
pass
class Product(Base):
__tablename__ = "products"
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
name: Mapped[str]
price: Mapped[Decimal] = mapped_column(Numeric(10, 2))
lot_size: Mapped[int] = mapped_column(Integer, default=1)
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
async def __admin_repr__(self, request: Request) -> str:
return self.name
class DivisibleByFilter(BaseFilter):
"""
Filters database rows where the column value is an exact multiple of a given divisor.
"""
name = "divisible_by"
label = "Is divisible by"
data_type = FilterDataType.NUMBER
def parse_value(self, raw: str) -> int:
"""Validates and converts the raw admin UI input into an integer divisor."""
try:
divisor = int(raw)
except ValueError:
raise FilterValidationError(f"{raw!r} is not a valid integer") from None
if divisor == 0:
raise FilterValidationError("divisor must not be 0")
return divisor
def apply(self, ctx: FilterApplyContext) -> Any:
"""Applies the modulus condition to the underlying SQLAlchemy query context."""
column = getattr(ctx.view.model, ctx.field_name)
return column % ctx.value == 0
class ProductFilterRegistry(SqlaFilterRegistry):
"""
Custom filter registry that injects `DivisibleByFilter` into integer fields.
Overriding `integer_filters` gives every IntegerField the divisibility filter
on top of the standard numeric defaults. Other numeric fields, such as
DecimalField, are unaffected.
"""
@filters(IntegerField)
def integer_filters(self, field: BaseField) -> list[type[BaseFilter]]:
return [*self.numeric_filters(field), DivisibleByFilter]
class ProductView(ModelView):
fields = [
"id",
"name",
"price",
# Note: Passing just the string "lot_size" would also work, as SQLAlchemy's
# default converter automatically maps integer columns to IntegerField.
IntegerField("lot_size"),
]
def get_filter_registry(self) -> FilterRegistry:
"""Binds the custom filter registry to this specific view."""
return ProductFilterRegistry()
@asynccontextmanager
async def lifespan(app: FastAPI):
Base.metadata.create_all(engine)
yield
app = FastAPI(lifespan=lifespan)
admin = Admin(engine, title="Blog Admin", secret_key="change-me")
admin.add_view(ProductView(Product, icon="fa fa-product"))
admin.mount_to(app)
Unter examples/02-filters finden Sie eine lauffähige App mit einer benutzerdefinierten BaseFilter-Subklasse, die auf dieselbe Weise registriert ist.
Die Option lot_size__divisible_by erscheint nun als Filter für IntegerField("lot_size"), ohne explizite filters=-Deklaration am Feld. Beispielsweise matcht lot_size__divisible_by=6 Produkte, deren Losgröße ein Vielfaches von 6 ist:
Tip
Verwenden Sie eine FilterRegistry-Subklasse, wenn ein Filter generisch genug ist, um auf jedes Feld eines bestimmten Typs in einer View angewendet zu werden. Verwenden Sie die pro-Feld-filters=-Liste, wenn die Logik nur zu einem einzigen Feld gehört. Der Filters-Leitfaden enthält Beispiele für das Pro-Feld-Muster.
Dynamische Auswahlmöglichkeiten mit get_choices
Standardmäßig folgt das Werteingabefeld eines Filters seinem data_type: ein einfaches Textfeld für STRING, ein Zahlenfeld für NUMBER usw. Überschreiben Sie get_choices(request), wenn der Wert stattdessen aus einem Dropdown stammen soll, das mit einer pro Request generierten Liste von (value, label)-Paaren gefüllt wird. Ein „is one of"-Filter über ein Relationsfeld ist der typische Fall: Der zurückgesendete Wert ist ein Fremdschlüssel, aber die Auswahl sollte einen lesbaren Namen anzeigen.
get_choices erhält den aktuellen Request und gibt eine Sequenz von (value, label)-Paaren zurück oder None (der Standard), um das einfache Eingabefeld beizubehalten. Ein nicht leeres Ergebnis hat Vorrang vor dem einfachen Eingabefeld sowie vor allen Auswahlmöglichkeiten, die das Feld selbst bereitstellt, wie es bei EnumField der Fall ist.
Das folgende Beispiel aus examples/advanced/07-hr fügt dem department-Feld in der Employee-Liste ein „is one of"- und „is not one of"-Paar hinzu. department ist ein RelationField, daher stellt ihm die Standard-Registry nur Null-Prüfungen bereit: Es gibt keine generische Möglichkeit, eine verknüpfte Zeile mit einem Rohtext zu vergleichen. get_choices listet jede Department nach Name für das Dropdown auf, und parse_value wandelt die zurückgesendeten Werte in Ganzzahlen um, sodass apply direkt auf dem Fremdschlüssel Department.id matchen kann, statt über die Beziehung zu joinen und Namen zu vergleichen:
# examples/advanced/07-hr/filters.py
from typing import Any
from models import Department, Employee
from sqlalchemy import select
from sqlalchemy.orm import Session
from starlette.requests import Request
from starlette_admin.filters.base import FilterApplyContext, FilterValidationError
from starlette_admin.filters.enum import InFilter, NotInFilter
class _DepartmentChoicesMixin:
"""Shared `get_choices`/`parse_value` for the two filters below: the
filter builder's dropdown lists every department by name, and posts back
the department's `id` rather than its name, so `apply` can match on the
primary key instead of an `ilike` comparison.
"""
def get_choices(self, request: Request) -> list[tuple[int, str]]:
session: Session = request.state.session
return list(
session.execute(
select(Department.id, Department.name).order_by(Department.name)
).all()
)
def parse_value(self, raw: Any) -> list[int]:
values = super().parse_value(raw) # type: ignore[misc]
try:
return [int(v) for v in values]
except ValueError as err:
raise FilterValidationError("Department id must be an integer") from err
class DepartmentInFilter(_DepartmentChoicesMixin, InFilter):
"""Employees in one of the selected departments."""
name = "department_in"
label = "is one of"
def apply(self, ctx: FilterApplyContext) -> Any:
return Employee.department_id.in_(ctx.value)
class DepartmentNotInFilter(_DepartmentChoicesMixin, NotInFilter):
"""Employees not in any of the selected departments"""
name = "department_not_in"
label = "is not one of"
def apply(self, ctx: FilterApplyContext) -> Any:
return ~Employee.department_id.in_(ctx.value)
Einige Anmerkungen zu diesem Muster:
- Der Mixin steht in der MRO vor der Basisfilter-Klasse.
_DepartmentChoicesMixinkommt zuerst inclass DepartmentInFilter(_DepartmentChoicesMixin, InFilter), daher überschreiben seinget_choicesund seinparse_valuediejenigen, die jeder Filter sonst erben würde.super().parse_value(raw)erreicht dennochInFilter.parse_value, das den Rohwert in eine Liste aufteilt, bevor der Mixin ihn in Ganzzahlen umwandelt. get_choiceswird bei jedem Request ausgeführt, nicht einmalig beim Import, daher spiegelt das Dropdown stets die aktuellen Zeilen wider. Eine neu hinzugefügteDepartmenterscheint sofort im Filter Builder, ohne Serverneustart und ohne Cache, der ungültig gemacht werden müsste.- Die
(value, label)-Paare und der Ausgabetyp vonparse_valuemüssen übereinstimmen. Das Dropdown sendet den vom Benutzer gewähltenvaluezurück, daher wandeltparse_valueihn in das um, wasapplyerwartet.Department.idist hier bereits einint, daher stellt der Mixin dies in seinemparse_valueerneut sicher und wirft einen Validierungsfehler bei allem anderen. InFilterundNotInFilterhaben bereits standardmäßigdata_type = FilterDataType.ENUM, eine Mehrfachauswahl, daher benötigt keine der beiden Subklassen eindata_type-Override. Das Überschreiben vonget_choicesgenügt, um diese Mehrfachauswahl mit Departments zu füllen, statt sie leer zu lassen.
Was kommt als Nächstes?
- Filters: Erfahren Sie mehr über die Standardfilter pro Feldtyp, das URL-Format und das
filters=-Override. - SQLAlchemy: Erkunden Sie das in diesem Beispiel verwendete SQLAlchemy-Backend.
- Extension points: Sehen Sie sich die vollständige Liste der Methoden an, die Sie in
ModelViewüberschreiben können.