From a1cb30eb6ac8d03a2c484fa0ee0b5405a17b25ee Mon Sep 17 00:00:00 2001 From: Timur Kady Date: Thu, 27 Nov 2025 23:37:48 +0300 Subject: [PATCH] Improve integrity error messaging --- .../contrib/adapters/tortoise/adapter.py | 5 +- freeadmin/core/interface/base.py | 4 +- tests/test_admin_integrity_error.py | 33 +++++++++ tests/test_tortoise_adapter_m2m.py | 72 +++++++++++++++++++ 4 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 tests/test_admin_integrity_error.py create mode 100644 tests/test_tortoise_adapter_m2m.py diff --git a/freeadmin/contrib/adapters/tortoise/adapter.py b/freeadmin/contrib/adapters/tortoise/adapter.py index 7f323cd..cc58957 100644 --- a/freeadmin/contrib/adapters/tortoise/adapter.py +++ b/freeadmin/contrib/adapters/tortoise/adapter.py @@ -16,6 +16,7 @@ from __future__ import annotations +from inspect import isawaitable from typing import Any, Iterable from tortoise import Tortoise, connections @@ -511,7 +512,9 @@ async def m2m_clear(self, manager) -> None: """ if manager is None: return - await manager.clear() + clear_result = manager.clear() + if isawaitable(clear_result): + await clear_result async def m2m_add(self, manager, objs: Iterable[Model]) -> None: """Add multiple objects to a many-to-many relation manager. diff --git a/freeadmin/core/interface/base.py b/freeadmin/core/interface/base.py index 65d5282..c0bcb4c 100644 --- a/freeadmin/core/interface/base.py +++ b/freeadmin/core/interface/base.py @@ -680,7 +680,9 @@ def handle_integrity_error(self, exc: Exception) -> None: Subclasses may override this to provide custom responses and may raise :class:`AdminIntegrityError` with an appropriate message. """ - raise AdminIntegrityError("Integrity error.") + detail = str(exc).strip() + message = f"Integrity error: {detail}" if detail else "Integrity error." + raise AdminIntegrityError(message) def _flatten_fields(self, items: Iterable[Any]) -> list[str]: """Recursively flatten nested field groups into a plain list.""" diff --git a/tests/test_admin_integrity_error.py b/tests/test_admin_integrity_error.py new file mode 100644 index 0000000..576d335 --- /dev/null +++ b/tests/test_admin_integrity_error.py @@ -0,0 +1,33 @@ +"""Unit tests for admin integrity error handling.""" + +from __future__ import annotations + +import pytest + +from freeadmin.core.interface.base import BaseModelAdmin +from freeadmin.core.interface.exceptions import AdminIntegrityError + + +class DummyAdapter: + """Minimal adapter stub for ``BaseModelAdmin`` tests.""" + + def __init__(self) -> None: + """Initialize the adapter stub without extra state.""" + + +class DummyModel: + """Placeholder model type for admin instantiation.""" + + +def test_handle_integrity_error_preserves_detail() -> None: + """Wrap integrity errors using the underlying exception message.""" + + admin = BaseModelAdmin(DummyModel, DummyAdapter()) + with pytest.raises(AdminIntegrityError) as captured: + admin.handle_integrity_error(Exception("unique constraint failed")) + + assert "unique constraint failed" in str(captured.value) + + +# The End + diff --git a/tests/test_tortoise_adapter_m2m.py b/tests/test_tortoise_adapter_m2m.py new file mode 100644 index 0000000..c240cdd --- /dev/null +++ b/tests/test_tortoise_adapter_m2m.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +"""Unit tests for Tortoise adapter many-to-many helpers.""" + +from __future__ import annotations + +import pytest + +from freeadmin.contrib.adapters.tortoise.adapter import Adapter + + +@pytest.fixture +def anyio_backend(): + """Force AnyIO to run tests with the asyncio backend.""" + + return "asyncio" + + +class SyncRelationManager: + """Relation manager with a synchronous ``clear`` implementation.""" + + def __init__(self) -> None: + """Initialize the cleared flag for assertions.""" + + self.cleared = False + + def clear(self): + """Mark the relation as cleared using a synchronous method.""" + + self.cleared = True + return None + + +class AsyncRelationManager: + """Relation manager exposing an asynchronous ``clear`` coroutine.""" + + def __init__(self) -> None: + """Initialize the cleared flag for assertions.""" + + self.cleared = False + + async def clear(self): + """Mark the relation as cleared using an awaited coroutine.""" + + self.cleared = True + + +@pytest.mark.anyio +async def test_m2m_clear_supports_sync_managers(): + """Ensure ``m2m_clear`` handles managers with synchronous ``clear``.""" + + adapter = Adapter.__new__(Adapter) + manager = SyncRelationManager() + + await adapter.m2m_clear(manager) + + assert manager.cleared is True + + +@pytest.mark.anyio +async def test_m2m_clear_supports_async_managers(): + """Ensure ``m2m_clear`` awaits managers exposing async ``clear``.""" + + adapter = Adapter.__new__(Adapter) + manager = AsyncRelationManager() + + await adapter.m2m_clear(manager) + + assert manager.cleared is True + + +# The End +