Skip to content

Commit 7428a33

Browse files
committed
Fixed filters
1 parent 4fd63a8 commit 7428a33

10 files changed

Lines changed: 135 additions & 41 deletions

File tree

fastadmin/api/frameworks/flask/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ async def add(model: str) -> dict:
163163
raise http_exception
164164

165165

166-
@api_router.route("/change-password/<string:id>", methods=["PATCH"])
166+
@api_router.route("/change-password/<string:id>", methods=["PATCH"]) # type: ignore
167167
async def change_password(id: UUID | int) -> UUID | int:
168168
"""This method is used to change password.
169169

fastadmin/api/helpers.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
import jwt
55

66
from fastadmin.models.helpers import get_admin_model
7+
from fastadmin.models.schemas import ModelFieldWidgetSchema
78
from fastadmin.settings import settings
89

910

10-
def sanitize(value: str) -> bool | None | str:
11+
def sanitize_filter_value(value: str) -> bool | None | str:
1112
"""Sanitize value
1213
1314
:params value: a value.
@@ -22,6 +23,23 @@ def sanitize(value: str) -> bool | None | str:
2223
return value
2324

2425

26+
def sanitize_filter_key(key: str, fields: list[ModelFieldWidgetSchema]) -> tuple[str, str]:
27+
"""Sanitize key.
28+
29+
:param key: A key.
30+
:param fields: A list of fields.
31+
:return: A sanitized key.
32+
"""
33+
if "__" not in key:
34+
key = f"{key}__exact"
35+
field_name = key.split("__", 1)[0]
36+
condition = key.split("__", 1)[1]
37+
field: ModelFieldWidgetSchema | None = next((field for field in fields if field.name == field_name), None)
38+
if field and field.filter_widget_props.get("parentModel") and not field.is_m2m:
39+
field_name = f"{field_name}_id"
40+
return field_name, condition
41+
42+
2543
def is_valid_uuid(uuid_to_test: str) -> bool:
2644
"""Check if uuid_to_test is a valid uuid.
2745

fastadmin/api/service.py

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from asgiref.sync import sync_to_async
1010

1111
from fastadmin.api.exceptions import AdminApiException
12-
from fastadmin.api.helpers import get_user_id_from_session_id, sanitize
12+
from fastadmin.api.helpers import get_user_id_from_session_id, sanitize_filter_key, sanitize_filter_value
1313
from fastadmin.api.schemas import (
1414
ActionInputSchema,
1515
ChangePasswordInputSchema,
@@ -88,8 +88,6 @@ async def list(
8888
if not admin_model:
8989
raise AdminApiException(404, detail=f"{model} model is not registered.")
9090

91-
filters = {k: sanitize(v) for k, v in filters.items() if k not in ("search", "sort_by", "offset", "limit")}
92-
9391
# validations
9492
fields = admin_model.get_fields_for_serialize()
9593

@@ -98,11 +96,19 @@ async def list(
9896
if field not in fields:
9997
raise AdminApiException(422, detail=f"Search by {field} is not allowed")
10098

99+
exclude_filter_fields = ("search", "sort_by", "offset", "limit")
101100
if filters:
102-
for filter_condition in filters.keys():
103-
field = filter_condition.split("__", 1)[0]
101+
for k in filters.keys():
102+
if k in exclude_filter_fields:
103+
continue
104+
field = k.split("__", 1)[0]
104105
if field not in fields:
105-
raise AdminApiException(422, detail=f"Filter by {filter_condition} is not allowed")
106+
raise AdminApiException(422, detail=f"Filter by {k} is not allowed")
107+
filters = {
108+
sanitize_filter_key(k, admin_model.get_model_fields_with_widget_types()): sanitize_filter_value(v)
109+
for k, v in filters.items()
110+
if k not in exclude_filter_fields
111+
}
106112

107113
if sort_by:
108114
if sort_by.strip("-") not in fields:
@@ -169,7 +175,7 @@ async def change_password(
169175
if not current_user_id:
170176
raise AdminApiException(401, detail="User is not authenticated.")
171177

172-
admin_model = get_admin_or_admin_inline_model(settings.ADMIN_USER_MODEL)
178+
admin_model = get_admin_model(settings.ADMIN_USER_MODEL)
173179
if not admin_model:
174180
raise AdminApiException(404, detail=f"{settings.ADMIN_USER_MODEL} model is not registered.")
175181

@@ -223,7 +229,35 @@ async def export(
223229
if not admin_model:
224230
raise AdminApiException(404, detail=f"{model} model is not registered.")
225231

226-
filters = {k: sanitize(v) for k, v in filters.items() if k not in ("search", "sort_by", "offset", "limit")}
232+
# validations
233+
fields = admin_model.get_fields_for_serialize()
234+
235+
if search and admin_model.search_fields:
236+
for field in admin_model.search_fields:
237+
if field not in fields:
238+
raise AdminApiException(422, detail=f"Search by {field} is not allowed")
239+
240+
exclude_filter_fields = ("search", "sort_by", "offset", "limit")
241+
if filters:
242+
for k in filters.keys():
243+
if k in exclude_filter_fields:
244+
continue
245+
field = k.split("__", 1)[0]
246+
if field not in fields:
247+
raise AdminApiException(422, detail=f"Filter by {k} is not allowed")
248+
filters = {
249+
sanitize_filter_key(k, admin_model.get_model_fields_with_widget_types()): sanitize_filter_value(v)
250+
for k, v in filters.items()
251+
if k not in exclude_filter_fields
252+
}
253+
254+
if sort_by:
255+
if sort_by.strip("-") not in fields:
256+
raise AdminApiException(422, detail=f"Sort by {sort_by} is not allowed")
257+
elif admin_model.ordering:
258+
for ordering_field in admin_model.ordering:
259+
if ordering_field.strip("-") not in fields:
260+
raise AdminApiException(422, detail=f"Sort by {ordering_field} is not allowed")
227261

228262
content_type = "text/plain"
229263
file_name = f"{model}.txt"

fastadmin/models/orms/django.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,10 @@ def orm_get_list(
230230
qs = self.model_cls.objects.all()
231231

232232
if filters:
233-
for filter_condition, value in filters.items():
234-
qs = qs.filter(**{filter_condition: value})
233+
for field_with_condition, value in filters.items():
234+
field = field_with_condition[0]
235+
condition = field_with_condition[1]
236+
qs = qs.filter(**{f"{field}__{condition}" if condition != "exact" else field: value})
235237

236238
if search and self.search_fields:
237239
ids = []

fastadmin/models/orms/ponyorm.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -230,28 +230,29 @@ def orm_get_list(
230230
qs = select(m for m in self.model_cls)
231231

232232
if filters:
233-
for filter_condition, value in filters.items():
234-
if "__" not in filter_condition:
235-
filter_condition = f"{filter_condition}__exact"
236-
field = filter_condition.split("__")[0]
237-
cond = filter_condition.split("__")[1]
238-
pony_cond = "=="
239-
match cond:
233+
for field_with_condition, value in filters.items():
234+
field = field_with_condition[0]
235+
condition = field_with_condition[1]
236+
model_pk_name = self.get_model_pk_name(self.model_cls)
237+
if field.endswith(f"_{model_pk_name}"):
238+
field = field.replace(f"_{model_pk_name}", f".{model_pk_name}")
239+
pony_condition = "=="
240+
match condition:
240241
case "lte":
241-
pony_cond = ">="
242+
pony_condition = ">="
242243
case "gte":
243-
pony_cond = "<="
244+
pony_condition = "<="
244245
case "lt":
245-
pony_cond = ">"
246+
pony_condition = ">"
246247
case "gt":
247-
pony_cond = "<"
248+
pony_condition = "<"
248249
case "exact":
249-
pony_cond = "=="
250+
pony_condition = "=="
250251
case "contains":
251-
pony_cond = "in"
252+
pony_condition = "in"
252253
case "icontains":
253-
pony_cond = "in"
254-
filter_expr = f""""{value}" {pony_cond} m.{field}"""
254+
pony_condition = "in"
255+
filter_expr = f""""{value}" {pony_condition} m.{field}"""
255256
qs = qs.filter(filter_expr)
256257

257258
if search and self.search_fields:

fastadmin/models/orms/sqlalchemy.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -251,10 +251,24 @@ def convert_sort_by(sort_by: str) -> str:
251251

252252
if filters:
253253
q = []
254-
for filter_condition, value in filters.items():
255-
if "__icontains" in filter_condition:
256-
field = filter_condition.replace("__icontains", "")
257-
q.append(getattr(self.model_cls, field).ilike(f"%{value}%"))
254+
for field_with_condition, value in filters.items():
255+
field = field_with_condition[0]
256+
condition = field_with_condition[1]
257+
match condition:
258+
case "lte":
259+
q.append(getattr(self.model_cls, field) >= value)
260+
case "gte":
261+
q.append(getattr(self.model_cls, field) <= value)
262+
case "lt":
263+
q.append(getattr(self.model_cls, field) > value)
264+
case "gt":
265+
q.append(getattr(self.model_cls, field) < value)
266+
case "exact":
267+
q.append(getattr(self.model_cls, field) == value)
268+
case "contains":
269+
q.append(getattr(self.model_cls, field).like(f"%{value}%"))
270+
case "icontains":
271+
q.append(getattr(self.model_cls, field).ilike(f"%{value}%"))
258272
qs = qs.filter(and_(*q))
259273

260274
if search and self.search_fields:

fastadmin/models/orms/tortoise.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,10 @@ async def orm_get_list(
231231
qs = self.model_cls.all()
232232

233233
if filters:
234-
for filter_condition, value in filters.items():
235-
qs = qs.filter(**{filter_condition: value})
234+
for field_with_condition, value in filters.items():
235+
field = field_with_condition[0]
236+
condition = field_with_condition[1]
237+
qs = qs.filter(**{f"{field}__{condition}" if condition != "exact" else field: value})
236238

237239
if search and self.search_fields:
238240
ids = []

frontend/src/components/inline-widget/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export const InlineWidget: React.FC<IInlineWidget> = ({ modelConfiguration, pare
6666

6767
const onOpenAdd = useCallback(() => {
6868
formAdd.resetFields();
69-
formAdd.setFieldsValue({ [modelConfiguration.fk_name]: parentId });
69+
formAdd.setFieldValue(modelConfiguration.fk_name, parentId);
7070
setOpenAdd(true);
7171
setOpenChange(undefined);
7272
}, [formAdd, parentId, modelConfiguration.fk_name]);
@@ -79,7 +79,7 @@ export const InlineWidget: React.FC<IInlineWidget> = ({ modelConfiguration, pare
7979
const onOpenChange = useCallback(
8080
(item: any) => {
8181
formChange.resetFields();
82-
formChange.setFieldsValue({ [modelConfiguration.fk_name]: parentId });
82+
formChange.setFieldValue(modelConfiguration.fk_name, parentId);
8383
formChange.setFieldsValue(transformDataFromServer(item));
8484
setOpenChange(item);
8585
setOpenAdd(false);
@@ -97,6 +97,7 @@ export const InlineWidget: React.FC<IInlineWidget> = ({ modelConfiguration, pare
9797
sort_by: sortBy,
9898
offset: (page - 1) * pageSize,
9999
limit: pageSize,
100+
[modelConfiguration.fk_name]: parentId,
100101
...transformFiltersToServer(filters),
101102
});
102103

tests/api/test_helpers.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,21 @@
33

44
import jwt
55

6-
from fastadmin.api.helpers import get_user_id_from_session_id, is_digit, is_valid_id, is_valid_uuid, sanitize
6+
from fastadmin.api.helpers import (
7+
get_user_id_from_session_id,
8+
is_digit,
9+
is_valid_id,
10+
is_valid_uuid,
11+
sanitize_filter_value,
12+
)
713
from fastadmin.settings import settings
814

915

10-
async def test_sanitize():
11-
assert sanitize("true") is True
12-
assert sanitize("false") is False
13-
assert sanitize("null") is None
14-
assert sanitize("foo") == "foo"
16+
async def test_sanitize_filter_value():
17+
assert sanitize_filter_value("true") is True
18+
assert sanitize_filter_value("false") is False
19+
assert sanitize_filter_value("null") is None
20+
assert sanitize_filter_value("foo") == "foo"
1521

1622

1723
async def test_is_valid_uuid():

tests/api/test_list.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,22 @@ async def test_list_filters(session_id, event, client):
4545
)
4646
assert r.status_code == 422, r.text
4747

48+
r = await client.get(
49+
f"/api/list/{event.get_model_name()}?tournament=-1",
50+
)
51+
assert r.status_code == 200, r.text
52+
data = r.json()
53+
assert data
54+
assert data["total"] == 0
55+
56+
r = await client.get(
57+
f"/api/list/{event.get_model_name()}?tournament={event.tournament_id if hasattr(event, 'tournament_id') else event.tournament.id}",
58+
)
59+
assert r.status_code == 200, r.text
60+
data = r.json()
61+
assert data
62+
assert data["total"] > 0
63+
4864

4965
async def test_list_search(session_id, admin_models, event, client):
5066
assert session_id

0 commit comments

Comments
 (0)