1
- from typing import Type
1
+ import json
2
+ from typing import Awaitable , Callable , Type
2
3
4
+ import pytest
3
5
import 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
5
9
from sqlalchemy .orm import DeclarativeBase , Mapped , mapped_column , relationship
6
10
11
+ import aiohttp_admin
12
+ from _auth import check_credentials
7
13
from aiohttp_admin .backends .sqlalchemy import SAResource
8
14
15
+ _Login = Callable [[TestClient ], Awaitable [dict [str , str ]]]
16
+
9
17
10
18
def test_pk (base : Type [DeclarativeBase ], mock_engine : AsyncEngine ) -> None :
11
19
class TestModel (base ): # type: ignore[misc,valid-type]
@@ -15,7 +23,7 @@ class TestModel(base): # type: ignore[misc,valid-type]
15
23
16
24
r = SAResource (mock_engine , TestModel )
17
25
assert r .name == "dummy"
18
- assert r .repr_field == "id"
26
+ assert r .primary_key == "id"
19
27
assert r .fields == {
20
28
"id" : {"type" : "NumberField" , "props" : {}},
21
29
"num" : {"type" : "TextField" , "props" : {}}
@@ -34,7 +42,7 @@ def test_table(mock_engine: AsyncEngine) -> None:
34
42
35
43
r = SAResource (mock_engine , dummy_table )
36
44
assert r .name == "dummy"
37
- assert r .repr_field == "id"
45
+ assert r .primary_key == "id"
38
46
assert r .fields == {
39
47
"id" : {"type" : "NumberField" , "props" : {}},
40
48
"num" : {"type" : "TextField" , "props" : {}}
@@ -57,7 +65,7 @@ class TestChildModel(base): # type: ignore[misc,valid-type]
57
65
58
66
r = SAResource (mock_engine , TestChildModel )
59
67
assert r .name == "child"
60
- assert r .repr_field == "id"
68
+ assert r .primary_key == "id"
61
69
assert r .fields == {"id" : {"type" : "ReferenceField" , "props" : {"reference" : "dummy" }}}
62
70
# PK with FK constraint should be shown in create form.
63
71
assert r .inputs == {"id" : {
@@ -82,3 +90,104 @@ class TestOne(base): # type: ignore[misc,valid-type]
82
90
"props" : {"children" : {"id" : {"props" : {}, "type" : "NumberField" }},
83
91
"label" : "Ones" , "reference" : "one" , "source" : "id" , "target" : "many_id" }}
84
92
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