Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions sqladmin/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ def add_view(self, view: type[ModelView] | type[BaseView]) -> None:
else:
self.add_base_view(view)

@staticmethod
def _find_decorated_funcs(
self,
view: type[BaseView | ModelView],
view_instance: BaseView | ModelView,
handle_fn: Callable[
Expand All @@ -164,7 +164,11 @@ def _find_decorated_funcs(
) -> None:
funcs = inspect.getmembers(view_instance, predicate=inspect.ismethod)

for _, func in funcs[::-1]:
for _, func in sorted(
funcs,
key=lambda x: inspect.getsourcelines(x[1])[1],
reverse=True,
):
handle_fn(func, view, view_instance)

def _handle_action_decorated_func(
Expand Down
17 changes: 17 additions & 0 deletions tests/test_base_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ async def custom(self, request: Request):
async def custom_report(self, request: Request):
return await self.templates.TemplateResponse(request, "custom.html")

# Add this for second test: Before alphabetically (!)
# first `expose` was BaseView url, now it's first by `order`
@expose("/a")
async def a(self, request: Request):
return await self.templates.TemplateResponse(request, "custom.html")


@pytest.fixture
def client() -> Generator[TestClient, None, None]:
Expand All @@ -44,3 +50,14 @@ def test_base_view(client: TestClient) -> None:

response = client.get("/admin/custom/report")
assert response.status_code == 200


def test_menu_view_url(client: TestClient) -> None:
admin.add_view(CustomAdmin)

response = client.get("/admin")
assert response.status_code == 200

assert (
'<a class="nav-link " href="http://testserver/admin/custom">' in response.text
)
16 changes: 12 additions & 4 deletions tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ async def prepare_data(prepare_database: Any) -> AsyncGenerator[None, None]:
salary=80000.50,
description="Senior administrator with management responsibilities",
birthdate=datetime.date(2001, 7, 14),
created_at=datetime.datetime(2024, 11, 12, 3, 4, 5, tzinfo=datetime.timezone.utc),
created_at=datetime.datetime(
2024, 11, 12, 3, 4, 5, tzinfo=datetime.timezone.utc
),
)
user2 = User(
name="Regular User",
Expand All @@ -173,7 +175,9 @@ async def prepare_data(prepare_database: Any) -> AsyncGenerator[None, None]:
salary=55000.75,
description="Software developer specializing in web applications",
birthdate=datetime.date(1994, 5, 31),
created_at=datetime.datetime(2024, 12, 31, 23, 59, 58, tzinfo=datetime.timezone.utc),
created_at=datetime.datetime(
2024, 12, 31, 23, 59, 58, tzinfo=datetime.timezone.utc
),
)
user3 = User(
name="Test User",
Expand All @@ -184,7 +188,9 @@ async def prepare_data(prepare_database: Any) -> AsyncGenerator[None, None]:
salary=65000.00,
description="Data analyst working on business intelligence",
birthdate=datetime.date(1998, 10, 31),
created_at=datetime.datetime(2023, 3, 14, 12, 30, 0, tzinfo=datetime.timezone.utc),
created_at=datetime.datetime(
2023, 3, 14, 12, 30, 0, tzinfo=datetime.timezone.utc
),
)
session.add_all([user1, user2, user3])
await session.commit()
Expand Down Expand Up @@ -918,7 +924,9 @@ async def test_column_filter_conversion_edge_cases():
result = created_at_filter._convert_value_for_column(
"2021-11-30T22:33:43+00:00", User.created_at.property.columns[0]
)
assert result == datetime.datetime(2021, 11, 30, 22, 33, 43, tzinfo=datetime.timezone.utc)
assert result == datetime.datetime(
2021, 11, 30, 22, 33, 43, tzinfo=datetime.timezone.utc
)

# Test valid date conversion
birthdate_filter = OperationColumnFilter(User.birthdate)
Expand Down
20 changes: 15 additions & 5 deletions tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import enum
from typing import Generator
from uuid import UUID as PyUUID

import pytest
from jinja2 import TemplateNotFound
from markupsafe import Markup
from sqlalchemy import Boolean, Column, Enum, ForeignKey, Integer, String, select
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import (
Mapped,
contains_eager,
declarative_base,
mapped_column,
relationship,
sessionmaker,
)
Expand All @@ -19,9 +22,7 @@

from sqladmin import Admin, ModelView, expose
from sqladmin.exceptions import InvalidModelError
from sqladmin.filters import (
AllUniqueStringValuesFilter,
)
from sqladmin.filters import AllUniqueStringValuesFilter
from sqladmin.helpers import get_column_python_type
from tests.common import sync_engine as engine

Expand Down Expand Up @@ -414,6 +415,16 @@ class PostgresModel(Base):
get_column_python_type(PostgresModel.uuid) is str


@pytest.mark.skipif(engine.name != "postgresql", reason="PostgreSQL only")
def test_get_python_annotated_type_postgresql() -> None:
class PostgresModel(Base):
__tablename__ = "postgres_model2"

uuid: Mapped[PyUUID] = mapped_column(primary_key=True)

get_column_python_type(PostgresModel.uuid) is str


def test_model_default_sort() -> None:
class UserAdmin(ModelView, model=User): ...

Expand Down Expand Up @@ -569,8 +580,7 @@ class AddressAdmin(ModelView, model=Address): ...


def test_count_query() -> None:
class AddressAdmin(ModelView, model=Address):
...
class AddressAdmin(ModelView, model=Address): ...

request = Request({"type": "http"})
stmt = AddressAdmin().count_query(request)
Expand Down
102 changes: 102 additions & 0 deletions tests/test_views/test_uuid_pk_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
from typing import Any, Generator
from uuid import UUID

import pytest
from sqlalchemy import ForeignKey
from sqlalchemy.orm import (
Mapped,
declarative_base,
mapped_column,
relationship,
sessionmaker,
)
from starlette.applications import Starlette
from starlette.testclient import TestClient

from sqladmin import Admin, ModelView
from tests.common import sync_engine as engine

if engine.name != "postgresql":
pytest.skip("PostgreSQL only", allow_module_level=True)


Base = declarative_base() # type: Any
session_maker = sessionmaker(bind=engine)

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


class User(Base):
__tablename__ = "users"

uuid: Mapped[UUID] = mapped_column(primary_key=True)
name: Mapped[str]
posts: Mapped[list["Post"]] = relationship("Post", back_populates="user")

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


class Post(Base):
__tablename__ = "posts"

id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[UUID] = mapped_column(ForeignKey("users.uuid"))
user: Mapped[User] = relationship("User", back_populates="posts")
title: Mapped[str]


@pytest.fixture
def prepare_database() -> Generator[None, None, None]:
Base.metadata.create_all(engine)
yield
Base.metadata.drop_all(engine)


@pytest.fixture
def client(prepare_database: Any) -> Generator[TestClient, None, None]:
with TestClient(app=app, base_url="http://testserver") as c:
yield c


class UserAdmin(ModelView, model=User):
column_list = [User.uuid, User.name, User.posts]


class PostAdmin(ModelView, model=Post):
column_list = [Post.id, Post.title, Post.user]


admin.add_view(UserAdmin)
admin.add_view(PostAdmin)


def base_content():
with session_maker() as session:
user = User(uuid=UUID("00000000-0000-0000-0000-000000000001"), name="John")
session.add(user)

post1 = Post(id=1, title="Post 1", user_id=user.uuid)
post2 = Post(id=2, title="Post 2", user_id=user.uuid)
session.add_all([post1, post2])
session.commit()


def test_uuid_pk_view(client: TestClient) -> None:
base_content()
response = client.get("/admin/user/details/00000000-0000-0000-0000-000000000001")

assert response.status_code == 200


def test_uuid_url_from_posts(client: TestClient) -> None:
base_content()
response = client.get("/admin/post/details/1")

assert response.status_code == 200

assert (
'<a href="http://testserver/admin/user/details/00000000-0000-0000-0000-000000000001">'
in response.text
)
Loading