starlette-admin icon indicating copy to clipboard operation
starlette-admin copied to clipboard

Enhancement: custom relation field

Open Ilya-Green opened this issue 11 months ago • 6 comments

Is your feature request related to a problem? Please describe. I want to make custom relation field so i can set custom render_function_key and custom display template.

Describe the solution you'd like A clear and concise description of what you want to happen. Something like this in list: image and something like this in detail: image

Describe alternatives you've considered If i trying to use StringField as base class:

@dataclass
class NotesField(StringField):
    rows: int = 6
    render_function_key: str = "notes"
    class_: str = "field-textarea form-control"
    form_template: str = "forms/textarea.html"
    display_template: str = "displays/note.html"
    exclude_from_create: Optional[bool] = True
    exclude_from_edit: Optional[bool] = True
    exclude_from_list: Optional[bool] = False

class ClientView(MyModelView):
    fields = [
        NotesField("notes"),
        Client.notes,
    ]

class DesktView(MyModelView):
    fields = [
        Desk.clients,
    ]

It leads to this api response: GET: http://127.0.0.1:8000/admin/api/desk?skip=0&limit=20&order_by=id%20asc: Reponse:

   {
    "items": [
        {
            "id": 1,
            "client": [
                {
                    "id": 15,
                    "notes": "[Note(content='test', client_id=15, id=4,), Note(content='teaa', client_id=15, id=6)]",
                    "_repr": "15",
                    "_detail_url": "http://127.0.0.1:8000/admin/client/detail/15",
                    "_edit_url": "http://127.0.0.1:8000/admin/client/edit/15"
                }
            ],

As you can see it resolving "notes" field (relation of a relation) and showing it like text (serializing func thinks it string field). This is problem because it overloads backend by resolving a lot of related records. And it might be not a problem if there is no a lot of relations, but if there a long chain of relations it starting resolving literally all databse which leads to crash of worker and even cycling of resolving records which leads to endless fetching to the database (and blocking it) until worker crashing and restarts by itself.

I found out that i can change "serialize" function to exclude this field:

    @dataclass
    class CustomRelationField(StringField):
        rows: int = 6

    async def serialize(
        self,
        obj: Any,
        request: Request,
        action: RequestAction,
        include_relationships: bool = True,
        include_relationships2: bool = True,
        include_relationships3: bool = True,
        include_select2: bool = False,
    ) -> Dict[str, Any]:
    ...
                elif not isinstance(field, RelationField):
+                    if isinstance(field, CustomRelationField):
+                           continue
                     ...

but this method is not allowing me to use relations information in detail view.

This method leads to overloading database in detail view:

    async def serialize(
        self,
        obj: Any,
        request: Request,
        action: RequestAction,
        include_relationships: bool = True,
        include_relationships2: bool = True,
        include_relationships3: bool = True,
        include_select2: bool = False,
    ) -> Dict[str, Any]:
    ...
                elif not isinstance(field, RelationField):
+                    if isinstance(field, CustomRelationField)  and action == RequestAction.LIST:
+                           continue
                     ...

How i can change serialize def so it stop to resolving in some moment? Or it will be even better if there is another more correct way to customize relation field display?

Ilya-Green avatar Mar 06 '24 01:03 Ilya-Green

Somehow this saves the optimization and at the same moment returns all required data:

@dataclass
class CustomRelationField(StringField):
    async def serialize_value(
        self, request: Request, value: Any, action: RequestAction
    ) -> Any:
        if action == RequestAction.DETAIL:
            return '123'

I don't understand how it all works. In some cases it actually didt resolve some needed fields, in some it resolves exactly as many dates as needed.

Ilya-Green avatar Mar 07 '24 04:03 Ilya-Green

I found thys syntax in admin demo source code:

class Post(SQLModel, table=True):
    id: Optional[int] = Field(primary_key=True)
    async def __admin_select2_repr__(self, request: Request) -> str:
        template_str = (
            "<span><strong>Title: </strong>{{obj.title}}, <strong>Publish by:"
            " </strong>{{obj.publisher.full_name}}</span>"
        )
        return Template(template_str, autoescape=True).render(obj=self)

I belive this can help to change list view. Finally, it remains to understand how to change relation field template in the detail view.

Ilya-Green avatar Mar 07 '24 15:03 Ilya-Green

Is there any way to set representation html template for detail view also?

Ilya-Green avatar Mar 07 '24 15:03 Ilya-Green

I made _detail_repr If anyone need this I can provide modified code

Ilya-Green avatar Mar 12 '24 23:03 Ilya-Green

You can simply override the detail template for this specific field. You can find more information on https://jowilf.github.io/starlette-admin/advanced/custom-field/?h=custom#detail-page

Also, I noticed that you are currently using an outdated version of starlette-admin. I recommend upgrading to the latest version.

jowilf avatar Mar 13 '24 05:03 jowilf

class ClientView(MyModelView):
    fields = [
        NotesField("notes"),
        Client.notes,
    ]

The field names must be unique per view

jowilf avatar Mar 13 '24 05:03 jowilf