Skip to content
Open
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
1 change: 1 addition & 0 deletions admin_ui/src/components/NewForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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]
"
Expand Down
10 changes: 10 additions & 0 deletions docs/source/custom_forms/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``
----------------

Expand Down
26 changes: 26 additions & 0 deletions e2e/test_forms.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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("[email protected]")
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()
22 changes: 22 additions & 0 deletions piccolo_admin/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from __future__ import annotations

import enum
import inspect
import io
import itertools
Expand Down Expand Up @@ -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__)
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

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

Can't we just leave the annotation as what it was originally?

Copy link
Member Author

Choose a reason for hiding this comment

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

We need to reassign the annotation type because if we don't, that annotation is None and that causes a form validation error. I'm using the string type because if we reassign annotation to the Enum, frontend don't work (working with strings because choices display_value and value are strings, and choices does not work with Enum). In the whole PR I tried to use Piccolo choices so that there would be as few changes as possible on the frontend. If you don't like the whole concept, feel free to delete this PR.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, yes - I forgot that the front end would get confused by the open api schema is we leave the enum in there.

pydantic_model.model_rebuild(force=True)


class FormConfigResponseModel(BaseModel):
Expand Down
2 changes: 2 additions & 0 deletions piccolo_admin/example/forms/__init__.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -10,4 +11,5 @@
EMAIL_FORM,
IMAGE_FORM,
MEGA_FORM,
ENUM_FORM,
]
38 changes: 38 additions & 0 deletions piccolo_admin/example/forms/enum.py
Original file line number Diff line number Diff line change
@@ -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",
)
13 changes: 13 additions & 0 deletions piccolo_admin/utils.py
Original file line number Diff line number Diff line change
@@ -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
12 changes: 11 additions & 1 deletion tests/test_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
},
],
)

Expand Down Expand Up @@ -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": [
{
Expand Down
Loading