Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug fix: unhandled exception during AjaxSelect load #727

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

diskream
Copy link

@diskream diskream commented Mar 11, 2024

There is an unhandled exception in AjaxSelect2Widget after any exception in model_view.insert_model.

The error occurs when you pick something in the Ajax select box and there is an exception in the overridden insert_model method. Here is a part of the traceback:

  File "/Users/diskream/Library/Caches/pypoetry/virtualenvs/aMY0E6K2-py3.12/lib/python3.12/site-packages/sqladmin/widgets.py", line 57, in __call__
    data = field.loader.format(field.data)
           │     │      │      │     └ <property object at 0x110181300>
           │     │      │      └ <sqladmin.fields.AjaxSelectField object at 0x110903b90>
           │     │      └ <function QueryAjaxModelLoader.format at 0x1101089a0>
           │     └ <sqladmin.ajax.QueryAjaxModelLoader object at 0x1101a0b30>
           └ <sqladmin.fields.AjaxSelectField object at 0x110903b90>
  File "/Users/diskream/Library/Caches/pypoetry/virtualenvs/aMY0E6K2-py3.12/lib/python3.12/site-packages/sqladmin/ajax.py", line 59, in format
    return {"id": str(get_object_identifier(model)), "text": str(model)}
                      │                     │                    └ '1'
                      │                     └ '1'
                      └ <function get_object_identifier at 0x110108360>
  File "/Users/diskream/Library/Caches/pypoetry/virtualenvs/aMY0E6K2-py3.12/lib/python3.12/site-packages/sqladmin/helpers.py", line 183, in get_object_identifier
    primary_keys = get_primary_keys(obj)
                   │                └ '1'
                   └ <function get_primary_keys at 0x1101082c0>
  File "/Users/diskream/Library/Caches/pypoetry/virtualenvs/aMY0E6K2-py3.12/lib/python3.12/site-packages/sqladmin/helpers.py", line 178, in get_primary_keys
    return tuple(inspect(model).mapper.primary_key)
                 │       └ '1'
                 └ <function inspect at 0x106b33f60>
  File "/Users/diskream/Library/Caches/pypoetry/virtualenvs/aMY0E6K2-py3.12/lib/python3.12/site-packages/sqlalchemy/inspection.py", line 147, in inspect
    raise exc.NoInspectionAvailable(
          │   └ <class 'sqlalchemy.exc.NoInspectionAvailable'>
          └ <module 'sqlalchemy.exc' from '/Users/diskream/Library/Caches/pypoetry/virtualenvs/aMY0E6K2-py3.12/lib/python3.1...

sqlalchemy.exc.NoInspectionAvailable: No inspection system is available for object of type <class 'str'>

As you can see, the widget receives a string from the form, not the model. A common approach is to handle exceptions at the time of parsing the form.

Here is snippet to reproduce error:

from typing import Any

from sqlalchemy import Column, Integer, ForeignKey, String
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import declarative_base, sessionmaker, relationship
from starlette.applications import Starlette
from starlette.requests import Request

from sqladmin import Admin, ModelView

async_engine = create_async_engine("sqlite+aiosqlite:///test.db")

Base = declarative_base()  # type: Any
session_maker = sessionmaker(bind=async_engine, class_=AsyncSession, expire_on_commit=False)

app = Starlette()


@app.on_event("startup")
async def startup() -> None:
    async with async_engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)


admin = Admin(app=app, engine=async_engine)


class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True)
    name = Column(String(length=16))

    addresses = relationship("Address", back_populates="user")

    def __str__(self) -> str:
        return f"User {self.id}"


class Address(Base):
    __tablename__ = "addresses"

    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey("users.id"))

    user = relationship("User", back_populates="addresses")

    def __str__(self) -> str:
        return f"Address {self.id}"


class UserAdmin(ModelView, model=User):
    form_columns = ("name",)
    form_ajax_refs = {
        "addresses": {
            "fields": ("id",),
        }
    }


class AddressAdmin(ModelView, model=Address):
    form_ajax_refs = {
        "user": {
            "fields": ("name",),
            "order_by": ("id"),
        }
    }

    async def insert_model(self, request: Request, data: dict) -> Any:
        1 / 0


admin.add_view(UserAdmin)
admin.add_view(AddressAdmin)

if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="127.0.0.1", port=8000)

Copy link
Owner

@aminalaee aminalaee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR, I have a question since there's no issue related to this.

@diskream
Copy link
Author

Thanks for the PR, I have a question since there's no issue related to this.

I decided to make a PR, not issue because I already have some workaround about it and provided snippet to reproduce. Should I have created an issue first?

@aminalaee
Copy link
Owner

Sorry I think my comment was discarded, my question was that how is this going to fix or workaround the issue?
This will just ignore the exception right?

@diskream
Copy link
Author

Yes, my solution is to clear ajax field if there is any error on field loading. May be this is not the best solution, but due to there was no exception handling, I think there is no need to do something more serious here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants