Skip to content

Commit 10c8b97

Browse files
committed
Added fieldsets interface
1 parent 270b054 commit 10c8b97

8 files changed

Lines changed: 143 additions & 51 deletions

File tree

fastadmin/api/api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ async def configuration(
430430
actions_on_bottom=admin_obj.actions_on_bottom,
431431
actions_selection_counter=admin_obj.actions_selection_counter,
432432
fields=fields_schema,
433+
fieldsets=admin_obj.fieldsets,
433434
list_per_page=admin_obj.list_per_page,
434435
save_on_top=admin_obj.save_on_top,
435436
save_as=admin_obj.save_as,

fastadmin/models/base.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -420,18 +420,21 @@ def get_fields(self) -> Sequence[str]:
420420
421421
:return: A list of model field names.
422422
"""
423-
fields = self.fields
424423
model_fields = self.get_model_fields()
425-
if not fields:
426-
return [f for f in model_fields if not self.exclude or f not in self.exclude]
427-
return [f for f in fields if f in model_fields]
428424

429-
# def get_fieldsets(self) -> Sequence[tuple[str | None, dict[str, Sequence[str]]]]:
430-
# """This method is used to get fieldsets data for form view.
431-
432-
# :return: A list of fieldsets data.
433-
# """
434-
# return self.fieldsets
425+
fields = [f for f in model_fields if model_fields[f].get("is_pk")]
426+
if not self.fields:
427+
if self.fieldsets:
428+
for item in self.fieldsets:
429+
for field in item[1].get("fields") or []:
430+
if field not in fields and field not in self.exclude:
431+
fields.append(field)
432+
else:
433+
for field in model_fields:
434+
if field not in fields and field not in self.exclude:
435+
fields.append(field)
436+
437+
return fields
435438

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

fastadmin/models/orm/tortoise.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ def get_form_widget(self, field_name: str) -> tuple[WidgetType, dict]:
247247
widget_props = {
248248
"required": field.get("required") or False,
249249
"disabled": field_name in self.readonly_fields,
250-
"readonly": field_name in self.readonly_fields,
250+
"readOnly": field_name in self.readonly_fields,
251251
}
252252
match field.get("orm_class_name"):
253253
case "CharField":

fastadmin/schemas/configuration.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ class ModelSchema(BaseModel):
9292
actions_on_bottom: bool | None
9393
actions_selection_counter: bool | None
9494
fields: Sequence[ModelFieldSchema]
95+
fieldsets: Sequence[tuple[str | None, dict[str, Sequence[str]]]] | None
9596
list_per_page: int | None
9697
save_on_top: bool | None
9798
save_as: bool | None

frontend/src/components/form-container/index.tsx

Lines changed: 63 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useCallback, useContext } from 'react';
22
import { useParams } from 'react-router-dom';
33
import { useTranslation } from 'react-i18next';
4-
import { Divider, Form } from 'antd';
4+
import { Collapse, Divider, Form } from 'antd';
55

66
import {
77
EFieldWidgetType,
@@ -50,39 +50,69 @@ export const FormContainer: React.FC<IFormContainer> = ({ form, onFinish, childr
5050
[mode]
5151
);
5252

53+
const formItemWidgets = useCallback(
54+
(formFields: IModelField[]) => {
55+
return formFields
56+
.sort((a: IModelField, b: IModelField) => (getConf(a).index || 0) - (getConf(b).index || 0))
57+
.map((field: IModelField) => (
58+
<Form.Item
59+
key={field.name}
60+
name={field.name}
61+
label={getTitleFromFieldName(field.name)}
62+
rules={
63+
getConf(field).required
64+
? [
65+
{
66+
required: true,
67+
},
68+
]
69+
: []
70+
}
71+
valuePropName={
72+
[
73+
EFieldWidgetType.Checkbox,
74+
EFieldWidgetType.Switch,
75+
EFieldWidgetType.CheckboxGroup,
76+
].includes(getConf(field).form_widget_type as EFieldWidgetType)
77+
? 'checked'
78+
: undefined
79+
}
80+
>
81+
{getWidget(getConf(field))}
82+
</Form.Item>
83+
));
84+
},
85+
[getConf, getWidget]
86+
);
87+
5388
const formItems = useCallback(() => {
54-
const fields = (modelConfiguration || { fields: [] }).fields;
55-
return fields
56-
.filter((field: IModelField) => !!getConf(field).form_widget_type)
57-
.sort((a: IModelField, b: IModelField) => (getConf(a).index || 0) - (getConf(b).index || 0))
58-
.map((field: IModelField) => (
59-
<Form.Item
60-
key={field.name}
61-
name={field.name}
62-
label={getTitleFromFieldName(field.name)}
63-
rules={
64-
getConf(field).required
65-
? [
66-
{
67-
required: true,
68-
},
69-
]
70-
: []
71-
}
72-
valuePropName={
73-
[
74-
EFieldWidgetType.Checkbox,
75-
EFieldWidgetType.Switch,
76-
EFieldWidgetType.CheckboxGroup,
77-
].includes(getConf(field).form_widget_type as EFieldWidgetType)
78-
? 'checked'
79-
: undefined
80-
}
81-
>
82-
{getWidget(getConf(field))}
83-
</Form.Item>
84-
));
85-
}, [getConf, getWidget, modelConfiguration]);
89+
const fields = (modelConfiguration?.fields || []).filter(
90+
(field: IModelField) => !!getConf(field).form_widget_type
91+
);
92+
const fieldsets = modelConfiguration?.fieldsets || [];
93+
if (fieldsets.length > 0) {
94+
const defaultActiveKey = fieldsets
95+
.filter((fieldset) => !(fieldset[1]?.classes || []).includes('collapse'))
96+
.map((fieldset) => JSON.stringify(fieldset[1]?.fields));
97+
return (
98+
<Collapse defaultActiveKey={defaultActiveKey}>
99+
{fieldsets.map((fieldset) => {
100+
const collapseTitle = fieldset[0];
101+
const collapseFields = fieldset[1]?.fields;
102+
return (
103+
<Collapse.Panel header={collapseTitle || ''} key={JSON.stringify(collapseFields)}>
104+
{formItemWidgets(
105+
fields.filter((field: IModelField) => (collapseFields || []).includes(field.name))
106+
)}
107+
</Collapse.Panel>
108+
);
109+
})}
110+
</Collapse>
111+
);
112+
}
113+
114+
return formItemWidgets(fields);
115+
}, [formItemWidgets, modelConfiguration?.fields, modelConfiguration?.fieldsets]);
86116

87117
return (
88118
<Form layout="vertical" form={form} onFinish={onFinish}>

frontend/src/interfaces/configuration.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export interface IModel {
6767
actions_on_bottom?: boolean;
6868
actions_selection_counter?: boolean;
6969
fields: IModelField[];
70+
fieldsets?: [string | undefined, Record<string, string[]>][];
7071
list_per_page?: number;
7172
save_on_top?: boolean;
7273
save_as?: boolean;

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.16"
3+
version = "0.1.17"
44
description = ""
55
authors = ["Seva D <[email protected]>"]
66
license = "MIT"

tests/api/api/test_configuration.py

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ async def test_configuration_list_display(objects, client):
142142
assert response_data
143143
validate_configuration_response_data(response_data)
144144

145+
admin_event_cls.list_display = ()
145146
unregister_admin_model([event.__class__])
146147
await sign_out(client, superuser)
147148

@@ -155,7 +156,7 @@ async def test_configuration_list_display_display_fields(objects, client):
155156
await sign_in(client, superuser, admin_user_cls)
156157

157158
register_admin_model(admin_event_cls, [event.__class__])
158-
admin_event_cls.list_display = ["started", "name_with_price"] # see EventAdmin display methods
159+
admin_event_cls.list_display = ("started", "name_with_price") # see EventAdmin display methods
159160
r = await client.get(
160161
f"/api/configuration",
161162
)
@@ -164,6 +165,7 @@ async def test_configuration_list_display_display_fields(objects, client):
164165
assert response_data
165166
validate_configuration_response_data(response_data)
166167

168+
admin_event_cls.list_display = ()
167169
unregister_admin_model([event.__class__])
168170
await sign_out(client, superuser)
169171

@@ -187,6 +189,8 @@ async def test_configuration_list_filter(objects, client):
187189
assert response_data
188190
validate_configuration_response_data(response_data)
189191

192+
admin_event_cls.list_display = ()
193+
admin_event_cls.list_filter = ()
190194
unregister_admin_model([event.__class__])
191195
await sign_out(client, superuser)
192196

@@ -202,7 +206,7 @@ async def test_configuration_sortable_by(objects, client):
202206
register_admin_model(admin_event_cls, [event.__class__])
203207
admin_event_cls.list_display = LIST_EVENT_FIELDS
204208
admin_event_cls.list_filter = LIST_EVENT_FIELDS
205-
admin_event_cls.sortable_by = ["name"]
209+
admin_event_cls.sortable_by = ("name",)
206210
r = await client.get(
207211
f"/api/configuration",
208212
)
@@ -211,6 +215,9 @@ async def test_configuration_sortable_by(objects, client):
211215
assert response_data
212216
validate_configuration_response_data(response_data)
213217

218+
admin_event_cls.list_display = ()
219+
admin_event_cls.list_filter = ()
220+
admin_event_cls.sortable_by = ()
214221
unregister_admin_model([event.__class__])
215222
await sign_out(client, superuser)
216223

@@ -226,7 +233,7 @@ async def test_configuration_radio_fields(objects, client):
226233
register_admin_model(admin_event_cls, [event.__class__])
227234
admin_event_cls.list_display = LIST_EVENT_FIELDS
228235
admin_event_cls.list_filter = LIST_EVENT_FIELDS
229-
admin_event_cls.radio_fields = ["event_type"]
236+
admin_event_cls.radio_fields = ("event_type",)
230237
r = await client.get(
231238
f"/api/configuration",
232239
)
@@ -235,6 +242,9 @@ async def test_configuration_radio_fields(objects, client):
235242
assert response_data
236243
validate_configuration_response_data(response_data)
237244

245+
admin_event_cls.list_display = ()
246+
admin_event_cls.list_filter = ()
247+
admin_event_cls.radio_fields = ()
238248
unregister_admin_model([event.__class__])
239249
await sign_out(client, superuser)
240250

@@ -250,7 +260,7 @@ async def test_configuration_filter_horizontal_vertical(objects, client):
250260
register_admin_model(admin_event_cls, [event.__class__])
251261
admin_event_cls.list_display = LIST_EVENT_FIELDS
252262
admin_event_cls.list_filter = LIST_EVENT_FIELDS
253-
admin_event_cls.filter_horizontal = ["participants"]
263+
admin_event_cls.filter_horizontal = ("participants",)
254264
r = await client.get(
255265
f"/api/configuration",
256266
)
@@ -268,6 +278,9 @@ async def test_configuration_filter_horizontal_vertical(objects, client):
268278
assert response_data
269279
validate_configuration_response_data(response_data)
270280

281+
admin_event_cls.list_display = ()
282+
admin_event_cls.list_filter = ()
283+
admin_event_cls.filter_horizontal = ()
271284
unregister_admin_model([event.__class__])
272285
await sign_out(client, superuser)
273286

@@ -283,7 +296,7 @@ async def test_configuration_raw_id_fields(objects, client):
283296
register_admin_model(admin_event_cls, [event.__class__])
284297
admin_event_cls.list_display = LIST_EVENT_FIELDS
285298
admin_event_cls.list_filter = LIST_EVENT_FIELDS
286-
admin_event_cls.raw_id_fields = ["participants", "tournament_id", "base_id"]
299+
admin_event_cls.raw_id_fields = ("participants", "tournament_id", "base_id")
287300
r = await client.get(
288301
f"/api/configuration",
289302
)
@@ -292,6 +305,9 @@ async def test_configuration_raw_id_fields(objects, client):
292305
assert response_data
293306
validate_configuration_response_data(response_data)
294307

308+
admin_event_cls.list_display = ()
309+
admin_event_cls.list_filter = ()
310+
admin_event_cls.raw_id_fields = ()
295311
unregister_admin_model([event.__class__])
296312
await sign_out(client, superuser)
297313

@@ -314,6 +330,7 @@ async def test_configuration_fields(objects, client):
314330
assert response_data
315331
validate_configuration_response_data(response_data)
316332

333+
admin_event_cls.fields = ()
317334
unregister_admin_model([event.__class__])
318335
await sign_out(client, superuser)
319336

@@ -327,7 +344,7 @@ async def test_configuration_actions(objects, client):
327344
await sign_in(client, superuser, admin_user_cls)
328345

329346
register_admin_model(admin_event_cls, [event.__class__])
330-
admin_event_cls.actions = ["make_is_active"]
347+
admin_event_cls.actions = ("make_is_active",)
331348
r = await client.get(
332349
f"/api/configuration",
333350
)
@@ -339,3 +356,42 @@ async def test_configuration_actions(objects, client):
339356
admin_event_cls.actions = ()
340357
unregister_admin_model([event.__class__])
341358
await sign_out(client, superuser)
359+
360+
361+
async def test_configuration_fieldsets(objects, client):
362+
superuser = objects["superuser"]
363+
event = objects["event"]
364+
admin_user_cls = objects["admin_user_cls"]
365+
admin_event_cls = objects["admin_event_cls"]
366+
367+
await sign_in(client, superuser, admin_user_cls)
368+
369+
register_admin_model(admin_event_cls, [event.__class__])
370+
admin_event_cls.fieldsets = [
371+
(None, {"fields": ("base_id", "name", "tournament_id", "participants")}),
372+
(
373+
"Types",
374+
{
375+
"classes": ("collapse",),
376+
"fields": ("is_active",),
377+
},
378+
),
379+
(
380+
"Geo",
381+
{
382+
"classes": ("collapse",),
383+
"fields": ("latitude", "longitude"),
384+
},
385+
),
386+
]
387+
r = await client.get(
388+
f"/api/configuration",
389+
)
390+
assert r.status_code == 200, r.text
391+
response_data = r.json()
392+
assert response_data
393+
validate_configuration_response_data(response_data)
394+
395+
admin_event_cls.fieldsets = ()
396+
unregister_admin_model([event.__class__])
397+
await sign_out(client, superuser)

0 commit comments

Comments
 (0)