diff --git a/admin_ui/src/components/NewForm.vue b/admin_ui/src/components/NewForm.vue index d31a7703..0128faae 100644 --- a/admin_ui/src/components/NewForm.vue +++ b/admin_ui/src/components/NewForm.vue @@ -12,6 +12,7 @@ v-bind:type="getType(property)" v-bind:value="property.default" v-bind:isNullable="isNullable(property)" + v-bind:choices="property.extra?.choices" v-bind:timeResolution=" schema?.extra?.time_resolution[columnName] " diff --git a/docs/source/custom_forms/index.rst b/docs/source/custom_forms/index.rst index 60489e2e..09e67f1c 100644 --- a/docs/source/custom_forms/index.rst +++ b/docs/source/custom_forms/index.rst @@ -39,6 +39,16 @@ Here's a more advanced example where we send an email, then return a string: .. literalinclude:: ../../../piccolo_admin/example/forms/email.py +``Enum`` +-------- + +Custom forms support ``Enum`` type. Here's a example: + +.. literalinclude:: ../../../piccolo_admin/example/forms/enum.py + +.. warning:: + We need to do the ``Enum`` type conversion from the form data ourselves as a result of how this feature is implemented. If you don't do this conversion, then the field with be provided as a ``str`` instead of the ``Enum``. + ``FileResponse`` ---------------- diff --git a/e2e/test_forms.py b/e2e/test_forms.py index 9af3cd92..9d265390 100644 --- a/e2e/test_forms.py +++ b/e2e/test_forms.py @@ -1,6 +1,7 @@ from playwright.sync_api import Page from piccolo_admin.example.forms.csv import FORM as CSV_FORM +from piccolo_admin.example.forms.enum import FORM as ENUM_FORM from piccolo_admin.example.forms.image import FORM as IMAGE_FORM from piccolo_admin.example.forms.nullable import FORM as NULLABLE_FORM @@ -79,3 +80,28 @@ def test_nullable_form(page: Page, dev_server): and response.status == 200 ): form_page.submit_form() + + +def test_form_enum(page: Page, dev_server): + """ + Make sure custom forms support the usage of Enum's. + """ + login_page = LoginPage(page=page) + login_page.reset() + login_page.login() + + form_page = FormPage( + page=page, + form_slug=ENUM_FORM.slug, + ) + form_page.reset() + page.locator('input[name="username"]').fill("piccolo") + page.locator('input[name="email"]').fill("piccolo@example.com") + page.locator('select[name="permissions"]').select_option("admissions") + + with page.expect_response( + lambda response: response.url == f"{BASE_URL}/api/forms/enum-form/" + and response.request.method == "POST" + and response.status == 200 + ): + form_page.submit_form() diff --git a/piccolo_admin/endpoints.py b/piccolo_admin/endpoints.py index 8bdbdec3..fdeeebb8 100644 --- a/piccolo_admin/endpoints.py +++ b/piccolo_admin/endpoints.py @@ -4,6 +4,7 @@ from __future__ import annotations +import enum import inspect import io import itertools @@ -63,6 +64,7 @@ TranslationListItem, TranslationListResponse, ) +from .utils import convert_enum_to_choices from .version import __VERSION__ as PICCOLO_ADMIN_VERSION logger = logging.getLogger(__name__) @@ -404,6 +406,26 @@ def __init__( self.description = description self.form_group = form_group self.slug = self.name.replace(" ", "-").lower() + for ( + field_name, + field_value, + ) in self.pydantic_model.model_fields.items(): + if inspect.isclass(field_value.annotation) and issubclass( + field_value.annotation, enum.Enum + ): + # update model fields, field annotation and + # rebuild the model for the changes to take effect + pydantic_model.model_fields[field_name] = Field( + json_schema_extra={ + "extra": { + "choices": convert_enum_to_choices( + field_value.annotation + ) + } + }, + ) + pydantic_model.model_fields[field_name].annotation = str + pydantic_model.model_rebuild(force=True) class FormConfigResponseModel(BaseModel): diff --git a/piccolo_admin/example/forms/__init__.py b/piccolo_admin/example/forms/__init__.py index 6cf310ae..f009d574 100644 --- a/piccolo_admin/example/forms/__init__.py +++ b/piccolo_admin/example/forms/__init__.py @@ -1,6 +1,7 @@ from .calculator import FORM as CALCULATOR_FORM from .csv import FORM as CSV_FORM from .email import FORM as EMAIL_FORM +from .enum import FORM as ENUM_FORM from .image import FORM as IMAGE_FORM from .nullable import FORM as MEGA_FORM @@ -10,4 +11,5 @@ EMAIL_FORM, IMAGE_FORM, MEGA_FORM, + ENUM_FORM, ] diff --git a/piccolo_admin/example/forms/enum.py b/piccolo_admin/example/forms/enum.py new file mode 100644 index 00000000..52bb9ed8 --- /dev/null +++ b/piccolo_admin/example/forms/enum.py @@ -0,0 +1,38 @@ +import enum + +from pydantic import BaseModel, EmailStr +from starlette.requests import Request + +from piccolo_admin.endpoints import FormConfig + + +# An example of using Python enum in custom forms +class Permission(str, enum.Enum): + admissions = "admissions" + gallery = "gallery" + notices = "notices" + uploads = "uploads" + + +class NewStaffModel(BaseModel): + username: str + email: EmailStr + superuser: bool + permissions: Permission + + +def new_staff_endpoint(request: Request, data: NewStaffModel) -> str: + # We need to do the enum type conversion ourselves like this: + # data.permissions = Permission(int(data.permissions)) # for int enum + # data.permissions = Permission(data.permissions) # for str enum + print(data) + return "A new staff member has been successfully created." + + +FORM = FormConfig( + name="Enum form", + pydantic_model=NewStaffModel, + endpoint=new_staff_endpoint, + description="Make a enum form.", + form_group="Text forms", +) diff --git a/piccolo_admin/utils.py b/piccolo_admin/utils.py new file mode 100644 index 00000000..b972cc62 --- /dev/null +++ b/piccolo_admin/utils.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from typing import Any + + +def convert_enum_to_choices(enum_data: Any) -> dict[str, Any]: + choices = {} + for item in enum_data: + choices[item.name] = { + "display_name": item.name, + "value": item.value, + } + return choices diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index 6eadb312..b76fdee8 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -311,6 +311,11 @@ def test_forms(self): "name": "Nullable fields", "slug": "nullable-fields", }, + { + "name": "Enum form", + "slug": "enum-form", + "description": "Make a enum form.", + }, ], ) @@ -526,7 +531,12 @@ def test_forms_grouped(self): "description": "Make a booking for a customer.", "name": "Booking form", "slug": "booking-form", - } + }, + { + "name": "Enum form", + "slug": "enum-form", + "description": "Make a enum form.", + }, ], "Test forms": [ {