Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,34 @@ def get_page_context(page_url):
"content": "Model-admin-specific methods and attributes:",
},
{"type": "code-python", "content": inspect.getsource(ModelAdmin)},
{
"type": "text",
"content": "You can customize relation loading and relation search by overriding <code>orm_get_list</code> and forwarding <code>prefetch_related_fields</code> and <code>additional_search_fields</code>:",
},
{
"type": "code-python",
"content": """class TaskAdmin(TortoiseModelAdmin):
search_fields = ("title",)

async def orm_get_list(
self,
offset=None,
limit=None,
search=None,
sort_by=None,
filters=None,
):
return await super().orm_get_list(
offset=offset,
limit=limit,
search=search,
sort_by=sort_by,
filters=filters,
prefetch_related_fields=["user"],
additional_search_fields=["user__email"],
)
""",
},
]
case "#model-form-field-types":
return [
Expand Down
64 changes: 63 additions & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ <h1>FastAdmin | Documentation</h1>
</li>
<li>
<strong>Updated:</strong>
22 February 2026
23 February 2026
</li>
</ul>
</div>
Expand Down Expand Up @@ -2269,6 +2269,8 @@ <h3>Methods and Attributes</h3>
search: str | None = None,
sort_by: str | None = None,
filters: dict | None = None,
prefetch_related_fields: list[str] | None = None,
additional_search_fields: list[str] | None = None,
) -> tuple[list[Any], int]:
"""This method is used to get list of orm/db model objects.

Expand All @@ -2277,6 +2279,8 @@ <h3>Methods and Attributes</h3>
:params search: a search query.
:params sort_by: a sort by field name.
:params filters: a dict of filters.
:params prefetch_related_fields: a list of related fields to prefetch.
:params additional_search_fields: a list of additional search fields.
:return: A tuple of list of objects and total count.
"""
raise NotImplementedError
Expand Down Expand Up @@ -2759,6 +2763,64 @@ <h3>Methods and Attributes</h3>




<p class="text-4">
You can customize relation loading and relation search by overriding <code>orm_get_list</code> and forwarding <code>prefetch_related_fields</code> and <code>additional_search_fields</code>:
</p>

























<pre>
<code class="language-python">
class TaskAdmin(TortoiseModelAdmin):
search_fields = ("title",)

async def orm_get_list(
self,
offset=None,
limit=None,
search=None,
sort_by=None,
filters=None,
):
return await super().orm_get_list(
offset=offset,
limit=limit,
search=search,
sort_by=sort_by,
filters=filters,
prefetch_related_fields=["user"],
additional_search_fields=["user__email"],
)

</code>
</pre>




</section>


Expand Down
4 changes: 4 additions & 0 deletions fastadmin/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ async def orm_get_list(
search: str | None = None,
sort_by: str | None = None,
filters: dict | None = None,
prefetch_related_fields: list[str] | None = None,
additional_search_fields: list[str] | None = None,
) -> tuple[list[Any], int]:
"""This method is used to get list of orm/db model objects.

Expand All @@ -235,6 +237,8 @@ async def orm_get_list(
:params search: a search query.
:params sort_by: a sort by field name.
:params filters: a dict of filters.
:params prefetch_related_fields: a list of related fields to prefetch.
:params additional_search_fields: a list of additional search fields.
:return: A tuple of list of objects and total count.
"""
raise NotImplementedError
Expand Down
20 changes: 16 additions & 4 deletions fastadmin/models/orms/django.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import operator
from base64 import b64decode
from typing import Any
from uuid import UUID
Expand Down Expand Up @@ -245,6 +244,8 @@ def orm_get_list(
search: str | None = None,
sort_by: str | None = None,
filters: dict | None = None,
prefetch_related_fields: list[str] | None = None,
additional_search_fields: list[str] | None = None,
) -> tuple[list[Any], int]:
"""This method is used to get list of orm/db model objects.

Expand All @@ -253,19 +254,30 @@ def orm_get_list(
:params search: a search query.
:params sort_by: a sort by field name.
:params filters: a dict of filters.
:params prefetch_related_fields: a list of related fields to prefetch.
:params additional_search_fields: a list of additional search fields.
:return: A tuple of list of objects and total count.
"""
qs = self.model_cls.objects.all()

if prefetch_related_fields:
qs = qs.prefetch_related(*prefetch_related_fields)

if filters:
for field_with_condition, value in filters.items():
field = field_with_condition[0]
condition = field_with_condition[1]
qs = qs.filter(**{f"{field}__{condition}" if condition != "exact" else field: value})

if search and self.search_fields:
search_conditions = [Q(**{f + "__icontains": search}) for f in self.search_fields]
search_q = search_conditions[0] if len(search_conditions) == 1 else operator.or_(*search_conditions)
search_fields = list(self.search_fields)
if additional_search_fields:
search_fields.extend(additional_search_fields)

if search and search_fields:
search_conditions = [Q(**{f + "__icontains": search}) for f in search_fields]
search_q = search_conditions[0]
for condition in search_conditions[1:]:
search_q |= condition
qs = qs.filter(search_q)
Comment on lines +276 to 281
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

operator.or_ only accepts 2 arguments. When search_fields contains 3+ entries (now more likely with additional_search_fields), operator.or_(*search_conditions) will raise TypeError. Build the combined Q with functools.reduce(operator.or_, search_conditions) (or loop with q |= cond) instead.

Copilot uses AI. Check for mistakes.

if sort_by:
Expand Down
15 changes: 13 additions & 2 deletions fastadmin/models/orms/ponyorm.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ def orm_get_list(
search: str | None = None,
sort_by: str | None = None,
filters: dict | None = None,
prefetch_related_fields: list[str] | None = None,
additional_search_fields: list[str] | None = None,
) -> tuple[list[Any], int]:
"""This method is used to get list of orm/db model objects.

Expand All @@ -232,6 +234,8 @@ def orm_get_list(
:params search: a search query.
:params sort_by: a sort by field name.
:params filters: a dict of filters.
:params prefetch_related_fields: a list of related fields to prefetch.
:params additional_search_fields: a list of additional search fields.
:return: A tuple of list of objects and total count.
"""

Expand Down Expand Up @@ -271,9 +275,13 @@ def orm_get_list(
filter_expr = f""""{value}" {pony_condition} m.{field}"""
qs = qs.filter(filter_expr)

if search and self.search_fields:
search_fields = list(self.search_fields)
if additional_search_fields:
search_fields.extend(additional_search_fields)

if search and search_fields:
ids = []
for search_field in self.search_fields:
for search_field in search_fields:
pony_search_field = search_field.replace("__", ".")
# Pony string filter for case-insensitive search
filter_expr = f'"{search.lower()}" in m.{pony_search_field}.lower()'
Expand All @@ -296,6 +304,9 @@ def orm_get_list(
if self.list_select_related:
qs = qs.prefetch(*[getattr(self.model_cls, field) for field in self.list_select_related])

if prefetch_related_fields:
qs = qs.prefetch(*[getattr(self.model_cls, field) for field in prefetch_related_fields])

if offset is not None and limit is not None:
qs = qs.limit(limit, offset=offset)

Expand Down
34 changes: 32 additions & 2 deletions fastadmin/models/orms/sqlalchemy.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ async def orm_get_list(
search: str | None = None,
sort_by: str | None = None,
filters: dict | None = None,
prefetch_related_fields: list[str] | None = None,
additional_search_fields: list[str] | None = None,
) -> tuple[list[Any], int]:
"""This method is used to get list of orm/db model objects.

Expand All @@ -308,6 +310,8 @@ async def orm_get_list(
:params search: a search query.
:params sort_by: a sort by field name.
:params filters: a dict of filters.
:params prefetch_related_fields: a list of related fields to prefetch.
:params additional_search_fields: a list of additional search fields.
:return: A tuple of list of objects and total count.
"""

Expand Down Expand Up @@ -354,9 +358,13 @@ def convert_sort_by(sort_by: str) -> str:
q.append(model_field.ilike(f"%{value}%"))
qs = qs.where(and_(*q))

if search and self.search_fields:
search_fields = list(self.search_fields)
if additional_search_fields:
search_fields.extend(additional_search_fields)

if search and search_fields:
q = []
for field in self.search_fields:
for field in search_fields:
condition = self._build_search_condition(field, search)
if condition is not None:
q.append(condition)
Expand All @@ -377,6 +385,28 @@ def convert_sort_by(sort_by: str) -> str:
for field in self.list_select_related:
qs = qs.options(selectinload(getattr(self.model_cls, field)))

if prefetch_related_fields:
for field_path in prefetch_related_fields:
parts = field_path.split("__")
current_model = self.model_cls
attr = getattr(current_model, parts[0], None)
if attr is None:
continue
option = selectinload(attr)
current_model = getattrs(attr, "property.mapper.class_")
for part in parts[1:]:
if current_model is None:
break
nested_attr = getattr(current_model, part, None)
if nested_attr is None:
break
next_model = getattrs(nested_attr, "property.mapper.class_")
if next_model is None:
break
option = option.selectinload(nested_attr)
current_model = next_model
qs = qs.options(option)

if offset is not None and limit is not None:
qs = qs.offset(offset)
qs = qs.limit(limit)
Expand Down
15 changes: 13 additions & 2 deletions fastadmin/models/orms/tortoise.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@ async def orm_get_list(
search: str | None = None,
sort_by: str | None = None,
filters: dict | None = None,
prefetch_related_fields: list[str] | None = None,
additional_search_fields: list[str] | None = None,
) -> tuple[list[Any], int]:
"""This method is used to get list of orm/db model objects.

Expand All @@ -262,21 +264,30 @@ async def orm_get_list(
:params search: a search query.
:params sort_by: a sort by field name.
:params filters: a dict of filters.
:params prefetch_related_fields: a list of related fields to prefetch.
:params additional_search_fields: a list of additional search fields.
:return: A tuple of list of objects and total count.
"""
qs = self.model_cls.all()

if prefetch_related_fields:
qs = qs.prefetch_related(*prefetch_related_fields).distinct()

if filters:
for field_with_condition, value in filters.items():
field = field_with_condition[0]
condition = field_with_condition[1]
qs = qs.filter(**{f"{field}__{condition}" if condition != "exact" else field: value})

if search and self.search_fields:
search_fields = list(self.search_fields)
if additional_search_fields:
search_fields.extend(additional_search_fields)

if search and search_fields:
qs = qs.filter(
functools.reduce(
operator.or_,
(Q(**{f + "__icontains": search}) for f in self.search_fields),
(Q(**{f + "__icontains": search}) for f in search_fields),
Q(),
)
)
Expand Down
Loading