Skip to content

Commit b141302

Browse files
committed
Added 100% coverage for backend, fixed bugs
1 parent c1d6532 commit b141302

18 files changed

Lines changed: 545 additions & 50 deletions

fastadmin/api/api.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22
from datetime import datetime, timedelta
3-
from typing import Any, List
3+
from typing import Any
44
from uuid import UUID
55

66
import jwt
@@ -94,11 +94,11 @@ async def list(
9494
request: Request,
9595
model: str,
9696
search: str | None = None,
97-
sort_by: str = "-created_at",
97+
sort_by: str = None,
9898
offset: int | None = 0,
9999
limit: int | None = 10,
100100
_: UUID | int = Depends(get_user_id),
101-
) -> dict[str, int | List[Any]]:
101+
):
102102
"""This method is used to get a list of objects.
103103
104104
:params request: a request object.
@@ -199,7 +199,7 @@ async def export(
199199
model: str,
200200
payload: ExportSchema,
201201
search: str | None = None,
202-
sort_by: str = "-created_at",
202+
sort_by: str = None,
203203
_: UUID | int = Depends(get_user_id),
204204
):
205205
"""This method is used to export a list of objects.
@@ -285,12 +285,13 @@ async def configuration(
285285

286286
fields_schema = []
287287
for field_name, field in model_fields.items():
288+
is_m2m = model_fields.get(field_name, {}).get("is_m2m")
288289
list_display = admin_obj.get_list_display()
289290
column_index = list_display.index(field_name) if field_name in list_display else None
290291
list_configuration = None
291292
filter_widget_type = None
292293
filter_widget_props = None
293-
if column_index is not None:
294+
if column_index is not None and not is_m2m:
294295
if field_name in admin_obj.list_filter:
295296
filter_widget_type, filter_widget_props = admin_obj.get_filter_widget(field_name)
296297
sorter = True

fastadmin/models/base.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
class BaseModelAdmin:
1414
"""Base class for model admin"""
1515

16+
# Field name for representation of model instance (labels in selects)
17+
repr_field = "id"
18+
1619
# Not supported setting
1720
# actions
1821

@@ -382,12 +385,12 @@ def get_fields(self) -> Sequence[str]:
382385
return [f for f in model_fields if not self.exclude or f not in self.exclude]
383386
return [f for f in fields if f in model_fields]
384387

385-
def get_fieldsets(self) -> Sequence[tuple[str | None, dict[str, Sequence[str]]]]:
386-
"""This method is used to get fieldsets data for form view.
388+
# def get_fieldsets(self) -> Sequence[tuple[str | None, dict[str, Sequence[str]]]]:
389+
# """This method is used to get fieldsets data for form view.
387390

388-
:return: A list of fieldsets data.
389-
"""
390-
return self.fieldsets
391+
# :return: A list of fieldsets data.
392+
# """
393+
# return self.fieldsets
391394

392395
def has_add_permission(self) -> bool:
393396
"""This method is used to check if user has permission to add new model instance.

fastadmin/models/helpers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def get_admin_model(model_name: str) -> BaseModelAdmin | None:
4848
"""
4949
from fastadmin.app import admin_models
5050

51-
for model, admin_model in admin_models.items():
51+
for model, admin_model_class in admin_models.items():
5252
if model.__name__ == model_name:
53-
return admin_model(model)
53+
return admin_model_class(model)
5454
return None

fastadmin/models/orm/tortoise.py

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@ async def obj_to_dict(obj: Any, with_m2m: bool = True) -> dict:
2020
obj_dict = {k: v for k, v in obj.__dict__.items() if not k.startswith("_")}
2121
if with_m2m:
2222
for field in obj.__class__._meta.m2m_fields:
23-
m2m_rel = getattr(obj, field, None)
24-
if m2m_rel is None:
25-
continue
23+
m2m_rel = getattr(obj, field)
2624
remote_model = m2m_rel.remote_model
2725
remote_ids = await m2m_rel.all().values_list(remote_model._meta.pk_attr, flat=True)
2826
obj_dict[field] = remote_ids
@@ -57,9 +55,7 @@ async def save_model(self, id: UUID | int | None, payload: dict) -> dict | None:
5755

5856
for key, values in payload.items():
5957
if key in m2m_fields:
60-
m2m_rel = getattr(obj, key, None)
61-
if m2m_rel is None:
62-
continue
58+
m2m_rel = getattr(obj, key)
6359
remote_model = m2m_rel.remote_model
6460
await m2m_rel.clear()
6561
remote_model_objs = []
@@ -117,15 +113,17 @@ async def get_list(
117113
field = filter_condition.split("__", 1)[0]
118114
if field not in fields:
119115
raise HTTPException(
120-
status.HTTP_400_BAD_REQUEST, detail=f"Filter by {filter_condition} is not allowed"
116+
status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Filter by {filter_condition} is not allowed"
121117
)
122118
qs = qs.filter(**{filter_condition: value})
123119

124120
if search:
125121
if self.search_fields:
126122
for field in self.search_fields:
127123
if field not in fields:
128-
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=f"Search by {field} is not allowed")
124+
raise HTTPException(
125+
status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Search by {field} is not allowed"
126+
)
129127
ids = await asyncio.gather(
130128
*(qs.filter(**{f + "__icontains": search}).values_list("id", flat=True) for f in self.search_fields)
131129
)
@@ -134,12 +132,15 @@ async def get_list(
134132

135133
if sort_by:
136134
if sort_by.strip("-") not in fields:
137-
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=f"Sort by {sort_by} is not allowed")
135+
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Sort by {sort_by} is not allowed")
138136
qs = qs.order_by(sort_by)
139137
else:
140138
if self.ordering:
141-
if self.ordering.strip("-") not in fields:
142-
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=f"Sort by {self.ordering} is not allowed")
139+
for sort_by in self.ordering:
140+
if sort_by.strip("-") not in fields:
141+
raise HTTPException(
142+
status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Sort by {sort_by} is not allowed"
143+
)
143144
qs = qs.order_by(*self.ordering)
144145

145146
total = await qs.count()
@@ -151,8 +152,10 @@ async def get_list(
151152
if self.list_select_related:
152153
for field in self.list_select_related:
153154
if field not in fields:
154-
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=f"Select related by {field} is not allowed")
155-
qs = qs.select_related(*self.list_select_related)
155+
raise HTTPException(
156+
status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Select related by {field} is not allowed"
157+
)
158+
qs = qs.select_related(*(f.replace("_id", "") for f in self.list_select_related))
156159

157160
results = await asyncio.gather(*(obj_to_dict(obj, with_m2m=False) for obj in await qs))
158161

@@ -186,14 +189,7 @@ def get_model_fields(self) -> OrderedDict[str, dict]:
186189
parent_model_label = "id"
187190
if parent_admin_model:
188191
parent_model_id = parent_admin_model.model_cls._meta.pk_attr
189-
parent_model_label = parent_admin_model.model_cls._meta.pk_attr
190-
parent_fields = parent_admin_model.model_cls._meta.fields_db_projection.keys()
191-
if "name" in parent_fields:
192-
parent_model_label = "name"
193-
elif "title" in parent_fields:
194-
parent_model_label = "title"
195-
elif "email" in parent_fields:
196-
parent_model_label = "email"
192+
parent_model_label = parent_admin_model.repr_field or parent_model_id
197193

198194
form_hidden = (
199195
getattr(field, "_generated", False)
@@ -231,7 +227,7 @@ def get_form_widget(self, field_name: str) -> tuple[WidgetType, dict]:
231227
fields = self.get_model_fields()
232228
field = fields.get(field_name)
233229
if not field:
234-
raise Exception("Invalid field name %s" % field_name)
230+
raise ValueError("Invalid field name %s" % field_name)
235231
widget_props = {
236232
"required": field.get("required") or False,
237233
"disabled": field_name in self.readonly_fields,

fastadmin/static/js/main.min.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fastadmin/static/js/main.min.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "fastadmin"
3-
version = "0.1.12"
3+
version = "0.1.13"
44
description = ""
55
authors = ["Seva D <vsdudakov@gmail.com>"]
66
license = "MIT"

tests/api/api/test_add.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ async def test_add_404(objects, client):
5151
tournament = objects["tournament"]
5252
event = objects["event"]
5353
admin_user_cls = objects["admin_user_cls"]
54+
unregister_admin_model([event.__class__])
5455
await sign_in(client, superuser, admin_user_cls)
5556
r = await client.post(
5657
f"/api/add/{event.__class__.__name__}",

tests/api/api/test_change.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ async def test_change_404_admin_class_found(objects, client):
4848
superuser = objects["superuser"]
4949
event = objects["event"]
5050
admin_user_cls = objects["admin_user_cls"]
51+
unregister_admin_model([event.__class__])
5152
await sign_in(client, superuser, admin_user_cls)
5253
r = await client.patch(
5354
f"/api/change/{event.__class__.__name__}/{event.id}",

0 commit comments

Comments
 (0)