1- from typing import Type
1+ import json
2+ from typing import Awaitable , Callable , Type
23
4+ import pytest
35import sqlalchemy as sa
4- from sqlalchemy .ext .asyncio import AsyncEngine
6+ from aiohttp import web
7+ from aiohttp .test_utils import TestClient
8+ from sqlalchemy .ext .asyncio import AsyncEngine , async_sessionmaker , create_async_engine
59from sqlalchemy .orm import DeclarativeBase , Mapped , mapped_column , relationship
610
11+ import aiohttp_admin
12+ from _auth import check_credentials
713from aiohttp_admin .backends .sqlalchemy import SAResource
814
15+ _Login = Callable [[TestClient ], Awaitable [dict [str , str ]]]
16+
917
1018def test_pk (base : Type [DeclarativeBase ], mock_engine : AsyncEngine ) -> None :
1119 class TestModel (base ): # type: ignore[misc,valid-type]
@@ -15,7 +23,7 @@ class TestModel(base): # type: ignore[misc,valid-type]
1523
1624 r = SAResource (mock_engine , TestModel )
1725 assert r .name == "dummy"
18- assert r .repr_field == "id"
26+ assert r .primary_key == "id"
1927 assert r .fields == {
2028 "id" : {"type" : "NumberField" , "props" : {}},
2129 "num" : {"type" : "TextField" , "props" : {}}
@@ -34,7 +42,7 @@ def test_table(mock_engine: AsyncEngine) -> None:
3442
3543 r = SAResource (mock_engine , dummy_table )
3644 assert r .name == "dummy"
37- assert r .repr_field == "id"
45+ assert r .primary_key == "id"
3846 assert r .fields == {
3947 "id" : {"type" : "NumberField" , "props" : {}},
4048 "num" : {"type" : "TextField" , "props" : {}}
@@ -57,7 +65,7 @@ class TestChildModel(base): # type: ignore[misc,valid-type]
5765
5866 r = SAResource (mock_engine , TestChildModel )
5967 assert r .name == "child"
60- assert r .repr_field == "id"
68+ assert r .primary_key == "id"
6169 assert r .fields == {"id" : {"type" : "ReferenceField" , "props" : {"reference" : "dummy" }}}
6270 # PK with FK constraint should be shown in create form.
6371 assert r .inputs == {"id" : {
@@ -82,3 +90,104 @@ class TestOne(base): # type: ignore[misc,valid-type]
8290 "props" : {"children" : {"id" : {"props" : {}, "type" : "NumberField" }},
8391 "label" : "Ones" , "reference" : "one" , "source" : "id" , "target" : "many_id" }}
8492 assert "ones" not in r .inputs
93+
94+
95+ async def test_nonid_pk (base : Type [DeclarativeBase ], mock_engine : AsyncEngine ) -> None :
96+ class TestModel (base ): # type: ignore[misc,valid-type]
97+ __tablename__ = "test"
98+ num : Mapped [int ] = mapped_column (primary_key = True )
99+ other : Mapped [str ]
100+
101+ r = SAResource (mock_engine , TestModel )
102+ assert r .name == "test"
103+ assert r .primary_key == "num"
104+ assert r .fields == {
105+ "num" : {"type" : "NumberField" , "props" : {}},
106+ "other" : {"type" : "TextField" , "props" : {}}
107+ }
108+ assert r .inputs == {
109+ "num" : {"type" : "NumberInput" , "show_create" : False , "props" : {}},
110+ "other" : {"type" : "TextInput" , "show_create" : True , "props" : {}}
111+ }
112+
113+
114+ async def test_id_nonpk (base : Type [DeclarativeBase ], mock_engine : AsyncEngine ) -> None :
115+ class NotPK (base ): # type: ignore[misc,valid-type]
116+ __tablename__ = "notpk"
117+ name : Mapped [str ] = mapped_column (primary_key = True )
118+ id : Mapped [int ]
119+
120+ class CompositePK (base ): # type: ignore[misc,valid-type]
121+ __tablename__ = "compound"
122+ id : Mapped [int ] = mapped_column (primary_key = True )
123+ other : Mapped [int ] = mapped_column (primary_key = True )
124+
125+ with pytest .warns (UserWarning , match = "A non-PK 'id' column is likely to break the admin." ):
126+ SAResource (mock_engine , NotPK )
127+ # TODO: Support composite PK.
128+ # with pytest.warns(UserWarning, match="'id' column in a composite PK is likely to"
129+ # + " break the admin"):
130+ # SAResource(mock_engine, CompositePK)
131+
132+
133+ async def test_nonid_pk_api (
134+ base : DeclarativeBase , aiohttp_client : Callable [[web .Application ], Awaitable [TestClient ]],
135+ login : _Login
136+ ) -> None :
137+ class TestModel (base ): # type: ignore[misc,valid-type]
138+ __tablename__ = "test"
139+ num : Mapped [int ] = mapped_column (primary_key = True )
140+ other : Mapped [str ]
141+
142+ app = web .Application ()
143+ engine = create_async_engine ("sqlite+aiosqlite:///:memory:" )
144+ db = async_sessionmaker (engine , expire_on_commit = False )
145+ async with engine .begin () as conn :
146+ await conn .run_sync (base .metadata .create_all )
147+ async with db .begin () as sess :
148+ sess .add (TestModel (num = 5 , other = "foo" ))
149+ sess .add (TestModel (num = 8 , other = "bar" ))
150+
151+ schema : aiohttp_admin .Schema = {
152+ "security" : {
153+ "check_credentials" : check_credentials ,
154+ "secure" : False
155+ },
156+ "resources" : ({"model" : SAResource (engine , TestModel )},)
157+ }
158+ app ["admin" ] = aiohttp_admin .setup (app , schema )
159+
160+ admin_client = await aiohttp_client (app )
161+ assert admin_client .app
162+ h = await login (admin_client )
163+
164+ url = app ["admin" ].router ["test_get_list" ].url_for ()
165+ p = {"pagination" : json .dumps ({"page" : 1 , "perPage" : 10 }),
166+ "sort" : json .dumps ({"field" : "id" , "order" : "DESC" }), "filter" : "{}" }
167+ async with admin_client .get (url , params = p , headers = h ) as resp :
168+ assert resp .status == 200
169+ assert await resp .json () == {"data" : [{"id" : 8 , "num" : 8 , "other" : "bar" },
170+ {"id" : 5 , "num" : 5 , "other" : "foo" }], "total" : 2 }
171+
172+ url = app ["admin" ].router ["test_get_one" ].url_for ()
173+ async with admin_client .get (url , params = {"id" : 8 }, headers = h ) as resp :
174+ assert resp .status == 200
175+ assert await resp .json () == {"data" : {"id" : 8 , "num" : 8 , "other" : "bar" }}
176+
177+ url = app ["admin" ].router ["test_get_many" ].url_for ()
178+ async with admin_client .get (url , params = {"ids" : "[5, 8]" }, headers = h ) as resp :
179+ assert resp .status == 200
180+ assert await resp .json () == {"data" : [{"id" : 5 , "num" : 5 , "other" : "foo" },
181+ {"id" : 8 , "num" : 8 , "other" : "bar" }]}
182+
183+ url = app ["admin" ].router ["test_create" ].url_for ()
184+ p = {"data" : json .dumps ({"num" : 12 , "other" : "this" })}
185+ async with admin_client .post (url , params = p , headers = h ) as resp :
186+ assert resp .status == 200
187+ assert await resp .json () == {"data" : {"id" : 12 , "num" : 12 , "other" : "this" }}
188+
189+ url = app ["admin" ].router ["test_update" ].url_for ()
190+ p1 = {"id" : 5 , "data" : json .dumps ({"id" : 5 , "other" : "that" }), "previousData" : "{}" }
191+ async with admin_client .put (url , params = p1 , headers = h ) as resp :
192+ assert resp .status == 200
193+ assert await resp .json () == {"data" : {"id" : 5 , "num" : 5 , "other" : "that" }}
0 commit comments