Zum Inhalt
Ü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.

Lesen Sie die ursprüngliche englische Version

Inline-Formulare

Inline-Formulare ermöglichen es Benutzern, zugehörige Datensätze direkt auf der Create- oder Edit-Seite eines übergeordneten Modells zu verwalten. Sie eignen sich für untergeordnete Modelle, die nur in Verbindung mit ihrem übergeordneten Modell sinnvoll sind – etwa Kommentare zu einem Artikel oder Aufgaben in einem Projekt – und ersparen Ihnen den Aufbau einer separaten Admin-Ansicht für das untergeordnete Modell.

Unter examples/06-inline-forms finden Sie eine lauffähige App, die alle drei Muster dieser Seite abdeckt: automatisch erkannter Fremdschlüssel, expliziter Fremdschlüssel und zusammengesetzter Fremdschlüssel.

Ein minimales Inline

from sqlalchemy import ForeignKey, Integer, String, Text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from starlette.requests import Request
from starlette_admin.contrib.sqla import InlineModelView, ModelView


class Base(DeclarativeBase):
    pass


class Article(Base):
    __tablename__ = "articles"

    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    title: Mapped[str] = mapped_column(String(200))
    body: Mapped[str] = mapped_column(Text, default="")

    comments: Mapped[list["Comment"]] = relationship(
        "Comment", back_populates="article", cascade="all, delete-orphan"
    )

    async def __admin_repr__(self, request: Request) -> str:
        return self.title


class Comment(Base):
    __tablename__ = "comments"

    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    article_id: Mapped[int] = mapped_column(Integer, ForeignKey("articles.id"))
    author: Mapped[str] = mapped_column(String(100), default="Anonymous")
    body: Mapped[str] = mapped_column(Text)

    article: Mapped["Article"] = relationship("Article", back_populates="comments")

    async def __admin_repr__(self, request: Request) -> str:
        return f"{self.author}: {self.body[:50]}"


class CommentInline(InlineModelView):
    model = Comment
    fields = ["author", "body"]
    extra = 1


class ArticleView(ModelView):
    fields = ["title", "body"]
    inlines = [CommentInline]

Die Einrichtung erfolgt in zwei Schritten: Definieren Sie eine InlineModelView-Unterklasse für das untergeordnete Modell und fügen Sie diese anschließend zur inlines-Liste des ModelView des übergeordneten Modells hinzu.

Auf den Create- und Edit-Seiten von ArticleView wird nun ein Comments-Formset unterhalb der eigenen Felder des Artikels gerendert. Das Formset beginnt mit einer leeren Zeile (extra = 1) und enthält die Steuerelemente zum Hinzufügen und Löschen, die das SQLAlchemy-Backend für Sie verdrahtet.

Beachten Sie, dass CommentInline niemals fk_attr setzt. Das SQLAlchemy-Backend untersucht Article.comments und leitet Comment.article_id als Fremdschlüssel ab, da es die einzige Beziehung ist, die auf Comment zeigt. Setzen Sie fk_attr selbst nur dann, wenn diese Ableitung nicht eindeutig möglich ist oder wenn die Beziehung nicht im ORM-Modell deklariert ist. Siehe Explizite und zusammengesetzte Fremdschlüssel.

InlineModelView-Referenz

Attribut Typ Standardwert Beschreibung
model ORM-Modellklasse None Das zugehörige Modell, das dieses Inline verwaltet. Erforderlich.
fk_attr str | tuple[str, ...] "" Name des Fremdschlüsselfelds im Inline-Modell, das auf das übergeordnete Modell zeigt. Ein Tupel deklariert einen zusammengesetzten Fremdschlüssel. Im SQLAlchemy-Backend optional; dieses erkennt ihn automatisch aus der Beziehung des übergeordneten Modells, wenn Sie ihn weglassen.
extra int 0 Anzahl leerer Zeilen, die zusätzlich zu bestehenden Zeilen auf Create- und Edit-Formularen angezeigt werden.
allow_delete bool True Zeigt eine Löschen-Checkbox bzw. einen Löschen-Button in jeder bestehenden Zeile an.
inline_template str "inline.html" Template, das zum Rendern des Formsets verwendet wird.
collapsible bool True Gibt an, ob Benutzer das Formset ein- und ausklappen können.
collapsed bool False Anfangszustand des Einklappens. Wirkt nur bei collapsible=True.

Der Konstruktor löst einen ValueError aus, wenn Sie fk_attr leer lassen und das Backend die Beziehung nicht eindeutig auflösen kann.

Ausklappbare Formsets

Standardmäßig (collapsible = True) rendert jedes InlineModelView sein Formset mit einem Header, den Benutzer auswählen können, um nicht benötigte untergeordnete Datensätze einzuklappen. Setzen Sie collapsed = True, um das Formset zunächst geschlossen statt geöffnet anzuzeigen:

class CommentInline(InlineModelView):
    model = Comment
    fields = ["author", "body"]
    extra = 1
    collapsed = True

Setzen Sie collapsible = False, um ein Formset vollständig vom Ausklappen auszuschließen; es wird dann immer ausgeklappt ohne Umschalter gerendert:

class CommentInline(InlineModelView):
    model = Comment
    fields = ["author", "body"]
    extra = 1
    collapsible = False

Explizite und zusammengesetzte Fremdschlüssel

Setzen Sie fk_attr selbst, wenn das übergeordnete Modell mehr als eine Beziehung zum selben untergeordneten Modell hat, wenn die Beziehung nicht im ORM-Modell deklariert ist oder wenn der Fremdschlüssel zusammengesetzt ist:

from sqlalchemy import ForeignKey, ForeignKeyConstraint, Integer, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from starlette.requests import Request
from starlette_admin import StringField
from starlette_admin.contrib.sqla import InlineModelView, ModelView


class Base(DeclarativeBase):
    pass


class Project(Base):
    __tablename__ = "projects"

    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    name: Mapped[str] = mapped_column(String(200))

    tasks: Mapped[list["Task"]] = relationship(
        "Task", back_populates="project", cascade="all, delete-orphan"
    )

    async def __admin_repr__(self, request: Request) -> str:
        return self.name


class Task(Base):
    __tablename__ = "tasks"

    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    project_id: Mapped[int] = mapped_column(Integer, ForeignKey("projects.id"))
    title: Mapped[str] = mapped_column(String(200))
    done: Mapped[bool] = mapped_column(default=False)

    project: Mapped["Project"] = relationship("Project", back_populates="tasks")

    async def __admin_repr__(self, request: Request) -> str:
        return self.title


class TaskInline(InlineModelView):
    model = Task
    fk_attr = "project_id"
    fields = ["title", "done"]
    extra = 2


class ProjectView(ModelView):
    fields = [StringField("name")]
    inlines = [TaskInline]


class Order(Base):
    __tablename__ = "orders"

    store_id: Mapped[int] = mapped_column(Integer, primary_key=True)
    seq: Mapped[int] = mapped_column(Integer, primary_key=True)
    customer: Mapped[str] = mapped_column(String(100))

    lines: Mapped[list["OrderLine"]] = relationship(
        "OrderLine", back_populates="order", cascade="all, delete-orphan"
    )

    async def __admin_repr__(self, request: Request) -> str:
        return f"Order #{self.store_id}-{self.seq} ({self.customer})"


class OrderLine(Base):
    __tablename__ = "order_lines"

    order_store_id: Mapped[int] = mapped_column(Integer, primary_key=True)
    order_seq: Mapped[int] = mapped_column(Integer, primary_key=True)
    line_no: Mapped[int] = mapped_column(Integer, primary_key=True)
    product: Mapped[str] = mapped_column(String(100))
    qty: Mapped[int] = mapped_column(Integer, default=1)

    order: Mapped["Order"] = relationship("Order", back_populates="lines")

    __table_args__ = (
        ForeignKeyConstraint(
            ["order_store_id", "order_seq"],
            ["orders.store_id", "orders.seq"],
        ),
    )

    async def __admin_repr__(self, request: Request) -> str:
        return f"Line {self.line_no}: {self.product} x {self.qty}"


class OrderLineInline(InlineModelView):
    model = OrderLine
    fields = ["line_no", "product", "qty"]
    extra = 1


class OrderView(ModelView):
    fields = ["store_id", "seq", "customer"]
    inlines = [OrderLineInline]

Beachten Sie, dass OrderLineInline kein fk_attr benötigt, obwohl der Primärschlüssel von OrderLine zusammengesetzt ist (order_store_id, order_seq, line_no). Das SQLAlchemy-Backend löst den zusammengesetzten Fremdschlüssel über die ForeignKeyConstraint zwischen Order und OrderLine auf und füllt beide Spalten in neuen Zeilen. Übergeben Sie ein tuple[str, ...] an fk_attr nur dann, wenn die Constraint-Introspektion keine Übereinstimmung findet.

Validierung

Jede eingereichte Zeile wird für sich allein validiert – über dieselben create- und edit-Pfade, die auch ein eigenständiges ModelView verwendet. Die Admin speichert zuerst das übergeordnete Modell und verarbeitet anschließend jede Inline-Zeile der Reihe nach. Ein Tippfehler im Feld author eines Kommentars hindert die anderen Kommentare nicht an der Verarbeitung. Schlägt die Validierung einer Zeile fehl, werden ihre Fehler dieser Zeile zugeordnet, und das Formular rendert die Zeile an Ort und Stelle mit den eingereichten Werten neu, damit Benutzer den Eintrag korrigieren und erneut absenden können.

Important

Im SQLAlchemy-Backend gilt für die gesamte Anfrage Alles-oder-Nichts. Das übergeordnete Modell und jede Inline-Zeile teilen sich dieselbe request-scoped Session, und diese Session committet nur, wenn die gesamte Anfrage erfolgreich ist. Schlägt die Validierung einer beliebigen Zeile fehl, gibt die Antwort einen Fehler zurück und die Session wird zurückgerollt, sodass das übergeordnete Modell und alle Inline-Zeilen gemeinsam zurückgesetzt werden – einschließlich der Zeilen, die die Validierung bestanden haben. Betrachten Sie die Fehlermeldungen pro Zeile in der UI als Liste dessen, was zu korrigieren ist, nicht als Protokoll dessen, was gespeichert wurde.


Wie es weitergeht

  • SQLAlchemy: Wie Beziehungs-Introspektion die automatische Erkennung von Fremdschlüsseln ermöglicht.
  • Custom Views: Erstellen Sie Seiten jenseits des Standard-Workflows aus Create, Edit und List.
  • Events: Reagieren Sie auf Inline-Änderungen, nachdem Datensätze gespeichert wurden.