jowilf / starlette-admin

Fast, beautiful and extensible administrative interface framework for Starlette & FastApi applications
https://jowilf.github.io/starlette-admin/
MIT License
528 stars 55 forks source link

Enhancement: SQLAlchemy Inline Models support #440

Open hasansezertasan opened 6 months ago

hasansezertasan commented 6 months ago

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.

image

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.

jowilf commented 6 months ago

I previously described a workaround here. You can now use hooks instead of overriding the create and edit methods as shown in the example.

hasansezertasan commented 6 months ago

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.

hasansezertasan commented 6 months ago

I previously described a workaround here. You can now use hooks instead of overriding the create and edit 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.