Skip to content

Commit abeae1d

Browse files
committed
Add response types for actions. Fix Decimal fields handling.
1 parent ca00d57 commit abeae1d

20 files changed

Lines changed: 470 additions & 216 deletions

File tree

docs/build.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ def read_cls_docstring(cls):
4242

4343
def get_versions():
4444
return [
45+
{
46+
"version": "0.3.9",
47+
"changes": [
48+
"Add response types for actions.",
49+
"Fix Decimal fields handling.",
50+
],
51+
},
4552
{
4653
"version": "0.3.8",
4754
"changes": [

docs/index.html

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,10 @@ <h4 class="title">FastAdmin</h4>
196196

197197
<ul class="nav flex-column">
198198

199+
<li class="nav-item">
200+
<a class="nav-link" href="#v0_3_9">v0.3.9</a>
201+
</li>
202+
199203
<li class="nav-item">
200204
<a class="nav-link" href="#v0_3_8">v0.3.8</a>
201205
</li>
@@ -339,7 +343,7 @@ <h1>FastAdmin | Documentation</h1>
339343
<div class="row">
340344
<div class="col-sm-6 col-lg-4">
341345
<ul class="list-unstyled">
342-
<li><strong>Version:</strong> 0.3.8</li>
346+
<li><strong>Version:</strong> 0.3.9</li>
343347
<li>
344348
<strong>Author:</strong>
345349
<a href="mailto:vsdudakov@gmail.com" target="_blank">
@@ -2110,7 +2114,7 @@ <h3>Methods and Attributes</h3>
21102114
# @action(
21112115
# description="Mark selected stories as published",
21122116
# )
2113-
# async def make_published(self, objs: list[Any]) -> None:
2117+
# async def make_published(self, objs: list[Any]) -> ActionResponseSchema | None:
21142118
# ...
21152119
actions: Sequence[str] = ()
21162120

@@ -2463,7 +2467,14 @@ <h3>Methods and Attributes</h3>
24632467
:params attributes_to_serizalize: a list of attributes to serialize.
24642468
:return: A dict of serialized attributes.
24652469
"""
2466-
serialized_dict = {field.name: getattr(obj, field.column_name) for field in attributes_to_serizalize}
2470+
serialized_dict: dict[str, Any] = {}
2471+
for field in attributes_to_serizalize:
2472+
value = getattr(obj, field.column_name)
2473+
if isinstance(value, Decimal):
2474+
# Avoid scientific notation for Decimal values in API responses,
2475+
# e.g. 3.75E+3 -> "3750"
2476+
value = format(value, "f")
2477+
serialized_dict[field.name] = value
24672478
if inspect.iscoroutinefunction(obj.__str__):
24682479
str_fn = obj.__str__
24692480
else:
@@ -3365,6 +3376,49 @@ <h2>Changelog</h2>
33653376

33663377

33673378

3379+
<section id="v0_3_9">
3380+
<h3>v0.3.9</h3>
3381+
3382+
3383+
3384+
<p class="text-4">
3385+
Add response types for actions.
3386+
</p>
3387+
3388+
3389+
3390+
3391+
3392+
3393+
3394+
3395+
3396+
3397+
3398+
3399+
3400+
3401+
3402+
<p class="text-4">
3403+
Fix Decimal fields handling.
3404+
</p>
3405+
3406+
3407+
3408+
3409+
3410+
3411+
3412+
3413+
3414+
3415+
3416+
3417+
3418+
3419+
</section>
3420+
3421+
33683422
<section id="v0_3_8">
33693423
<h3>v0.3.8</h3>
33703424

fastadmin/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
except ModuleNotFoundError: # pragma: no cover
3333
logging.info("TortoiseORM is not installed") # pragma: no cover
3434

35+
# api
36+
from fastadmin.api.exceptions import AdminApiException # noqa: F401
37+
from fastadmin.api.schemas import ActionResponseSchema, ActionResponseType # noqa: F401
38+
3539
# models
3640
from fastadmin.models.base import DashboardWidgetAdmin, InlineModelAdmin, ModelAdmin # noqa: F401
3741
from fastadmin.models.decorators import action, display, register, register_widget # noqa: F401

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

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@
1313

1414
from fastadmin.api.exceptions import AdminApiException
1515
from fastadmin.api.helpers import is_valid_id, parse_list_filters_from_query_params
16-
from fastadmin.api.schemas import ActionInputSchema, ExportInputSchema, SignInInputSchema
16+
from fastadmin.api.schemas import (
17+
ActionInputSchema,
18+
ActionResponseSchema,
19+
ActionResponseType,
20+
ExportInputSchema,
21+
SignInInputSchema,
22+
)
1723
from fastadmin.api.service import ApiService, get_user_id_from_session_id
1824
from fastadmin.settings import settings
1925

@@ -379,21 +385,25 @@ async def action(
379385
:params model: a name of model.
380386
:params action: a name of action.
381387
:params payload: a payload object.
382-
:return: A list of objects.
388+
:return: action result.
383389
"""
384390
if request.method != "POST":
385391
return JsonResponse({"error": "Method not allowed"}, status=405)
386392
try:
387393
payload = ActionInputSchema(**json.loads(request.body))
388-
await api_service.action(
394+
response = await api_service.action(
389395
request.COOKIES.get(settings.ADMIN_SESSION_ID_KEY, None),
390396
model,
391397
action,
392398
payload,
393399
request=request,
394400
)
395-
return JsonResponse({})
396-
401+
if not response:
402+
response = ActionResponseSchema(
403+
type=ActionResponseType.MESSAGE,
404+
data="Successfully applied",
405+
)
406+
return JsonResponse(asdict(response))
397407
except AdminApiException as e:
398408
return JsonResponse({"detail": e.detail}, status=e.status_code)
399409

fastadmin/api/frameworks/fastapi/api.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
from dataclasses import asdict
23
from typing import Any
34
from uuid import UUID
45

@@ -7,7 +8,13 @@
78

89
from fastadmin.api.exceptions import AdminApiException
910
from fastadmin.api.helpers import parse_list_filters_from_query_params
10-
from fastadmin.api.schemas import ActionInputSchema, ExportInputSchema, SignInInputSchema
11+
from fastadmin.api.schemas import (
12+
ActionInputSchema,
13+
ActionResponseSchema,
14+
ActionResponseType,
15+
ExportInputSchema,
16+
SignInInputSchema,
17+
)
1118
from fastadmin.api.service import ApiService, get_user_id_from_session_id
1219
from fastadmin.models.schemas import ConfigurationSchema
1320
from fastadmin.settings import settings
@@ -328,22 +335,28 @@ async def action(
328335
model: str,
329336
action: str,
330337
payload: ActionInputSchema,
331-
) -> None:
338+
) -> dict:
332339
"""This method is used to perform an action.
333340
334341
:params model: a name of model.
335342
:params action: a name of action.
336343
:params payload: a payload object.
337-
:return: A list of objects.
344+
:return: action result.
338345
"""
339346
try:
340-
return await api_service.action(
347+
response = await api_service.action(
341348
request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
342349
model,
343350
action,
344351
payload,
345352
request=request,
346353
)
354+
if not response:
355+
response = ActionResponseSchema(
356+
type=ActionResponseType.MESSAGE,
357+
data="Successfully applied",
358+
)
359+
return asdict(response)
347360
except AdminApiException as e:
348361
raise HTTPException(e.status_code, detail=e.detail) from None
349362

fastadmin/api/frameworks/flask/api.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@
77

88
from fastadmin.api.exceptions import AdminApiException
99
from fastadmin.api.helpers import is_valid_id, parse_list_filters_from_query_params
10-
from fastadmin.api.schemas import ActionInputSchema, ExportInputSchema, SignInInputSchema
10+
from fastadmin.api.schemas import (
11+
ActionInputSchema,
12+
ActionResponseSchema,
13+
ActionResponseType,
14+
ExportInputSchema,
15+
SignInInputSchema,
16+
)
1117
from fastadmin.api.service import ApiService, get_user_id_from_session_id
1218
from fastadmin.settings import settings
1319

@@ -336,25 +342,30 @@ async def delete(
336342
async def action(
337343
model: str,
338344
action: str,
339-
) -> dict:
345+
) -> Response:
340346
"""This method is used to perform an action.
341347
342348
:params model: a name of model.
343349
:params action: a name of action.
344350
:params payload: a payload object.
345-
:return: A list of objects.
351+
:return: action result.
346352
"""
347353
try:
348354
request_payload: dict = request.json
349355
payload: ActionInputSchema = ActionInputSchema(**request_payload)
350-
await api_service.action(
356+
response = await api_service.action(
351357
request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
352358
model,
353359
action,
354360
payload,
355361
request=request,
356362
)
357-
return {}
363+
if not response:
364+
response = ActionResponseSchema(
365+
type=ActionResponseType.MESSAGE,
366+
data="Successfully applied",
367+
)
368+
return make_response(asdict(response))
358369
except AdminApiException as e:
359370
http_exception = HTTPException(e.detail)
360371
http_exception.code = e.status_code

fastadmin/api/schemas.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ class ExportFormat(str, Enum):
1010
JSON = "JSON"
1111

1212

13+
class ActionResponseType(str, Enum):
14+
"""Action response type"""
15+
16+
DOWNLOAD_BASE64 = "DOWNLOAD_BASE64"
17+
MESSAGE = "MESSAGE"
18+
19+
1320
@dataclass
1421
class DashboardWidgetQuerySchema:
1522
"""DashboardWidge query schema"""
@@ -70,3 +77,12 @@ class ActionInputSchema:
7077
"""Action input schema"""
7178

7279
ids: list[int | UUID]
80+
81+
82+
@dataclass
83+
class ActionResponseSchema:
84+
"""Action response schema"""
85+
86+
type: ActionResponseType
87+
data: str
88+
file_name: str | None = None

fastadmin/api/service.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from fastadmin.api.helpers import sanitize_filter_key, sanitize_filter_value
1515
from fastadmin.api.schemas import (
1616
ActionInputSchema,
17+
ActionResponseSchema,
1718
ChangePasswordInputSchema,
1819
DashboardWidgetDataOutputSchema,
1920
DashboardWidgetQuerySchema,
@@ -478,7 +479,7 @@ async def action(
478479
action: str,
479480
payload: ActionInputSchema,
480481
request: Any | None = None,
481-
) -> None:
482+
) -> ActionResponseSchema | None:
482483
_current_user_id, current_user = await self._get_authenticated_user(session_id)
483484

484485
admin_model = get_admin_or_admin_inline_model(model)
@@ -498,7 +499,7 @@ async def action(
498499
else:
499500
action_function_fn = sync_to_async(action_function)
500501

501-
await action_function_fn(payload.ids)
502+
return await action_function_fn(payload.ids)
502503

503504
async def get_configuration(
504505
self,

fastadmin/models/base.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import json
55
from collections.abc import Sequence
66
from contextvars import ContextVar
7+
from decimal import Decimal
78
from io import BytesIO, StringIO
89
from typing import Any
910
from uuid import UUID
@@ -31,7 +32,7 @@ class BaseModelAdmin:
3132
# @action(
3233
# description="Mark selected stories as published",
3334
# )
34-
# async def make_published(self, objs: list[Any]) -> None:
35+
# async def make_published(self, objs: list[Any]) -> ActionResponseSchema | None:
3536
# ...
3637
actions: Sequence[str] = ()
3738

@@ -384,7 +385,14 @@ async def serialize_obj_attributes(
384385
:params attributes_to_serizalize: a list of attributes to serialize.
385386
:return: A dict of serialized attributes.
386387
"""
387-
serialized_dict = {field.name: getattr(obj, field.column_name) for field in attributes_to_serizalize}
388+
serialized_dict: dict[str, Any] = {}
389+
for field in attributes_to_serizalize:
390+
value = getattr(obj, field.column_name)
391+
if isinstance(value, Decimal):
392+
# Avoid scientific notation for Decimal values in API responses,
393+
# e.g. 3.75E+3 -> "3750"
394+
value = format(value, "f")
395+
serialized_dict[field.name] = value
388396
if inspect.iscoroutinefunction(obj.__str__):
389397
str_fn = obj.__str__
390398
else:

fastadmin/models/decorators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ def action(function=None, *, description: str | None = None):
55
@action(
66
description="Mark selected stories as published",
77
)
8-
async def make_published(self, objs: list[Any]) -> None:
8+
async def make_published(self, objs: list[Any]) -> ActionResponseSchema | None:
99
...
1010
1111
:param function: A function to decorate.

0 commit comments

Comments
 (0)