sqladmin
sqladmin copied to clipboard
Bug fix: unhandled exception during AjaxSelect load
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)
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?
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?
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.
I agree. These changes will not have any negative side effects. It would be good to see this fix in the main branch.
@aminalaee
I have the same problem, how soon will it be fixed?
This my code:
class FileAdmin(ModelView, model=File):
name = "Файл для игры"
name_plural = "Файлы для игры"
icon = "fa-solid fa-file"
column_list = [
File.file_id,
File.path,
]
form_columns = [
File.game,
File.path
]
form_ajax_refs = {
"game": {
"fields": ("game_id", "order_id", "keyword"),
"order_by": "game_id"
}
}
async def on_model_change(self, data: dict, model: Mailing, is_created: bool, request: Request):
if regex.search(pattern=r"\p{IsCyrillic}", string=data["path"].filename):
raise ValueError(
"Название файла не должно содержать кириллицу."
)
When a user uses Cyrillic in the file names, a ValueError exception is thrown, but it cannot be handled properly:
sqlalchemy.exc.NoInspectionAvailable: No inspection system is available for object of type <class 'str'>
And the admin panel does not load:
@Mat0mba24 I don't think your issue is related to ajax, you are raising the error in on_model_change method.
@aminalaee it is.I debugged the admin on error raising in insert_model, update_model and delete_model when Ajax was turned on. It was in the Ajax field load that received a field value (in @Mat0mba24 case it was a Cyrillic string), instead of the Ajax field. I have provided a snippet to reproduce the issue.
@aminalaee
I'm removing ajax:
class FileAdmin(ModelView, model=File):
name = "Файл для игры"
name_plural = "Файлы для игры"
icon = "fa-solid fa-file"
column_list = [
File.file_id,
File.path,
]
form_columns = [
File.game,
File.path
]
# form_ajax_refs = {
# "game": {
# "fields": ("game_id", "order_id", "keyword"),
# "order_by": "game_id"
# }
# }
async def on_model_change(self, data: dict, model: File, is_created: bool, request: Request):
if regex.search(pattern=r"\p{IsCyrillic}", string=data["path"].filename):
raise ValueError(
"Название файла не должно содержать кириллицу."
)
And everything works as intended:
I'm returning it:
class FileAdmin(ModelView, model=File):
name = "Файл для игры"
name_plural = "Файлы для игры"
icon = "fa-solid fa-file"
column_list = [
File.file_id,
File.path,
]
form_columns = [
File.game,
File.path
]
form_ajax_refs = {
"game": {
"fields": ("game_id", "order_id", "keyword"),
"order_by": "game_id"
}
}
async def on_model_change(self, data: dict, model: File, is_created: bool, request: Request):
if regex.search(pattern=r"\p{IsCyrillic}", string=data["path"].filename):
raise ValueError(
"Название файла не должно содержать кириллицу."
)
And again the same mistake:
File "C:\Users\devxd\PycharmProjects\Aiogram3\aiogram3_words_tg_bot\venv\Lib\site-packages\sqladmin\helpers.py", line 176, in get_primary_keys
return tuple(inspect(model).mapper.primary_key)
^^^^^^^^^^^^^^
File "C:\Users\devxd\PycharmProjects\Aiogram3\aiogram3_words_tg_bot\venv\Lib\site-packages\sqlalchemy\inspection.py", line 147, in inspect
raise exc.NoInspectionAvailable(
sqlalchemy.exc.NoInspectionAvailable: No inspection system is available for object of type <class 'str'>
It was a bit confusing but at least I know what the issue is now.
Theform_ajax_refs
creates the special HTML form and widget with Select2. When there's an error with this form, we only have the string PK of the model, and we don't have access to the User
object, in this case.
The suggested solution works around this, but it causes a side-effect that when we are redirected to the form again, we lose the state of this select2 input because we set data
to None.
I think ideally we could force select2 to pass ID and Text of the selected item to backend so we could keep the input state in case of error.