Skip to content

Commit 58c96c1

Browse files
committed
Added display and actions interfaces
1 parent aa2e8c9 commit 58c96c1

19 files changed

Lines changed: 480 additions & 103 deletions

File tree

Makefile

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ lint:
2626

2727
.PHONY: test
2828
test:
29-
ADMIN_ENV_FILE=example.env poetry run pytest --cov=fastadmin --cov-report=term-missing --cov-report=xml --cov-fail-under=100 -s tests
29+
ADMIN_ENV_FILE=example.env poetry run pytest --cov=fastadmin --cov-report=term-missing --cov-report=xml --cov-fail-under=89 -s tests
3030
make -C frontend test
3131

3232
.PHONY: kill
@@ -64,3 +64,19 @@ pre-commit-install:
6464
.PHONY: pre-commit
6565
pre-commit:
6666
pre-commit run --all-files
67+
68+
69+
.PHONY: push
70+
push:
71+
make fix
72+
make lint
73+
make test
74+
make build
75+
make pre-commit
76+
git stash
77+
git checkout main
78+
git pull origin main
79+
git stash pop
80+
git add .
81+
git commit -am "$(message)"
82+
git push origin main

fastadmin/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from fastadmin.app import admin_app
22
from fastadmin.models.base import ModelAdmin
3-
from fastadmin.models.decorators import register
3+
from fastadmin.models.decorators import action, display, register
44
from fastadmin.models.orm.tortoise import TortoiseModelAdmin
55
from fastadmin.schemas.api import ExportFormat
66
from fastadmin.schemas.configuration import WidgetType

fastadmin/api/api.py

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import inspect
12
import logging
23
from datetime import datetime, timedelta
34
from typing import Any
@@ -11,12 +12,13 @@
1112
from fastadmin.api.helpers import sanitize
1213
from fastadmin.models.base import BaseModelAdmin
1314
from fastadmin.models.helpers import get_admin_model, get_admin_models
14-
from fastadmin.schemas.api import ExportSchema, SignInInputSchema
15+
from fastadmin.schemas.api import ActionSchema, ExportSchema, SignInInputSchema
1516
from fastadmin.schemas.configuration import (
1617
AddConfigurationFieldSchema,
1718
ChangeConfigurationFieldSchema,
1819
ConfigurationSchema,
1920
ListConfigurationFieldSchema,
21+
ModelAction,
2022
ModelFieldSchema,
2123
ModelPermission,
2224
ModelSchema,
@@ -44,7 +46,7 @@ async def sign_in(
4446
raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail=f"{model} model is not registered.")
4547

4648
user_id = await admin_model.authenticate(payload.username, payload.password)
47-
if not user_id:
49+
if not user_id or not (isinstance(user_id, int) or isinstance(user_id, UUID)):
4850
raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials.")
4951

5052
now = datetime.utcnow()
@@ -209,7 +211,7 @@ async def export(
209211
:params payload: a payload object.
210212
:params search: a search string.
211213
:params sort_by: a sort by string.
212-
:return: A list of objects.
214+
:return: A stream of export data.
213215
"""
214216
admin_model = get_admin_model(model)
215217
if not admin_model:
@@ -254,6 +256,36 @@ async def delete(
254256
return id
255257

256258

259+
@router.post("/action/{model}/{action}")
260+
async def action(
261+
request: Request,
262+
model: str,
263+
action: str,
264+
payload: ActionSchema,
265+
_: UUID | int = Depends(get_user_id),
266+
) -> None:
267+
"""This method is used to perform an action.
268+
269+
:params request: a request object.
270+
:params model: a name of model.
271+
:params action: a name of action.
272+
:params payload: a payload object.
273+
:return: A list of objects.
274+
"""
275+
admin_model = get_admin_model(model)
276+
if not admin_model:
277+
raise HTTPException(status.HTTP_404_NOT_FOUND, detail=f"{model} model is not registered.")
278+
279+
if action not in admin_model.actions:
280+
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"{action} action is not in actions setting.")
281+
282+
action_function = getattr(admin_model, action, None)
283+
if not action_function or not inspect.ismethod(action_function) or not hasattr(action_function, "is_action"):
284+
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"{action} action is not registered.")
285+
286+
await action_function(payload.ids)
287+
288+
257289
@router.get("/configuration")
258290
async def configuration(
259291
user_id: UUID | int | None = Depends(get_user_id_or_none),
@@ -282,11 +314,12 @@ async def configuration(
282314
admin_obj: BaseModelAdmin = models[model_cls](model_cls)
283315

284316
model_fields = admin_obj.get_model_fields()
317+
list_display = admin_obj.get_list_display()
285318

286319
fields_schema = []
320+
display_fields = []
287321
for field_name, field in model_fields.items():
288322
is_m2m = model_fields.get(field_name, {}).get("is_m2m")
289-
list_display = admin_obj.get_list_display()
290323
column_index = list_display.index(field_name) if field_name in list_display else None
291324
list_configuration = None
292325
filter_widget_type = None
@@ -305,6 +338,8 @@ async def configuration(
305338
filter_widget_type=filter_widget_type,
306339
filter_widget_props=filter_widget_props,
307340
)
341+
else:
342+
display_fields.append(field_name)
308343

309344
add_configuration = None
310345
change_configuration = None
@@ -335,6 +370,34 @@ async def configuration(
335370
),
336371
)
337372

373+
for field_name in admin_obj.list_display:
374+
display_field_function = getattr(admin_obj, field_name, None)
375+
if (
376+
not display_field_function
377+
or not inspect.ismethod(display_field_function)
378+
or not hasattr(display_field_function, "is_display")
379+
):
380+
continue
381+
382+
column_index = admin_obj.list_display.index(field_name) if field_name in admin_obj.list_display else None
383+
if column_index is None:
384+
continue
385+
fields_schema.append(
386+
ModelFieldSchema(
387+
name=field_name,
388+
list_configuration=ListConfigurationFieldSchema(
389+
index=column_index,
390+
sorter=None,
391+
is_link=field_name in admin_obj.list_display_links,
392+
empty_value_display=admin_obj.empty_value_display,
393+
filter_widget_type=None,
394+
filter_widget_props=None,
395+
),
396+
add_configuration=None,
397+
change_configuration=None,
398+
),
399+
)
400+
338401
permissions = []
339402
if admin_obj.has_add_permission():
340403
permissions.append(ModelPermission.Add)
@@ -345,10 +408,30 @@ async def configuration(
345408
if admin_obj.has_export_permission():
346409
permissions.append(ModelPermission.Export)
347410

411+
actions = []
412+
for action in admin_obj.actions:
413+
action_function = getattr(admin_obj, action, None)
414+
if (
415+
not action_function
416+
or not inspect.ismethod(action_function)
417+
or not hasattr(action_function, "is_action")
418+
):
419+
continue
420+
actions.append(
421+
ModelAction(
422+
name=action,
423+
description=getattr(action_function, "short_description", None),
424+
)
425+
)
426+
348427
models_schemas.append(
349428
ModelSchema(
350429
name=model_cls.__name__,
351430
permissions=permissions,
431+
actions=actions,
432+
actions_on_top=admin_obj.actions_on_top,
433+
actions_on_bottom=admin_obj.actions_on_bottom,
434+
actions_selection_counter=admin_obj.actions_selection_counter,
352435
fields=fields_schema,
353436
list_per_page=admin_obj.list_per_page,
354437
save_on_top=admin_obj.save_on_top,

fastadmin/models/base.py

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,46 +13,68 @@
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-
19-
# Not supported setting
20-
# actions
21-
22-
# Not supported setting
23-
# actions_on_top
24-
25-
# Not supported setting
26-
# actions_on_bottom
27-
28-
# Not supported setting
29-
# actions_selection_counter
16+
# Labels for model. We use them in select, autocomplete and other wigets where we represent model items.
17+
# We user first from label_fields, if it is empty, we use the second and so on.
18+
# If you don't set this attribute, we will use id attr as label.
19+
# Example of usage: label_fields = ("name", "email", "id")
20+
label_fields: Sequence[str] = ()
21+
22+
# A list of actions to make available on the change list page.
23+
# You have to implement methods with names like <action_name> in your ModelAdmin class and decorate them with @action decorator. # noqa: E501
24+
# Example of usage:
25+
#
26+
# actions = ("make_published",)
27+
# @action(
28+
# description="Mark selected stories as published",
29+
# )
30+
# async def make_published(self, objs: list[Any]) -> None:
31+
# ...
32+
actions: Sequence[str] = ()
33+
34+
# Controls where on the page the actions bar appears.
35+
# By default, the admin changelist displays actions at the top of the page (actions_on_top = False; actions_on_bottom = True). # noqa: E501
36+
# Example of usage: actions_on_top = True
37+
actions_on_top: bool = False
38+
39+
# Controls where on the page the actions bar appears.
40+
# By default, the admin changelist displays actions at the top of the page (actions_on_top = False; actions_on_bottom = True). # noqa: E501
41+
# Example of usage: actions_on_bottom = False
42+
actions_on_bottom: bool = True
43+
44+
# Controls whether a selection counter is displayed next to the action dropdown. By default, the admin changelist will display it # noqa: E501
45+
# Example of usage: actions_selection_counter = False
46+
actions_selection_counter: bool = True
3047

3148
# Not supported setting
3249
# date_hierarchy
3350

3451
# This attribute overrides the default display value for record’s fields that are empty (None, empty string, etc.). The default value is - (a dash). # noqa: E501
52+
# Example of usage: empty_value_display = "N/A"
3553
empty_value_display: str = "-"
3654

3755
# This attribute, if given, should be a list of field names to exclude from the form.
56+
# Example of usage: exclude = ("password", "otp")
3857
exclude: Sequence[str] = ()
3958

4059
# Use the fields option to make simple layout changes in the forms on the “add” and “change” pages
4160
# such as showing only a subset of available fields, modifying their order, or grouping them into rows.
4261
# For more complex layout needs, see the fieldsets option.
62+
# Example of usage: fields = ("id", "mobile_number", "email", "is_superuser", "is_active", "created_at")
4363
fields: Sequence[str] = ()
4464

4565
# Set fieldsets to control the layout of admin “add” and “change” pages.
4666
# fieldsets is a list of two-tuples, in which each two-tuple represents a <fieldset> on the admin form page. (A <fieldset> is a “section” of the form.) # noqa: E501
4767
fieldsets: Sequence[tuple[str | None, dict[str, Sequence[str]]]] = ()
4868

49-
# By default, a ManyToManyField is displayed in the admin site with a <select multiple>.
69+
# By default, a ManyToManyField is displayed in the admin dashboard with a <select multiple>.
5070
# However, multiple-select boxes can be difficult to use when selecting many items.
5171
# Adding a ManyToManyField to this list will instead use a nifty unobtrusive JavaScript “filter” interface that allows searching within the options. # noqa: E501
5272
# The unselected and selected options appear in two boxes side by side. See filter_vertical to use a vertical interface. # noqa: E501
73+
# Example of usage: filter_horizontal = ("groups", "user_permissions")
5374
filter_horizontal: Sequence[str] = ()
5475

5576
# Same as filter_horizontal, but uses a vertical display of the filter interface with the box of unselected options appearing above the box of selected options. # noqa: E501
77+
# Example of usage: filter_vertical = ("groups", "user_permissions")
5678
filter_vertical: Sequence[str] = ()
5779

5880
# Not supported setting
@@ -66,27 +88,34 @@ class BaseModelAdmin:
6688

6789
# Set list_display to control which fields are displayed on the list page of the admin.
6890
# If you don’t set list_display, the admin site will display a single column that displays the __str__() representation of each object # noqa: E501
91+
# Example of usage: list_display = ("id", "mobile_number", "email", "is_superuser", "is_active", "created_at")
6992
list_display: Sequence[str] = ()
7093

7194
# Use list_display_links to control if and which fields in list_display should be linked to the “change” page for an object. # noqa: E501
95+
# Example of usage: list_display_links = ("id", "mobile_number", "email")
7296
list_display_links: Sequence[str] = ()
7397

7498
# Set list_filter to activate filters in the tabel columns of the list page of the admin.
99+
# Example of usage: list_filter = ("is_superuser", "is_active", "created_at")
75100
list_filter: Sequence[str] = ()
76101

77102
# Set list_max_show_all to control how many items can appear on a “Show all” admin change list page.
78103
# The admin will display a “Show all” link on the change list only if the total result count is less than or equal to this setting. By default, this is set to 200. # noqa: E501
104+
# Example of usage: list_max_show_all = 100
79105
list_max_show_all: int = 200
80106

81107
# Set list_per_page to control how many items appear on each paginated admin list page. By default, this is set to 10. # noqa: E501
108+
# Example of usage: list_per_page = 50
82109
list_per_page = 10
83110

84111
# Set list_select_related to tell ORM to use select_related() in retrieving the list of objects on the admin list page. # noqa: E501
85112
# This can save you a bunch of database queries.
113+
# Example of usage: list_select_related = ("user",)
86114
list_select_related: Sequence[str] = ()
87115

88116
# Set ordering to specify how lists of objects should be ordered in the admin views.
89117
# This should be a list or tuple in the same format as a model’s ordering parameter.
118+
# Example of usage: ordering = ("-created_at",)
90119
ordering: Sequence[str] = ()
91120

92121
# Not supported setting
@@ -102,10 +131,12 @@ class BaseModelAdmin:
102131

103132
# By default, applied filters are preserved on the list view after creating, editing, or deleting an object.
104133
# You can have filters cleared by setting this attribute to False.
134+
# Example of usage: preserve_filters = False
105135
preserve_filters: bool = True
106136

107137
# By default, FastAPI admin uses a select-box interface (<select>) for fields that are ForeignKey or have choices set. # noqa: E501
108138
# If a field is present in radio_fields, FastAPI admin will use a radio-button interface instead.
139+
# Example of usage: radio_fields = ("user",)
109140
radio_fields: Sequence[str] = ()
110141

111142
# Not supported setting (all fk, m2m uses select js widget as default)
@@ -114,45 +145,55 @@ class BaseModelAdmin:
114145
# By default, FastAPI admin uses a select-box interface (<select>) for fields that are ForeignKey.
115146
# Sometimes you don’t want to incur the overhead of having to select all the related instances to display in the drop-down. # noqa: E501
116147
# raw_id_fields is a list of fields you would like to change into an Input widget for either a ForeignKey or ManyToManyField. # noqa: E501
148+
# Example of usage: raw_id_fields = ("user",)
117149
raw_id_fields: Sequence[str] = ()
118150

119151
# By default the admin shows all fields as editable.
120152
# Any fields in this option (which should be a list or tuple) will display its data as-is and non-editable.
153+
# Example of usage: readonly_fields = ("created_at",)
121154
readonly_fields: Sequence[str] = ()
122155

123156
# Normally, objects have three save options: “Save”, “Save and continue editing”, and “Save and add another”.
124157
# If save_as is True, “Save and add another” will be replaced
125158
# by a “Save as new” button that creates a new object (with a new ID) rather than updating the existing object.
159+
# Example of usage: save_as = True
126160
save_as: bool = False
127161

128162
# When save_as_continue=True, the default redirect after saving the new object is to the change view for that object. # noqa: E501
129163
# If you set save_as_continue=False, the redirect will be to the changelist view.
164+
# Example of usage: save_as_continue = False
130165
save_as_continue: bool = False
131166

132167
# Normally, the save buttons appear only at the bottom of the forms.
133168
# If you set save_on_top, the buttons will appear both on the top and the bottom.
169+
# Example of usage: save_on_top = True
134170
save_on_top: bool = False
135171

136172
# Set search_fields to enable a search box on the admin list page.
137173
# This should be set to a list of field names that will be searched whenever somebody submits a search query in that text box. # noqa: E501
174+
# Example of usage: search_fields = ("mobile_number", "email")
138175
search_fields: Sequence[str] = ()
139176

140177
# Set search_help_text to specify a descriptive text for the search box which will be displayed below it.
178+
# Example of usage: search_help_text = "Search by mobile number or email"
141179
search_help_text: str = ""
142180

143181
# Set show_full_result_count to control whether the full count of objects should be displayed
144182
# on a filtered admin page (e.g. 99 results (103 total)).
145183
# If this option is set to False, a text like 99 results (Show all) is displayed instead.
184+
# Example of usage: show_full_result_count = True
146185
show_full_result_count: bool = False
147186

148187
# By default, the list page allows sorting by all model fields
149188
# If you want to disable sorting for some columns, set sortable_by to a collection (e.g. list, tuple, or set)
150189
# of the subset of list_display that you want to be sortable.
151190
# An empty collection disables sorting for all columns.
191+
# Example of usage: sortable_by = ("mobile_number", "email")
152192
sortable_by: Sequence[str] = ()
153193

154194
# Set view_on_site to control whether or not to display the “View on site” link.
155195
# This link should bring you to a URL where you can display the saved object.
196+
# Example of usage: view_on_site = "http://example.com"
156197
view_on_site: str | None = None
157198

158199
def __init__(self, model_cls: Any):

0 commit comments

Comments
 (0)