fastadmin 0.3.4__tar.gz → 0.3.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. {fastadmin-0.3.4 → fastadmin-0.3.5}/PKG-INFO +1 -1
  2. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/api/frameworks/django/app/api.py +15 -1
  3. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/api/frameworks/fastapi/api.py +26 -3
  4. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/api/frameworks/flask/api.py +15 -1
  5. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/api/service.py +67 -34
  6. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/models/base.py +47 -0
  7. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/models/helpers.py +6 -0
  8. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/models/orms/django.py +16 -4
  9. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/models/orms/ponyorm.py +13 -2
  10. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/models/orms/sqlalchemy.py +32 -2
  11. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/models/orms/tortoise.py +13 -2
  12. {fastadmin-0.3.4 → fastadmin-0.3.5}/pyproject.toml +1 -1
  13. {fastadmin-0.3.4 → fastadmin-0.3.5}/LICENSE +0 -0
  14. {fastadmin-0.3.4 → fastadmin-0.3.5}/README.md +0 -0
  15. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/__init__.py +0 -0
  16. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/api/__init__.py +0 -0
  17. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/api/exceptions.py +0 -0
  18. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/api/frameworks/__init__.py +0 -0
  19. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/api/frameworks/django/__init__.py +0 -0
  20. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/api/frameworks/django/app/__init__.py +0 -0
  21. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/api/frameworks/django/app/urls.py +0 -0
  22. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/api/frameworks/django/app/views.py +0 -0
  23. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/api/frameworks/fastapi/__init__.py +0 -0
  24. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/api/frameworks/fastapi/app.py +0 -0
  25. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/api/frameworks/fastapi/views.py +0 -0
  26. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/api/frameworks/flask/__init__.py +0 -0
  27. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/api/frameworks/flask/app.py +0 -0
  28. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/api/frameworks/flask/views.py +0 -0
  29. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/api/helpers.py +0 -0
  30. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/api/schemas.py +0 -0
  31. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/models/__init__.py +0 -0
  32. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/models/decorators.py +0 -0
  33. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/models/orms/__init__.py +0 -0
  34. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/models/schemas.py +0 -0
  35. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/settings.py +0 -0
  36. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/static/assets/worker-6Z7niv9l.js +0 -0
  37. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/static/assets/worker-C151k0-L.js +0 -0
  38. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/static/assets/worker-C9KPOOKD.js +0 -0
  39. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/static/assets/worker-D1fCgYB7.js +0 -0
  40. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/static/assets/worker-D3TWcJOI.js +0 -0
  41. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/static/assets/worker-DRNcaZ-V.js +0 -0
  42. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/static/assets/worker-MF2p-l5_.js +0 -0
  43. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/static/images/favicon.png +0 -0
  44. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/static/images/header-logo.svg +0 -0
  45. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/static/images/sign-in-logo.svg +0 -0
  46. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/static/index.html +0 -0
  47. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/static/index.min.css +0 -0
  48. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/static/index.min.js +0 -0
  49. {fastadmin-0.3.4 → fastadmin-0.3.5}/fastadmin/templates/index.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastadmin
3
- Version: 0.3.4
3
+ Version: 0.3.5
4
4
  Summary: FastAdmin is an easy-to-use Admin Dashboard App for FastAPI/Flask/Django inspired by Django Admin.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -64,6 +64,7 @@ async def sign_in(request: HttpRequest) -> JsonResponse:
64
64
  session_id = await api_service.sign_in(
65
65
  request.COOKIES.get(settings.ADMIN_SESSION_ID_KEY, None),
66
66
  payload,
67
+ request=request,
67
68
  )
68
69
 
69
70
  response = JsonResponse({})
@@ -111,7 +112,10 @@ async def me(request: HttpRequest) -> JsonResponse:
111
112
  if not user_id:
112
113
  raise AdminApiException(401, "User is not authenticated.")
113
114
  obj = await api_service.get(
114
- request.COOKIES.get(settings.ADMIN_SESSION_ID_KEY, None), settings.ADMIN_USER_MODEL, user_id
115
+ request.COOKIES.get(settings.ADMIN_SESSION_ID_KEY, None),
116
+ settings.ADMIN_USER_MODEL,
117
+ user_id,
118
+ request=request,
115
119
  )
116
120
  return JsonResponse(obj)
117
121
 
@@ -143,6 +147,7 @@ async def dashboard_widget(request: HttpRequest, model: str) -> JsonResponse:
143
147
  min_x_field=min_x_field,
144
148
  max_x_field=max_x_field,
145
149
  period_x_field=period_x_field,
150
+ request=request,
146
151
  )
147
152
  return JsonResponse(data)
148
153
  except AdminApiException as e:
@@ -182,6 +187,7 @@ async def list_objs(request: HttpRequest, model: str) -> JsonResponse:
182
187
  filters=list_filters,
183
188
  offset=offset,
184
189
  limit=limit,
190
+ request=request,
185
191
  )
186
192
  return JsonResponse(
187
193
  {
@@ -212,6 +218,7 @@ async def get(request: HttpRequest, model: str, id: UUID | int | str) -> JsonRes
212
218
  request.COOKIES.get(settings.ADMIN_SESSION_ID_KEY, None),
213
219
  model,
214
220
  id,
221
+ request=request,
215
222
  )
216
223
  return JsonResponse(obj)
217
224
 
@@ -234,6 +241,7 @@ async def add(request: HttpRequest, model: str) -> JsonResponse:
234
241
  request.COOKIES.get(settings.ADMIN_SESSION_ID_KEY, None),
235
242
  model,
236
243
  json.loads(request.body),
244
+ request=request,
237
245
  )
238
246
  return JsonResponse(obj)
239
247
  except AdminApiException as e:
@@ -257,6 +265,7 @@ async def change_password(request: HttpRequest, id: UUID | int | str) -> JsonRes
257
265
  request.COOKIES.get(settings.ADMIN_SESSION_ID_KEY, None),
258
266
  id,
259
267
  json.loads(request.body),
268
+ request=request,
260
269
  )
261
270
  return JsonResponse(id, safe=False)
262
271
 
@@ -283,6 +292,7 @@ async def change(request: HttpRequest, model: str, id: UUID | int | str) -> Json
283
292
  model,
284
293
  id,
285
294
  json.loads(request.body),
295
+ request=request,
286
296
  )
287
297
  return JsonResponse(obj)
288
298
 
@@ -319,6 +329,7 @@ async def export(request: HttpRequest, model: str) -> JsonResponse:
319
329
  search=search,
320
330
  sort_by=sort_by,
321
331
  filters=list_filters,
332
+ request=request,
322
333
  )
323
334
  response = StreamingHttpResponse(stream, content_type=content_type)
324
335
  response.headers["Content-Disposition"] = f'attachment; filename="{file_name}"'
@@ -349,6 +360,7 @@ async def delete(
349
360
  request.COOKIES.get(settings.ADMIN_SESSION_ID_KEY, None),
350
361
  model,
351
362
  id,
363
+ request=request,
352
364
  )
353
365
  return JsonResponse(deleted_id, safe=False)
354
366
 
@@ -378,6 +390,7 @@ async def action(
378
390
  model,
379
391
  action,
380
392
  payload,
393
+ request=request,
381
394
  )
382
395
  return JsonResponse({})
383
396
 
@@ -397,5 +410,6 @@ async def configuration(request: HttpRequest) -> JsonResponse:
397
410
 
398
411
  obj = await api_service.get_configuration(
399
412
  request.COOKIES.get(settings.ADMIN_SESSION_ID_KEY, None),
413
+ request=request,
400
414
  )
401
415
  return JsonResponse(asdict(obj))
@@ -35,6 +35,7 @@ async def sign_in(
35
35
  session_id = await api_service.sign_in(
36
36
  request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
37
37
  payload,
38
+ request=request,
38
39
  )
39
40
 
40
41
  response.set_cookie(settings.ADMIN_SESSION_ID_KEY, value=session_id, httponly=True)
@@ -78,7 +79,10 @@ async def me(
78
79
  if not user_id:
79
80
  raise AdminApiException(401, "User is not authenticated.")
80
81
  return await api_service.get(
81
- request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None), settings.ADMIN_USER_MODEL, user_id
82
+ request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
83
+ settings.ADMIN_USER_MODEL,
84
+ user_id,
85
+ request=request,
82
86
  )
83
87
  except AdminApiException as e:
84
88
  raise HTTPException(e.status_code, detail=e.detail) from None
@@ -107,6 +111,7 @@ async def dashboard_widget(
107
111
  min_x_field=min_x_field,
108
112
  max_x_field=max_x_field,
109
113
  period_x_field=period_x_field,
114
+ request=request,
110
115
  )
111
116
  return data
112
117
  except AdminApiException as e:
@@ -146,6 +151,7 @@ async def list_objs(
146
151
  filters=list_filters,
147
152
  offset=offset,
148
153
  limit=limit,
154
+ request=request,
149
155
  )
150
156
  return {
151
157
  "total": total,
@@ -172,6 +178,7 @@ async def get(
172
178
  request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
173
179
  model,
174
180
  id,
181
+ request=request,
175
182
  )
176
183
  except AdminApiException as e:
177
184
  raise HTTPException(e.status_code, detail=e.detail) from None
@@ -194,6 +201,7 @@ async def add(
194
201
  request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
195
202
  model,
196
203
  payload,
204
+ request=request,
197
205
  )
198
206
  except AdminApiException as e:
199
207
  raise HTTPException(e.status_code, detail=e.detail) from None
@@ -212,7 +220,12 @@ async def change_password(
212
220
  :return: An object.
213
221
  """
214
222
  try:
215
- await api_service.change_password(request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None), id, payload)
223
+ await api_service.change_password(
224
+ request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
225
+ id,
226
+ payload,
227
+ request=request,
228
+ )
216
229
  return id
217
230
  except AdminApiException as e:
218
231
  raise HTTPException(e.status_code, detail=e.detail) from None
@@ -233,7 +246,13 @@ async def change(
233
246
  :return: An object.
234
247
  """
235
248
  try:
236
- return await api_service.change(request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None), model, id, payload)
249
+ return await api_service.change(
250
+ request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
251
+ model,
252
+ id,
253
+ payload,
254
+ request=request,
255
+ )
237
256
  except AdminApiException as e:
238
257
  raise HTTPException(e.status_code, detail=e.detail) from None
239
258
 
@@ -268,6 +287,7 @@ async def export(
268
287
  search=search,
269
288
  sort_by=sort_by,
270
289
  filters=list_filters,
290
+ request=request,
271
291
  )
272
292
  headers = {"Content-Disposition": f'attachment; filename="{file_name}"'}
273
293
  return StreamingResponse(
@@ -296,6 +316,7 @@ async def delete(
296
316
  request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
297
317
  model,
298
318
  id,
319
+ request=request,
299
320
  )
300
321
  except AdminApiException as e:
301
322
  raise HTTPException(e.status_code, detail=e.detail) from None
@@ -321,6 +342,7 @@ async def action(
321
342
  model,
322
343
  action,
323
344
  payload,
345
+ request=request,
324
346
  )
325
347
  except AdminApiException as e:
326
348
  raise HTTPException(e.status_code, detail=e.detail) from None
@@ -337,4 +359,5 @@ async def configuration(
337
359
  """
338
360
  return await api_service.get_configuration(
339
361
  request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
362
+ request=request,
340
363
  )
@@ -30,6 +30,7 @@ async def sign_in() -> Response:
30
30
  session_id = await api_service.sign_in(
31
31
  request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
32
32
  payload,
33
+ request=request,
33
34
  )
34
35
  response = make_response({})
35
36
  response.set_cookie(settings.ADMIN_SESSION_ID_KEY, value=session_id, httponly=True)
@@ -74,7 +75,10 @@ async def me() -> dict:
74
75
  if not user_id:
75
76
  raise AdminApiException(401, "User is not authenticated.")
76
77
  return await api_service.get(
77
- request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None), settings.ADMIN_USER_MODEL, user_id
78
+ request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
79
+ settings.ADMIN_USER_MODEL,
80
+ user_id,
81
+ request=request,
78
82
  )
79
83
  except AdminApiException as e:
80
84
  http_exception = HTTPException(e.detail)
@@ -104,6 +108,7 @@ async def dashboard_widget(model: str) -> dict:
104
108
  min_x_field=min_x_field,
105
109
  max_x_field=max_x_field,
106
110
  period_x_field=period_x_field,
111
+ request=request,
107
112
  )
108
113
  return data
109
114
  except AdminApiException as e:
@@ -142,6 +147,7 @@ async def list_objs(model: str) -> dict:
142
147
  filters=list_filters,
143
148
  offset=offset,
144
149
  limit=limit,
150
+ request=request,
145
151
  )
146
152
  return {
147
153
  "total": total,
@@ -174,6 +180,7 @@ async def get(model: str, id: UUID | int | str) -> dict:
174
180
  request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
175
181
  model,
176
182
  id,
183
+ request=request,
177
184
  )
178
185
  except AdminApiException as e:
179
186
  http_exception = HTTPException(e.detail)
@@ -195,6 +202,7 @@ async def add(model: str) -> dict:
195
202
  request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
196
203
  model,
197
204
  payload,
205
+ request=request,
198
206
  )
199
207
  except AdminApiException as e:
200
208
  http_exception = HTTPException(e.detail)
@@ -220,6 +228,7 @@ async def change_password(id: UUID | int | str) -> UUID | int | str:
220
228
  request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
221
229
  id,
222
230
  payload,
231
+ request=request,
223
232
  )
224
233
  return id
225
234
  except AdminApiException as e:
@@ -248,6 +257,7 @@ async def change(model: str, id: UUID | int | str) -> dict:
248
257
  model,
249
258
  id,
250
259
  payload,
260
+ request=request,
251
261
  )
252
262
  except AdminApiException as e:
253
263
  http_exception = HTTPException(e.detail)
@@ -283,6 +293,7 @@ async def export(model: str) -> Response:
283
293
  search=search,
284
294
  sort_by=sort_by,
285
295
  filters=list_filters,
296
+ request=request,
286
297
  )
287
298
  response = Response(stream, mimetype=content_type)
288
299
  response.headers["Content-Disposition"] = f'attachment; filename="{file_name}"'
@@ -313,6 +324,7 @@ async def delete(
313
324
  request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
314
325
  model,
315
326
  id,
327
+ request=request,
316
328
  )
317
329
  except AdminApiException as e:
318
330
  http_exception = HTTPException(e.detail)
@@ -340,6 +352,7 @@ async def action(
340
352
  model,
341
353
  action,
342
354
  payload,
355
+ request=request,
343
356
  )
344
357
  return {}
345
358
  except AdminApiException as e:
@@ -357,5 +370,6 @@ async def configuration() -> dict:
357
370
  """
358
371
  obj = await api_service.get_configuration(
359
372
  request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
373
+ request=request,
360
374
  )
361
375
  return asdict(obj)
@@ -102,10 +102,31 @@ async def get_user_id_from_session_id(session_id: str | None) -> UUID | int | No
102
102
 
103
103
 
104
104
  class ApiService:
105
+ @staticmethod
106
+ def _bind_admin_context(
107
+ admin_model: ModelAdmin | InlineModelAdmin | Any, request: Any | None, user: Any | None
108
+ ) -> None:
109
+ if hasattr(admin_model, "set_context"):
110
+ admin_model.set_context(request=request, user=user)
111
+
112
+ async def _get_authenticated_user(self, session_id: str | None) -> tuple[UUID | int, Any | None]:
113
+ current_user_id = await get_user_id_from_session_id(session_id)
114
+ if not current_user_id:
115
+ raise AdminApiException(401, detail="User is not authenticated.")
116
+
117
+ admin_user_model = get_admin_model(settings.ADMIN_USER_MODEL)
118
+ current_user = (
119
+ await admin_user_model.get_obj(current_user_id)
120
+ if admin_user_model and hasattr(admin_user_model, "get_obj")
121
+ else None
122
+ )
123
+ return current_user_id, current_user
124
+
105
125
  async def sign_in(
106
126
  self,
107
127
  session_id: str | None,
108
128
  payload: SignInInputSchema,
129
+ request: Any | None = None,
109
130
  ) -> str:
110
131
  model = settings.ADMIN_USER_MODEL
111
132
  admin_model = get_admin_model(model)
@@ -118,6 +139,7 @@ class ApiService:
118
139
  else:
119
140
  authenticate_fn = sync_to_async(admin_model.authenticate) # type: ignore [arg-type]
120
141
 
142
+ self._bind_admin_context(admin_model, request=request, user=None)
121
143
  user_id = await authenticate_fn(payload.username, payload.password)
122
144
 
123
145
  if not user_id or not isinstance(user_id, int | UUID):
@@ -139,10 +161,9 @@ class ApiService:
139
161
  async def sign_out(
140
162
  self,
141
163
  session_id: str | None,
164
+ request: Any | None = None,
142
165
  ) -> bool:
143
- current_user_id = await get_user_id_from_session_id(session_id)
144
- if not current_user_id:
145
- raise AdminApiException(401, detail="User is not authenticated.")
166
+ _current_user_id, _current_user = await self._get_authenticated_user(session_id)
146
167
 
147
168
  return True
148
169
 
@@ -153,10 +174,9 @@ class ApiService:
153
174
  min_x_field: str | None = None,
154
175
  max_x_field: str | None = None,
155
176
  period_x_field: str | None = None,
177
+ request: Any | None = None,
156
178
  ) -> dict[str, str | int | float]:
157
- current_user_id = await get_user_id_from_session_id(session_id)
158
- if not current_user_id:
159
- raise AdminApiException(401, detail="User is not authenticated.")
179
+ _current_user_id, current_user = await self._get_authenticated_user(session_id)
160
180
 
161
181
  query_params = DashboardWidgetQuerySchema(
162
182
  min_x_field=min_x_field,
@@ -168,6 +188,8 @@ class ApiService:
168
188
  if not dashboard_widget_model:
169
189
  raise AdminApiException(404, detail=f"{model} model is not registered.")
170
190
 
191
+ self._bind_admin_context(dashboard_widget_model, request=request, user=current_user)
192
+
171
193
  if inspect.iscoroutinefunction(dashboard_widget_model.get_data):
172
194
  get_data = dashboard_widget_model.get_data
173
195
  else:
@@ -190,10 +212,9 @@ class ApiService:
190
212
  filters: dict | None = None,
191
213
  offset: int | None = 0,
192
214
  limit: int | None = 10,
215
+ request: Any | None = None,
193
216
  ) -> tuple[list[dict], int]:
194
- current_user_id = await get_user_id_from_session_id(session_id)
195
- if not current_user_id:
196
- raise AdminApiException(401, detail="User is not authenticated.")
217
+ _current_user_id, current_user = await self._get_authenticated_user(session_id)
197
218
 
198
219
  query_params = ListQuerySchema(
199
220
  search=search,
@@ -206,6 +227,7 @@ class ApiService:
206
227
  admin_model = get_admin_or_admin_inline_model(model)
207
228
  if not admin_model:
208
229
  raise AdminApiException(404, detail=f"{model} model is not registered.")
230
+ self._bind_admin_context(admin_model, request=request, user=current_user)
209
231
 
210
232
  # validations
211
233
  fields = set(admin_model.get_fields_for_serialize())
@@ -256,14 +278,14 @@ class ApiService:
256
278
  session_id: str | None,
257
279
  model: str,
258
280
  id: UUID | int | str,
281
+ request: Any | None = None,
259
282
  ) -> dict:
260
- current_user_id = await get_user_id_from_session_id(session_id)
261
- if not current_user_id:
262
- raise AdminApiException(401, detail="User is not authenticated.")
283
+ _current_user_id, current_user = await self._get_authenticated_user(session_id)
263
284
 
264
285
  admin_model = get_admin_or_admin_inline_model(model)
265
286
  if not admin_model:
266
287
  raise AdminApiException(404, detail=f"{model} model is not registered.")
288
+ self._bind_admin_context(admin_model, request=request, user=current_user)
267
289
 
268
290
  try:
269
291
  obj = await admin_model.get_obj(id)
@@ -278,14 +300,14 @@ class ApiService:
278
300
  session_id: str | None,
279
301
  model: str,
280
302
  payload: dict,
303
+ request: Any | None = None,
281
304
  ) -> dict:
282
- current_user_id = await get_user_id_from_session_id(session_id)
283
- if not current_user_id:
284
- raise AdminApiException(401, detail="User is not authenticated.")
305
+ _current_user_id, current_user = await self._get_authenticated_user(session_id)
285
306
 
286
307
  admin_model = get_admin_or_admin_inline_model(model)
287
308
  if not admin_model:
288
309
  raise AdminApiException(404, detail=f"{model} model is not registered.")
310
+ self._bind_admin_context(admin_model, request=request, user=current_user)
289
311
  return await admin_model.save_model(None, payload) # type: ignore [return-value]
290
312
 
291
313
  async def change_password(
@@ -293,14 +315,14 @@ class ApiService:
293
315
  session_id: str | None,
294
316
  id: UUID | int | str,
295
317
  payload: dict,
318
+ request: Any | None = None,
296
319
  ) -> None:
297
- current_user_id = await get_user_id_from_session_id(session_id)
298
- if not current_user_id:
299
- raise AdminApiException(401, detail="User is not authenticated.")
320
+ _current_user_id, current_user = await self._get_authenticated_user(session_id)
300
321
 
301
322
  admin_model = get_admin_model(settings.ADMIN_USER_MODEL)
302
323
  if not admin_model:
303
324
  raise AdminApiException(404, detail=f"{settings.ADMIN_USER_MODEL} model is not registered.")
325
+ self._bind_admin_context(admin_model, request=request, user=current_user)
304
326
 
305
327
  payload = ChangePasswordInputSchema(**payload)
306
328
  if payload.password != payload.confirm_password:
@@ -333,14 +355,14 @@ class ApiService:
333
355
  model: str,
334
356
  id: UUID | int | str,
335
357
  payload: dict,
358
+ request: Any | None = None,
336
359
  ) -> dict:
337
- current_user_id = await get_user_id_from_session_id(session_id)
338
- if not current_user_id:
339
- raise AdminApiException(401, detail="User is not authenticated.")
360
+ _current_user_id, current_user = await self._get_authenticated_user(session_id)
340
361
 
341
362
  admin_model = get_admin_or_admin_inline_model(model)
342
363
  if not admin_model:
343
364
  raise AdminApiException(404, detail=f"{model} model is not registered.")
365
+ self._bind_admin_context(admin_model, request=request, user=current_user)
344
366
 
345
367
  try:
346
368
  obj = await admin_model.save_model(id, payload)
@@ -358,10 +380,9 @@ class ApiService:
358
380
  search: str | None = None,
359
381
  sort_by: str | None = None,
360
382
  filters: dict | None = None,
383
+ request: Any | None = None,
361
384
  ) -> tuple[str, str, StringIO | BytesIO | None]:
362
- current_user_id = await get_user_id_from_session_id(session_id)
363
- if not current_user_id:
364
- raise AdminApiException(401, detail="User is not authenticated.")
385
+ _current_user_id, current_user = await self._get_authenticated_user(session_id)
365
386
 
366
387
  query_params = ListQuerySchema(
367
388
  search=search,
@@ -374,6 +395,7 @@ class ApiService:
374
395
  admin_model = get_admin_or_admin_inline_model(model)
375
396
  if not admin_model:
376
397
  raise AdminApiException(404, detail=f"{model} model is not registered.")
398
+ self._bind_admin_context(admin_model, request=request, user=current_user)
377
399
 
378
400
  # validations
379
401
  fields = set(admin_model.get_fields_for_serialize())
@@ -432,14 +454,14 @@ class ApiService:
432
454
  session_id: str | None,
433
455
  model: str,
434
456
  id: UUID | int | str,
457
+ request: Any | None = None,
435
458
  ) -> UUID | int | str:
436
- current_user_id = await get_user_id_from_session_id(session_id)
437
- if not current_user_id:
438
- raise AdminApiException(401, detail="User is not authenticated.")
459
+ current_user_id, current_user = await self._get_authenticated_user(session_id)
439
460
 
440
461
  admin_model = get_admin_or_admin_inline_model(model)
441
462
  if not admin_model:
442
463
  raise AdminApiException(404, detail=f"{model} model is not registered.")
464
+ self._bind_admin_context(admin_model, request=request, user=current_user)
443
465
 
444
466
  if str(current_user_id) == str(id) and model == settings.ADMIN_USER_MODEL:
445
467
  raise AdminApiException(403, detail="You cannot delete yourself.")
@@ -449,14 +471,20 @@ class ApiService:
449
471
  raise AdminApiException(404, detail=f"{model} not found.") from None
450
472
  return id
451
473
 
452
- async def action(self, session_id: str | None, model: str, action: str, payload: ActionInputSchema) -> None:
453
- current_user_id = await get_user_id_from_session_id(session_id)
454
- if not current_user_id:
455
- raise AdminApiException(401, detail="User is not authenticated.")
474
+ async def action(
475
+ self,
476
+ session_id: str | None,
477
+ model: str,
478
+ action: str,
479
+ payload: ActionInputSchema,
480
+ request: Any | None = None,
481
+ ) -> None:
482
+ _current_user_id, current_user = await self._get_authenticated_user(session_id)
456
483
 
457
484
  admin_model = get_admin_or_admin_inline_model(model)
458
485
  if not admin_model:
459
486
  raise AdminApiException(404, detail=f"{model} model is not registered.")
487
+ self._bind_admin_context(admin_model, request=request, user=current_user)
460
488
 
461
489
  if action not in admin_model.actions:
462
490
  raise AdminApiException(422, detail=f"{action} action is not in actions setting.")
@@ -475,9 +503,11 @@ class ApiService:
475
503
  async def get_configuration(
476
504
  self,
477
505
  session_id: str | None,
506
+ request: Any | None = None,
478
507
  ) -> ConfigurationSchema:
479
- current_user_id = await get_user_id_from_session_id(session_id)
480
- if not current_user_id:
508
+ try:
509
+ current_user_id, current_user = await self._get_authenticated_user(session_id)
510
+ except AdminApiException:
481
511
  return ConfigurationSchema(
482
512
  site_name=settings.ADMIN_SITE_NAME,
483
513
  site_sign_in_logo=settings.ADMIN_SITE_SIGN_IN_LOGO,
@@ -493,7 +523,10 @@ class ApiService:
493
523
  )
494
524
 
495
525
  admin_models = cast(dict[Any, ModelAdmin | InlineModelAdmin], get_admin_models())
496
- models = cast(Sequence[ModelSchema], await generate_models_schema(admin_models, user_id=current_user_id))
526
+ models = cast(
527
+ Sequence[ModelSchema],
528
+ await generate_models_schema(admin_models, user_id=current_user_id, user=current_user, request=request),
529
+ )
497
530
  dashboard_widgets = generate_dashboard_widgets_schema()
498
531
  return ConfigurationSchema(
499
532
  site_name=settings.ADMIN_SITE_NAME,
@@ -3,6 +3,7 @@ import datetime
3
3
  import inspect
4
4
  import json
5
5
  from collections.abc import Sequence
6
+ from contextvars import ContextVar
6
7
  from io import BytesIO, StringIO
7
8
  from typing import Any
8
9
  from uuid import UUID
@@ -16,6 +17,9 @@ from fastadmin.models.schemas import DashboardWidgetType, ModelFieldWidgetSchema
16
17
  class BaseModelAdmin:
17
18
  """Base class for model admin"""
18
19
 
20
+ _request_context: ContextVar[Any | None]
21
+ _user_context: ContextVar[Any | None]
22
+
19
23
  # Use it only if you use several orms in your project.
20
24
  model_name_prefix: str | None = None
21
25
 
@@ -198,6 +202,23 @@ class BaseModelAdmin:
198
202
  :params model_cls: an orm/db model class.
199
203
  """
200
204
  self.model_cls = model_cls
205
+ self._request_context = ContextVar(f"fastadmin_admin_request_context_{id(self)}", default=None)
206
+ self._user_context = ContextVar(f"fastadmin_admin_user_context_{id(self)}", default=None)
207
+
208
+ @property
209
+ def request(self) -> Any | None:
210
+ """Current request object for this async context."""
211
+ return self._request_context.get()
212
+
213
+ @property
214
+ def user(self) -> Any | None:
215
+ """Current authenticated user object for this async context."""
216
+ return self._user_context.get()
217
+
218
+ def set_context(self, request: Any | None = None, user: Any | None = None) -> None:
219
+ """Set request/user context for the current async task."""
220
+ self._request_context.set(request)
221
+ self._user_context.set(user)
201
222
 
202
223
  @staticmethod
203
224
  def get_model_pk_name(orm_model_cls: Any) -> str:
@@ -227,6 +248,8 @@ class BaseModelAdmin:
227
248
  search: str | None = None,
228
249
  sort_by: str | None = None,
229
250
  filters: dict | None = None,
251
+ prefetch_related_fields: list[str] | None = None,
252
+ additional_search_fields: list[str] | None = None,
230
253
  ) -> tuple[list[Any], int]:
231
254
  """This method is used to get list of orm/db model objects.
232
255
 
@@ -235,6 +258,8 @@ class BaseModelAdmin:
235
258
  :params search: a search query.
236
259
  :params sort_by: a sort by field name.
237
260
  :params filters: a dict of filters.
261
+ :params prefetch_related_fields: a list of related fields to prefetch.
262
+ :params additional_search_fields: a list of additional search fields.
238
263
  :return: A tuple of list of objects and total count.
239
264
  """
240
265
  raise NotImplementedError
@@ -697,6 +722,9 @@ class ModelAdmin(BaseModelAdmin):
697
722
 
698
723
 
699
724
  class DashboardWidgetAdmin:
725
+ _request_context: ContextVar[Any | None]
726
+ _user_context: ContextVar[Any | None]
727
+
700
728
  title: str
701
729
  dashboard_widget_type: DashboardWidgetType
702
730
  x_field: str
@@ -706,6 +734,25 @@ class DashboardWidgetAdmin:
706
734
  x_field_filter_widget_props: dict[str, Any] | None = None
707
735
  x_field_periods: list[str] | None = None
708
736
 
737
+ def __init__(self) -> None:
738
+ self._request_context = ContextVar(f"fastadmin_dashboard_request_context_{id(self)}", default=None)
739
+ self._user_context = ContextVar(f"fastadmin_dashboard_user_context_{id(self)}", default=None)
740
+
741
+ @property
742
+ def request(self) -> Any | None:
743
+ """Current request object for this async context."""
744
+ return self._request_context.get()
745
+
746
+ @property
747
+ def user(self) -> Any | None:
748
+ """Current authenticated user object for this async context."""
749
+ return self._user_context.get()
750
+
751
+ def set_context(self, request: Any | None = None, user: Any | None = None) -> None:
752
+ """Set request/user context for the current async task."""
753
+ self._request_context.set(request)
754
+ self._user_context.set(user)
755
+
709
756
  async def get_data(
710
757
  self,
711
758
  min_x_field: str | None = None,
@@ -101,6 +101,8 @@ def get_admin_or_admin_inline_model(orm_model_cls: str) -> ModelAdmin | InlineMo
101
101
  async def generate_models_schema(
102
102
  admin_models: dict[Any, ModelAdmin | InlineModelAdmin],
103
103
  user_id: UUID | int | None = None,
104
+ user: Any | None = None,
105
+ request: Any | None = None,
104
106
  inline_parent_admin_modal: ModelAdmin | None = None,
105
107
  ) -> list[ModelSchema | InlineModelSchema]:
106
108
  """Generate models schema
@@ -111,6 +113,8 @@ async def generate_models_schema(
111
113
  """
112
114
  models_schemas: list[ModelSchema | InlineModelSchema] = []
113
115
  for orm_model_cls, admin_model_obj in admin_models.items():
116
+ if hasattr(admin_model_obj, "set_context"):
117
+ admin_model_obj.set_context(request=request, user=user)
114
118
  orm_model_fields = admin_model_obj.get_model_fields_with_widget_types()
115
119
  orm_model_fields_for_serialize = admin_model_obj.get_fields_for_serialize()
116
120
 
@@ -258,6 +262,8 @@ async def generate_models_schema(
258
262
  view_on_site=admin_model_obj.view_on_site,
259
263
  inlines=await generate_models_schema(
260
264
  {inline.model: inline(inline.model) for inline in admin_model_obj.inlines},
265
+ user=user,
266
+ request=request,
261
267
  inline_parent_admin_modal=admin_model_obj,
262
268
  user_id=user_id,
263
269
  ), # type: ignore [arg-type]
@@ -1,4 +1,3 @@
1
- import operator
2
1
  from base64 import b64decode
3
2
  from typing import Any
4
3
  from uuid import UUID
@@ -245,6 +244,8 @@ class DjangoORMMixin:
245
244
  search: str | None = None,
246
245
  sort_by: str | None = None,
247
246
  filters: dict | None = None,
247
+ prefetch_related_fields: list[str] | None = None,
248
+ additional_search_fields: list[str] | None = None,
248
249
  ) -> tuple[list[Any], int]:
249
250
  """This method is used to get list of orm/db model objects.
250
251
 
@@ -253,19 +254,30 @@ class DjangoORMMixin:
253
254
  :params search: a search query.
254
255
  :params sort_by: a sort by field name.
255
256
  :params filters: a dict of filters.
257
+ :params prefetch_related_fields: a list of related fields to prefetch.
258
+ :params additional_search_fields: a list of additional search fields.
256
259
  :return: A tuple of list of objects and total count.
257
260
  """
258
261
  qs = self.model_cls.objects.all()
259
262
 
263
+ if prefetch_related_fields:
264
+ qs = qs.prefetch_related(*prefetch_related_fields)
265
+
260
266
  if filters:
261
267
  for field_with_condition, value in filters.items():
262
268
  field = field_with_condition[0]
263
269
  condition = field_with_condition[1]
264
270
  qs = qs.filter(**{f"{field}__{condition}" if condition != "exact" else field: value})
265
271
 
266
- if search and self.search_fields:
267
- search_conditions = [Q(**{f + "__icontains": search}) for f in self.search_fields]
268
- search_q = search_conditions[0] if len(search_conditions) == 1 else operator.or_(*search_conditions)
272
+ search_fields = list(self.search_fields)
273
+ if additional_search_fields:
274
+ search_fields.extend(additional_search_fields)
275
+
276
+ if search and search_fields:
277
+ search_conditions = [Q(**{f + "__icontains": search}) for f in search_fields]
278
+ search_q = search_conditions[0]
279
+ for condition in search_conditions[1:]:
280
+ search_q |= condition
269
281
  qs = qs.filter(search_q)
270
282
 
271
283
  if sort_by:
@@ -224,6 +224,8 @@ class PonyORMMixin:
224
224
  search: str | None = None,
225
225
  sort_by: str | None = None,
226
226
  filters: dict | None = None,
227
+ prefetch_related_fields: list[str] | None = None,
228
+ additional_search_fields: list[str] | None = None,
227
229
  ) -> tuple[list[Any], int]:
228
230
  """This method is used to get list of orm/db model objects.
229
231
 
@@ -232,6 +234,8 @@ class PonyORMMixin:
232
234
  :params search: a search query.
233
235
  :params sort_by: a sort by field name.
234
236
  :params filters: a dict of filters.
237
+ :params prefetch_related_fields: a list of related fields to prefetch.
238
+ :params additional_search_fields: a list of additional search fields.
235
239
  :return: A tuple of list of objects and total count.
236
240
  """
237
241
 
@@ -271,9 +275,13 @@ class PonyORMMixin:
271
275
  filter_expr = f""""{value}" {pony_condition} m.{field}"""
272
276
  qs = qs.filter(filter_expr)
273
277
 
274
- if search and self.search_fields:
278
+ search_fields = list(self.search_fields)
279
+ if additional_search_fields:
280
+ search_fields.extend(additional_search_fields)
281
+
282
+ if search and search_fields:
275
283
  ids = []
276
- for search_field in self.search_fields:
284
+ for search_field in search_fields:
277
285
  pony_search_field = search_field.replace("__", ".")
278
286
  # Pony string filter for case-insensitive search
279
287
  filter_expr = f'"{search.lower()}" in m.{pony_search_field}.lower()'
@@ -296,6 +304,9 @@ class PonyORMMixin:
296
304
  if self.list_select_related:
297
305
  qs = qs.prefetch(*[getattr(self.model_cls, field) for field in self.list_select_related])
298
306
 
307
+ if prefetch_related_fields:
308
+ qs = qs.prefetch(*[getattr(self.model_cls, field) for field in prefetch_related_fields])
309
+
299
310
  if offset is not None and limit is not None:
300
311
  qs = qs.limit(limit, offset=offset)
301
312
 
@@ -300,6 +300,8 @@ class SqlAlchemyMixin:
300
300
  search: str | None = None,
301
301
  sort_by: str | None = None,
302
302
  filters: dict | None = None,
303
+ prefetch_related_fields: list[str] | None = None,
304
+ additional_search_fields: list[str] | None = None,
303
305
  ) -> tuple[list[Any], int]:
304
306
  """This method is used to get list of orm/db model objects.
305
307
 
@@ -308,6 +310,8 @@ class SqlAlchemyMixin:
308
310
  :params search: a search query.
309
311
  :params sort_by: a sort by field name.
310
312
  :params filters: a dict of filters.
313
+ :params prefetch_related_fields: a list of related fields to prefetch.
314
+ :params additional_search_fields: a list of additional search fields.
311
315
  :return: A tuple of list of objects and total count.
312
316
  """
313
317
 
@@ -354,9 +358,13 @@ class SqlAlchemyMixin:
354
358
  q.append(model_field.ilike(f"%{value}%"))
355
359
  qs = qs.where(and_(*q))
356
360
 
357
- if search and self.search_fields:
361
+ search_fields = list(self.search_fields)
362
+ if additional_search_fields:
363
+ search_fields.extend(additional_search_fields)
364
+
365
+ if search and search_fields:
358
366
  q = []
359
- for field in self.search_fields:
367
+ for field in search_fields:
360
368
  condition = self._build_search_condition(field, search)
361
369
  if condition is not None:
362
370
  q.append(condition)
@@ -377,6 +385,28 @@ class SqlAlchemyMixin:
377
385
  for field in self.list_select_related:
378
386
  qs = qs.options(selectinload(getattr(self.model_cls, field)))
379
387
 
388
+ if prefetch_related_fields:
389
+ for field_path in prefetch_related_fields:
390
+ parts = field_path.split("__")
391
+ current_model = self.model_cls
392
+ attr = getattr(current_model, parts[0], None)
393
+ if attr is None:
394
+ continue
395
+ option = selectinload(attr)
396
+ current_model = getattrs(attr, "property.mapper.class_")
397
+ for part in parts[1:]:
398
+ if current_model is None:
399
+ break
400
+ nested_attr = getattr(current_model, part, None)
401
+ if nested_attr is None:
402
+ break
403
+ next_model = getattrs(nested_attr, "property.mapper.class_")
404
+ if next_model is None:
405
+ break
406
+ option = option.selectinload(nested_attr)
407
+ current_model = next_model
408
+ qs = qs.options(option)
409
+
380
410
  if offset is not None and limit is not None:
381
411
  qs = qs.offset(offset)
382
412
  qs = qs.limit(limit)
@@ -254,6 +254,8 @@ class TortoiseMixin:
254
254
  search: str | None = None,
255
255
  sort_by: str | None = None,
256
256
  filters: dict | None = None,
257
+ prefetch_related_fields: list[str] | None = None,
258
+ additional_search_fields: list[str] | None = None,
257
259
  ) -> tuple[list[Any], int]:
258
260
  """This method is used to get list of orm/db model objects.
259
261
 
@@ -262,21 +264,30 @@ class TortoiseMixin:
262
264
  :params search: a search query.
263
265
  :params sort_by: a sort by field name.
264
266
  :params filters: a dict of filters.
267
+ :params prefetch_related_fields: a list of related fields to prefetch.
268
+ :params additional_search_fields: a list of additional search fields.
265
269
  :return: A tuple of list of objects and total count.
266
270
  """
267
271
  qs = self.model_cls.all()
268
272
 
273
+ if prefetch_related_fields:
274
+ qs = qs.prefetch_related(*prefetch_related_fields).distinct()
275
+
269
276
  if filters:
270
277
  for field_with_condition, value in filters.items():
271
278
  field = field_with_condition[0]
272
279
  condition = field_with_condition[1]
273
280
  qs = qs.filter(**{f"{field}__{condition}" if condition != "exact" else field: value})
274
281
 
275
- if search and self.search_fields:
282
+ search_fields = list(self.search_fields)
283
+ if additional_search_fields:
284
+ search_fields.extend(additional_search_fields)
285
+
286
+ if search and search_fields:
276
287
  qs = qs.filter(
277
288
  functools.reduce(
278
289
  operator.or_,
279
- (Q(**{f + "__icontains": search}) for f in self.search_fields),
290
+ (Q(**{f + "__icontains": search}) for f in search_fields),
280
291
  Q(),
281
292
  )
282
293
  )
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "fastadmin"
3
- version = "0.3.4"
3
+ version = "0.3.5"
4
4
  description = "FastAdmin is an easy-to-use Admin Dashboard App for FastAPI/Flask/Django inspired by Django Admin."
5
5
  authors = ["Seva D <vsdudakov@gmail.com>"]
6
6
  license = "MIT"
File without changes
File without changes