starlette-admin
starlette-admin copied to clipboard
Enhancement: SQLAlchemy Inline Models support
Is your feature request related to a problem? Please describe. Can't create child records within the parent create or edit page like the Odmantic demo on the Starlette Admin Demo page.
Describe alternatives you've considered Flask Admin, FastAPI Amis Admin, and Django Admin have this feature.
Additional context I think we can cheat it and take a look at the Flask Admin source code.
I previously described a workaround here. You can now use hooks instead of overriding the create
and edit
methods as shown in the example.
A workaround: #240
It's great that you have mentioned that. I think we can follow that workaround and come up with a solution.
I created this one to keep track of the development, I need this feature since it's the only feature that is missing from Flask Admin.
I previously described a workaround here. You can now use hooks instead of overriding the
create
andedit
methods as shown in the example.
I took a look at that workaround and tried something I needed.
Here is a little application:
import datetime
from sqlalchemy import ForeignKey, create_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from starlette.applications import Starlette
from starlette_admin import CollectionField, DateTimeField, IntegerField, ListField, StringField
from starlette_admin.contrib.sqla import Admin, ModelView
engine = create_engine("sqlite:///db.sqlite3", connect_args={"check_same_thread": False})
from typing import Any, Dict
from starlette.requests import Request
class Base(DeclarativeBase):
pass
class Mixin:
id: Mapped[int] = mapped_column(
primary_key=True,
autoincrement=True,
index=True,
unique=True,
)
date_created: Mapped[datetime.datetime] = mapped_column(
default=datetime.datetime.utcnow,
index=True,
)
date_updated: Mapped[datetime.datetime] = mapped_column(
default=datetime.datetime.utcnow,
onupdate=datetime.datetime.utcnow,
index=True,
)
class Shipment(Base, Mixin):
__tablename__ = "shipment"
date_to_ship: Mapped[datetime.datetime]
vehicles: Mapped[list["Vehicle"]] = relationship("Vehicle", back_populates="shipment")
class Vehicle(Base, Mixin):
__tablename__ = "vehicle"
make: Mapped[str]
model: Mapped[str]
year: Mapped[int]
shipment_id: Mapped[int] = mapped_column(ForeignKey("shipment.id"))
shipment: Mapped["Shipment"] = relationship("Shipment", back_populates="vehicles")
class ShipmentView(ModelView):
fields = [
IntegerField(
name="id",
label="ID",
help_text="ID of the record.",
read_only=True,
),
DateTimeField(
name="date_created",
label="Date Created",
help_text="Date the record was created.",
exclude_from_create=True,
exclude_from_edit=True,
read_only=True,
),
DateTimeField(
name="date_updated",
label="Date Updated",
help_text="Date the record was last updated.",
exclude_from_create=True,
exclude_from_edit=True,
read_only=True,
),
DateTimeField(
name="date_to_ship",
label="Date to Ship",
help_text="Date the shipment should be shipped.",
),
ListField(
field=CollectionField(
"vehicles",
fields=[
IntegerField(
name="id",
label="ID",
help_text="ID of the record.",
read_only=True,
),
StringField(
name="make",
label="Make",
help_text="Make of the vehicle.",
),
StringField(
name="model",
label="Model",
help_text="Model of the vehicle.",
),
IntegerField(
name="year",
label="Year",
help_text="Year of the vehicle.",
),
],
),
),
]
async def create(self, request: Request, data: Dict[str, Any]) -> Any:
vehicles = data.pop("vehicles")
vehicles = [Vehicle(**vehicle) for vehicle in vehicles]
data["vehicles"] = vehicles
return await super().create(request, data)
async def edit(self, request: Request, pk: Any, data: Dict[str, Any]) -> Any:
vehicles = data.pop("vehicles")
print(vehicles)
# Update the existing vehicles, delete the ones that are no longer there and add the new ones.
data["vehicles"] = vehicles
return await super().edit(request, pk, data)
Base.metadata.create_all(engine)
app = Starlette()
admin = Admin(engine, title="SQLAlchemy Inline Models")
admin.add_view(ShipmentView(Shipment))
admin.mount_to(app)
BTW, I have tried using the before_create
hook like this:
async def before_create(self, request: Request, data: Dict[str, Any], obj: Any) -> None:
vehicles = data.pop("vehicles")
vehicles = [Vehicle(**vehicle) for vehicle in vehicles]
data["vehicles"] = vehicles
return await super().before_create(request, data, obj)
...and it gave an error at this line:
obj = await self._populate_obj(request, self.model(), data)
So I still have to override the create
and edit
methods.