Skip to content

Commit 1e4e221

Browse files
authored
Add user and request context for admin class (#125)
* Add user and request context for admin class * Fix comments
1 parent 27478ad commit 1e4e221

13 files changed

Lines changed: 374 additions & 41 deletions

File tree

docs/build.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ def get_versions():
4444
return [
4545
{
4646
"version": "0.3.4",
47-
"changes": ["Fix sort by and search by relations. Fix examples."],
47+
"changes": [
48+
"Fix sort by and search by relations. Fix examples.",
49+
"Add request/user context on BaseModelAdmin for per-request custom logic.",
50+
],
4851
},
4952
{
5053
"version": "0.3.3",
@@ -589,6 +592,25 @@ def get_page_context(page_url):
589592
"content": "The following methods and attributes are available for model admins:",
590593
},
591594
{"type": "code-python", "content": inspect.getsource(BaseModelAdmin)},
595+
{
596+
"type": "alert-info",
597+
"content": "Use <code>self.request</code> and <code>self.user</code> in your admin methods (permissions, save hooks, actions) to access request-scoped context.",
598+
},
599+
{
600+
"type": "code-python",
601+
"content": """class EventAdmin(TortoiseModelAdmin):
602+
async def has_change_permission(self, user_id=None):
603+
# request/user are available for current request context
604+
if self.user and self.user.get("is_superuser"):
605+
return True
606+
return False
607+
608+
async def save_model(self, id, payload):
609+
if self.request:
610+
payload["changed_from_ip"] = getattr(self.request, "client", None)
611+
return await super().save_model(id, payload)
612+
""",
613+
},
592614
{
593615
"type": "text",
594616
"content": "Model-admin-specific methods and attributes:",

docs/index.html

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2058,6 +2058,9 @@ <h3>Methods and Attributes</h3>
20582058
class BaseModelAdmin:
20592059
"""Base class for model admin"""
20602060

2061+
_request_context: ContextVar[Any | None]
2062+
_user_context: ContextVar[Any | None]
2063+
20612064
# Use it only if you use several orms in your project.
20622065
model_name_prefix: str | None = None
20632066

@@ -2240,6 +2243,23 @@ <h3>Methods and Attributes</h3>
22402243
:params model_cls: an orm/db model class.
22412244
"""
22422245
self.model_cls = model_cls
2246+
self._request_context = ContextVar(f"fastadmin_admin_request_context_{id(self)}", default=None)
2247+
self._user_context = ContextVar(f"fastadmin_admin_user_context_{id(self)}", default=None)
2248+
2249+
@property
2250+
def request(self) -> Any | None:
2251+
"""Current request object for this async context."""
2252+
return self._request_context.get()
2253+
2254+
@property
2255+
def user(self) -> Any | None:
2256+
"""Current authenticated user object for this async context."""
2257+
return self._user_context.get()
2258+
2259+
def set_context(self, request: Any | None = None, user: Any | None = None) -> None:
2260+
"""Set request/user context for the current async task."""
2261+
self._request_context.set(request)
2262+
self._user_context.set(user)
22432263

22442264
@staticmethod
22452265
def get_model_pk_name(orm_model_cls: Any) -> str:
@@ -2664,6 +2684,55 @@ <h3>Methods and Attributes</h3>
26642684

26652685

26662686

2687+
2688+
2689+
2690+
2691+
<p class="alert alert-info">
2692+
Use <code>self.request</code> and <code>self.user</code> in your admin methods (permissions, save hooks, actions) to access request-scoped context.
2693+
</p>
2694+
2695+
2696+
2697+
2698+
2699+
2700+
2701+
2702+
2703+
2704+
2705+
2706+
2707+
2708+
2709+
2710+
2711+
2712+
2713+
2714+
2715+
<pre>
2716+
<code class="language-python">
2717+
class EventAdmin(TortoiseModelAdmin):
2718+
async def has_change_permission(self, user_id=None):
2719+
# request/user are available for current request context
2720+
if self.user and self.user.get("is_superuser"):
2721+
return True
2722+
return False
2723+
2724+
async def save_model(self, id, payload):
2725+
if self.request:
2726+
payload["changed_from_ip"] = getattr(self.request, "client", None)
2727+
return await super().save_model(id, payload)
2728+
2729+
</code>
2730+
</pre>
2731+
2732+
2733+
2734+
2735+
26672736
<p class="text-4">
26682737
Model-admin-specific methods and attributes:
26692738
</p>
@@ -3342,6 +3411,24 @@ <h3>v0.3.4</h3>
33423411

33433412

33443413

3414+
3415+
<p class="text-4">
3416+
Add request/user context on BaseModelAdmin for per-request custom logic.
3417+
</p>
3418+
3419+
3420+
3421+
3422+
3423+
3424+
3425+
3426+
3427+
3428+
3429+
3430+
3431+
33453432
</section>
33463433

33473434

fastadmin/api/frameworks/django/app/api.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ async def sign_in(request: HttpRequest) -> JsonResponse:
6464
session_id = await api_service.sign_in(
6565
request.COOKIES.get(settings.ADMIN_SESSION_ID_KEY, None),
6666
payload,
67+
request=request,
6768
)
6869

6970
response = JsonResponse({})
@@ -111,7 +112,10 @@ async def me(request: HttpRequest) -> JsonResponse:
111112
if not user_id:
112113
raise AdminApiException(401, "User is not authenticated.")
113114
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,
115119
)
116120
return JsonResponse(obj)
117121

@@ -143,6 +147,7 @@ async def dashboard_widget(request: HttpRequest, model: str) -> JsonResponse:
143147
min_x_field=min_x_field,
144148
max_x_field=max_x_field,
145149
period_x_field=period_x_field,
150+
request=request,
146151
)
147152
return JsonResponse(data)
148153
except AdminApiException as e:
@@ -182,6 +187,7 @@ async def list_objs(request: HttpRequest, model: str) -> JsonResponse:
182187
filters=list_filters,
183188
offset=offset,
184189
limit=limit,
190+
request=request,
185191
)
186192
return JsonResponse(
187193
{
@@ -212,6 +218,7 @@ async def get(request: HttpRequest, model: str, id: UUID | int | str) -> JsonRes
212218
request.COOKIES.get(settings.ADMIN_SESSION_ID_KEY, None),
213219
model,
214220
id,
221+
request=request,
215222
)
216223
return JsonResponse(obj)
217224

@@ -234,6 +241,7 @@ async def add(request: HttpRequest, model: str) -> JsonResponse:
234241
request.COOKIES.get(settings.ADMIN_SESSION_ID_KEY, None),
235242
model,
236243
json.loads(request.body),
244+
request=request,
237245
)
238246
return JsonResponse(obj)
239247
except AdminApiException as e:
@@ -257,6 +265,7 @@ async def change_password(request: HttpRequest, id: UUID | int | str) -> JsonRes
257265
request.COOKIES.get(settings.ADMIN_SESSION_ID_KEY, None),
258266
id,
259267
json.loads(request.body),
268+
request=request,
260269
)
261270
return JsonResponse(id, safe=False)
262271

@@ -283,6 +292,7 @@ async def change(request: HttpRequest, model: str, id: UUID | int | str) -> Json
283292
model,
284293
id,
285294
json.loads(request.body),
295+
request=request,
286296
)
287297
return JsonResponse(obj)
288298

@@ -319,6 +329,7 @@ async def export(request: HttpRequest, model: str) -> JsonResponse:
319329
search=search,
320330
sort_by=sort_by,
321331
filters=list_filters,
332+
request=request,
322333
)
323334
response = StreamingHttpResponse(stream, content_type=content_type)
324335
response.headers["Content-Disposition"] = f'attachment; filename="{file_name}"'
@@ -349,6 +360,7 @@ async def delete(
349360
request.COOKIES.get(settings.ADMIN_SESSION_ID_KEY, None),
350361
model,
351362
id,
363+
request=request,
352364
)
353365
return JsonResponse(deleted_id, safe=False)
354366

@@ -378,6 +390,7 @@ async def action(
378390
model,
379391
action,
380392
payload,
393+
request=request,
381394
)
382395
return JsonResponse({})
383396

@@ -397,5 +410,6 @@ async def configuration(request: HttpRequest) -> JsonResponse:
397410

398411
obj = await api_service.get_configuration(
399412
request.COOKIES.get(settings.ADMIN_SESSION_ID_KEY, None),
413+
request=request,
400414
)
401415
return JsonResponse(asdict(obj))

fastadmin/api/frameworks/fastapi/api.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ async def sign_in(
3535
session_id = await api_service.sign_in(
3636
request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
3737
payload,
38+
request=request,
3839
)
3940

4041
response.set_cookie(settings.ADMIN_SESSION_ID_KEY, value=session_id, httponly=True)
@@ -78,7 +79,10 @@ async def me(
7879
if not user_id:
7980
raise AdminApiException(401, "User is not authenticated.")
8081
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,
8286
)
8387
except AdminApiException as e:
8488
raise HTTPException(e.status_code, detail=e.detail) from None
@@ -107,6 +111,7 @@ async def dashboard_widget(
107111
min_x_field=min_x_field,
108112
max_x_field=max_x_field,
109113
period_x_field=period_x_field,
114+
request=request,
110115
)
111116
return data
112117
except AdminApiException as e:
@@ -146,6 +151,7 @@ async def list_objs(
146151
filters=list_filters,
147152
offset=offset,
148153
limit=limit,
154+
request=request,
149155
)
150156
return {
151157
"total": total,
@@ -172,6 +178,7 @@ async def get(
172178
request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
173179
model,
174180
id,
181+
request=request,
175182
)
176183
except AdminApiException as e:
177184
raise HTTPException(e.status_code, detail=e.detail) from None
@@ -194,6 +201,7 @@ async def add(
194201
request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
195202
model,
196203
payload,
204+
request=request,
197205
)
198206
except AdminApiException as e:
199207
raise HTTPException(e.status_code, detail=e.detail) from None
@@ -212,7 +220,12 @@ async def change_password(
212220
:return: An object.
213221
"""
214222
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+
)
216229
return id
217230
except AdminApiException as e:
218231
raise HTTPException(e.status_code, detail=e.detail) from None
@@ -233,7 +246,13 @@ async def change(
233246
:return: An object.
234247
"""
235248
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+
)
237256
except AdminApiException as e:
238257
raise HTTPException(e.status_code, detail=e.detail) from None
239258

@@ -268,6 +287,7 @@ async def export(
268287
search=search,
269288
sort_by=sort_by,
270289
filters=list_filters,
290+
request=request,
271291
)
272292
headers = {"Content-Disposition": f'attachment; filename="{file_name}"'}
273293
return StreamingResponse(
@@ -296,6 +316,7 @@ async def delete(
296316
request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
297317
model,
298318
id,
319+
request=request,
299320
)
300321
except AdminApiException as e:
301322
raise HTTPException(e.status_code, detail=e.detail) from None
@@ -321,6 +342,7 @@ async def action(
321342
model,
322343
action,
323344
payload,
345+
request=request,
324346
)
325347
except AdminApiException as e:
326348
raise HTTPException(e.status_code, detail=e.detail) from None
@@ -337,4 +359,5 @@ async def configuration(
337359
"""
338360
return await api_service.get_configuration(
339361
request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
362+
request=request,
340363
)

0 commit comments

Comments
 (0)