14
14
from pydantic import Json
15
15
16
16
from ..security import check , permissions_as_dict
17
- from ..types import ComponentState , InputState , fk
17
+ from ..types import ComponentState , InputState , fk , resources_key
18
18
19
19
if sys .version_info >= (3 , 10 ):
20
20
from typing import TypeAlias
26
26
else :
27
27
from typing_extensions import TypedDict
28
28
29
- _ID = TypeVar ("_ID" )
29
+ _ID = TypeVar ("_ID" , bound = tuple [ object , ...] )
30
30
Record = dict [str , object ]
31
31
Meta = Optional [dict [str , object ]]
32
32
@@ -87,6 +87,22 @@ class GetManyParams(_Params):
87
87
ids : Json [tuple [str , ...]]
88
88
89
89
90
+ class GetManyRefAPIParams (_Params ):
91
+ target : str
92
+ id : str
93
+ pagination : Json [_Pagination ]
94
+ sort : Json [_Sort ]
95
+ filter : Json [dict [str , object ]]
96
+
97
+
98
+ class GetManyRefParams (_Params ):
99
+ target : tuple [str , ...]
100
+ id : tuple [object , ...]
101
+ pagination : Json [_Pagination ]
102
+ sort : Json [_Sort ]
103
+ filter : Json [dict [str , object ]]
104
+
105
+
90
106
class _CreateData (TypedDict ):
91
107
"""Id will not be included for create calls."""
92
108
data : Record
@@ -116,6 +132,11 @@ class DeleteManyParams(_Params):
116
132
ids : Json [tuple [str , ...]]
117
133
118
134
135
+ class _ListQuery (TypedDict ):
136
+ sort : _Sort
137
+ filter : dict [str , object ]
138
+
139
+
119
140
class AbstractAdminResource (ABC , Generic [_ID ]):
120
141
name : str
121
142
fields : dict [str , ComponentState ]
@@ -155,6 +176,10 @@ async def get_one(self, record_id: _ID, meta: Meta) -> Record:
155
176
async def get_many (self , record_ids : Sequence [_ID ], meta : Meta ) -> list [Record ]:
156
177
"""Return the matching records."""
157
178
179
+ @abstractmethod
180
+ async def get_many_ref (self , params : GetManyRefParams ) -> tuple [list [Record ], int ]:
181
+ """Return list of records and total count available (when not paginating)."""
182
+
158
183
@abstractmethod
159
184
async def update (self , record_id : _ID , data : Record , previous_data : Record ,
160
185
meta : Meta ) -> Record :
@@ -176,38 +201,30 @@ async def delete(self, record_id: _ID, previous_data: Record, meta: Meta) -> Rec
176
201
async def delete_many (self , record_ids : Sequence [_ID ], meta : Meta ) -> list [_ID ]:
177
202
"""Delete the matching records and return their IDs."""
178
203
204
+ async def get_many_ref_name (self , target : str , meta : Meta ) -> str :
205
+ """Return the resource name for the reference.
206
+
207
+ This can be used to change which resource should be returned by get_many_ref().
208
+
209
+ For example, if we have an SQLAlchemy model called 'parent' with a relationship
210
+ called children, then a normal get_many_ref_name() call would go to the 'child'
211
+ model with the details from the parent, and the default behaviour would work.
212
+
213
+ However, the SQLAlchemy backend uses the meta to switch this and send the request
214
+ to the 'parent' model instead and then use the children ORM attribute to fetch
215
+ the referenced resources, thus requiring this method to return 'child'.
216
+ This allows the SQLAlchemy backend to support complex relationships (e.g.
217
+ many-to-many) without needing react-admin to know the details.
218
+ """
219
+ return self .name
220
+
179
221
# https://marmelab.com/react-admin/DataProviderWriting.html
180
222
181
223
@final
182
224
async def _get_list (self , request : web .Request ) -> web .Response :
183
225
await check_permission (request , f"admin.{ self .name } .view" , context = (request , None ))
184
226
query = check (GetListParams , request .query )
185
-
186
- # When sort order refers to "id", this should be translated to primary key.
187
- if query ["sort" ]["field" ] == "id" :
188
- query ["sort" ]["field" ] = self .primary_key [0 ]
189
- else :
190
- query ["sort" ]["field" ] = query ["sort" ]["field" ].removeprefix ("data." )
191
-
192
- query ["filter" ].update (check (dict [str , object ], query ["filter" ].pop ("data" , {}))) # type: ignore[type-var]
193
-
194
- merged_filter = {}
195
- for k , v in query ["filter" ].items ():
196
- if k .startswith ("fk_" ):
197
- v = check (str , v )
198
- for c , cv in zip (k .removeprefix ("fk_" ).split ("__" ), v .split ("|" )):
199
- merged_filter [c ] = check (self ._raw_record_type [c ], cv )
200
- else :
201
- merged_filter [k ] = check (self ._raw_record_type [k ], v )
202
- query ["filter" ] = merged_filter
203
-
204
- # Add filters from advanced permissions.
205
- # The permissions will be cached on the request from a previous permissions check.
206
- permissions = permissions_as_dict (request ["aiohttpadmin_permissions" ])
207
- filters = permissions .get (f"admin.{ self .name } .view" ,
208
- permissions .get (f"admin.{ self .name } .*" , {}))
209
- for k , v in filters .items ():
210
- query ["filter" ][k ] = v
227
+ self ._process_list_query (query , request )
211
228
212
229
raw_results , total = await self .get_list (query )
213
230
results = [await self ._convert_record (r , request ) for r in raw_results
@@ -239,6 +256,33 @@ async def _get_many(self, request: web.Request) -> web.Response:
239
256
if await permits (request , f"admin.{ self .name } .view" , context = (request , r ))]
240
257
return json_response ({"data" : results })
241
258
259
+ @final
260
+ async def _get_many_ref (self , request : web .Request ) -> web .Response :
261
+ query = check (GetManyRefAPIParams , request .query )
262
+ meta = query ["filter" ].pop ("__meta__" , None )
263
+ if meta is not None :
264
+ query ["meta" ] = check (dict [str , object ], meta )
265
+ reference = await self .get_many_ref_name (query ["target" ], query .get ("meta" ))
266
+ ref_model = request .app [resources_key ][reference ]
267
+
268
+ await check_permission (request , f"admin.{ ref_model .name } .view" , context = (request , None ))
269
+
270
+ ref_model ._process_list_query (query , request )
271
+
272
+ if query ["target" ].startswith ("fk_" ):
273
+ target = tuple (query ["target" ].removeprefix ("fk_" ).split ("__" ))
274
+ record_id = tuple (check (self ._raw_record_type [k ], v )
275
+ for k , v in zip (target , query ["id" ].split ("|" )))
276
+ else :
277
+ target = (query ["target" ],)
278
+ record_id = check (self ._id_type , query ["id" ].split ("|" ))
279
+
280
+ raw_results , total = await self .get_many_ref ({** query , "target" : target , "id" : record_id })
281
+
282
+ results = [await ref_model ._convert_record (r , request ) for r in raw_results
283
+ if await permits (request , f"admin.{ ref_model .name } .view" , context = (request , r ))]
284
+ return json_response ({"data" : results , "total" : total })
285
+
242
286
@final
243
287
async def _create (self , request : web .Request ) -> web .Response :
244
288
query = check (CreateParams , request .query )
@@ -350,7 +394,7 @@ async def _delete_many(self, request: web.Request) -> web.Response:
350
394
@final
351
395
def _check_record (self , record : Record ) -> Record :
352
396
"""Check and convert input record."""
353
- return check (self ._record_type , record ) # type: ignore[no-any-return]
397
+ return check (self ._record_type , record )
354
398
355
399
@final
356
400
async def _convert_record (self , record : Record , request : web .Request ) -> APIRecord :
@@ -371,6 +415,33 @@ def _convert_ids(self, ids: Sequence[_ID]) -> tuple[str, ...]:
371
415
"""Convert IDs to correct output format."""
372
416
return tuple (str (i ) for i in ids )
373
417
418
+ def _process_list_query (self , query : _ListQuery , request : web .Request ) -> None :
419
+ # When sort order refers to "id", this should be translated to primary key.
420
+ if query ["sort" ]["field" ] == "id" :
421
+ query ["sort" ]["field" ] = self .primary_key [0 ]
422
+ else :
423
+ query ["sort" ]["field" ] = query ["sort" ]["field" ].removeprefix ("data." )
424
+
425
+ query ["filter" ].update (check (dict [str , object ], query ["filter" ].pop ("data" , {})))
426
+
427
+ merged_filter = {}
428
+ for k , v in query ["filter" ].items ():
429
+ if k .startswith ("fk_" ):
430
+ v = check (str , v )
431
+ for c , cv in zip (k .removeprefix ("fk_" ).split ("__" ), v .split ("|" )):
432
+ merged_filter [c ] = check (self ._raw_record_type [c ], cv )
433
+ else :
434
+ merged_filter [k ] = check (self ._raw_record_type [k ], v )
435
+ query ["filter" ] = merged_filter
436
+
437
+ # Add filters from advanced permissions.
438
+ # The permissions will be cached on the request from a previous permissions check.
439
+ permissions = permissions_as_dict (request ["aiohttpadmin_permissions" ])
440
+ filters = permissions .get (f"admin.{ self .name } .view" ,
441
+ permissions .get (f"admin.{ self .name } .*" , {}))
442
+ for k , v in filters .items ():
443
+ query ["filter" ][k ] = v
444
+
374
445
@cached_property
375
446
def routes (self ) -> tuple [web .RouteDef , ...]:
376
447
"""Routes to act on this resource.
@@ -382,6 +453,7 @@ def routes(self) -> tuple[web.RouteDef, ...]:
382
453
web .get (url + "/list" , self ._get_list , name = self .name + "_get_list" ),
383
454
web .get (url + "/one" , self ._get_one , name = self .name + "_get_one" ),
384
455
web .get (url , self ._get_many , name = self .name + "_get_many" ),
456
+ web .get (url + "/ref" , self ._get_many_ref , name = self .name + "_get_many_ref" ),
385
457
web .post (url , self ._create , name = self .name + "_create" ),
386
458
web .put (url + "/update" , self ._update , name = self .name + "_update" ),
387
459
web .put (url + "/update_many" , self ._update_many , name = self .name + "_update_many" ),
0 commit comments