fastadmin 0.3.4__tar.gz → 0.3.6__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.6}/PKG-INFO +1 -1
  2. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/api/frameworks/django/app/api.py +15 -1
  3. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/api/frameworks/fastapi/api.py +26 -3
  4. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/api/frameworks/flask/api.py +15 -1
  5. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/api/service.py +67 -34
  6. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/models/base.py +43 -0
  7. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/models/helpers.py +6 -0
  8. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/models/orms/django.py +6 -4
  9. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/models/orms/ponyorm.py +3 -2
  10. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/models/orms/sqlalchemy.py +3 -3
  11. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/models/orms/tortoise.py +3 -2
  12. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/static/index.min.js +164 -164
  13. {fastadmin-0.3.4 → fastadmin-0.3.6}/pyproject.toml +1 -1
  14. {fastadmin-0.3.4 → fastadmin-0.3.6}/LICENSE +0 -0
  15. {fastadmin-0.3.4 → fastadmin-0.3.6}/README.md +0 -0
  16. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/__init__.py +0 -0
  17. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/api/__init__.py +0 -0
  18. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/api/exceptions.py +0 -0
  19. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/api/frameworks/__init__.py +0 -0
  20. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/api/frameworks/django/__init__.py +0 -0
  21. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/api/frameworks/django/app/__init__.py +0 -0
  22. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/api/frameworks/django/app/urls.py +0 -0
  23. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/api/frameworks/django/app/views.py +0 -0
  24. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/api/frameworks/fastapi/__init__.py +0 -0
  25. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/api/frameworks/fastapi/app.py +0 -0
  26. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/api/frameworks/fastapi/views.py +0 -0
  27. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/api/frameworks/flask/__init__.py +0 -0
  28. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/api/frameworks/flask/app.py +0 -0
  29. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/api/frameworks/flask/views.py +0 -0
  30. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/api/helpers.py +0 -0
  31. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/api/schemas.py +0 -0
  32. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/models/__init__.py +0 -0
  33. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/models/decorators.py +0 -0
  34. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/models/orms/__init__.py +0 -0
  35. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/models/schemas.py +0 -0
  36. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/settings.py +0 -0
  37. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/static/assets/worker-6Z7niv9l.js +0 -0
  38. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/static/assets/worker-C151k0-L.js +0 -0
  39. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/static/assets/worker-C9KPOOKD.js +0 -0
  40. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/static/assets/worker-D1fCgYB7.js +0 -0
  41. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/static/assets/worker-D3TWcJOI.js +0 -0
  42. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/static/assets/worker-DRNcaZ-V.js +0 -0
  43. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/static/assets/worker-MF2p-l5_.js +0 -0
  44. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/static/images/favicon.png +0 -0
  45. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/static/images/header-logo.svg +0 -0
  46. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/static/images/sign-in-logo.svg +0 -0
  47. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/static/index.html +0 -0
  48. {fastadmin-0.3.4 → fastadmin-0.3.6}/fastadmin/static/index.min.css +0 -0
  49. {fastadmin-0.3.4 → fastadmin-0.3.6}/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.6
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:
@@ -697,6 +718,9 @@ class ModelAdmin(BaseModelAdmin):
697
718
 
698
719
 
699
720
  class DashboardWidgetAdmin:
721
+ _request_context: ContextVar[Any | None]
722
+ _user_context: ContextVar[Any | None]
723
+
700
724
  title: str
701
725
  dashboard_widget_type: DashboardWidgetType
702
726
  x_field: str
@@ -706,6 +730,25 @@ class DashboardWidgetAdmin:
706
730
  x_field_filter_widget_props: dict[str, Any] | None = None
707
731
  x_field_periods: list[str] | None = None
708
732
 
733
+ def __init__(self) -> None:
734
+ self._request_context = ContextVar(f"fastadmin_dashboard_request_context_{id(self)}", default=None)
735
+ self._user_context = ContextVar(f"fastadmin_dashboard_user_context_{id(self)}", default=None)
736
+
737
+ @property
738
+ def request(self) -> Any | None:
739
+ """Current request object for this async context."""
740
+ return self._request_context.get()
741
+
742
+ @property
743
+ def user(self) -> Any | None:
744
+ """Current authenticated user object for this async context."""
745
+ return self._user_context.get()
746
+
747
+ def set_context(self, request: Any | None = None, user: Any | None = None) -> None:
748
+ """Set request/user context for the current async task."""
749
+ self._request_context.set(request)
750
+ self._user_context.set(user)
751
+
709
752
  async def get_data(
710
753
  self,
711
754
  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
@@ -263,9 +262,12 @@ class DjangoORMMixin:
263
262
  condition = field_with_condition[1]
264
263
  qs = qs.filter(**{f"{field}__{condition}" if condition != "exact" else field: value})
265
264
 
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)
265
+ search_fields = list(self.search_fields)
266
+ if search and search_fields:
267
+ search_conditions = [Q(**{f + "__icontains": search}) for f in search_fields]
268
+ search_q = search_conditions[0]
269
+ for condition in search_conditions[1:]:
270
+ search_q |= condition
269
271
  qs = qs.filter(search_q)
270
272
 
271
273
  if sort_by: