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
8 changes: 6 additions & 2 deletions admin_ui/src/components/ChoiceSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,16 @@ export default defineComponent({
},
data() {
return {
localValue: "" as string | undefined
localValue: undefined as string | number | undefined
}
},
emits: ["updated"],
mounted() {
this.localValue = this.isFilter ? "all" : this.value
if (this.isFilter) {
this.localValue = "all"
} else if (this.value !== undefined) {
this.localValue = this.value
}
},
watch: {
value(newValue) {
Expand Down
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
7 changes: 7 additions & 0 deletions docs/source/custom_forms/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ 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

``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("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()
74 changes: 73 additions & 1 deletion 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 All @@ -14,7 +15,7 @@
from dataclasses import dataclass
from datetime import timedelta
from functools import partial
from typing import Any, Optional, TypeVar, Union
from typing import Any, Optional, TypeVar, Union, cast

import typing_extensions
from fastapi import FastAPI, File, Form, UploadFile
Expand Down Expand Up @@ -50,6 +51,7 @@
from piccolo_api.session_auth.middleware import SessionsAuthBackend
from piccolo_api.session_auth.tables import SessionsBase
from pydantic import BaseModel, Field, ValidationError
from pydantic_core import PydanticUndefined
from starlette.middleware import Middleware
from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.middleware.exceptions import HTTPException
Expand All @@ -63,6 +65,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 +407,75 @@ 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():
# Extract the actual type, handling Optional/Union
field_type = field_value.annotation
is_optional = False
assert field_type

# Check if it's Optional (Union with None)
if (
hasattr(field_type, "__origin__")
and field_type.__origin__ is Union
):
# Get all args from Union, filter out NoneType
type_args = [
arg for arg in field_type.__args__ if arg is not type(None)
]
if len(type_args) == 1:
field_type = type_args[0]
is_optional = True

if inspect.isclass(field_type) and issubclass(
field_type, enum.Enum
):
# Get the default value if it exists
default_value = None
if (
field_value.default is not None
and field_value.default is not PydanticUndefined
):
# If default is an Enum instance, get its value
if isinstance(field_value.default, enum.Enum):
default_value = field_value.default.value
else:
default_value = field_value.default

# Update model fields, field annotation and
# rebuild the model for the changes to take effect
json_schema_extra_dict: dict[str, Any] = {
"extra": {"choices": convert_enum_to_choices(field_type)}
}

# Add default value if it exists
if default_value is not None:
json_schema_extra_dict["extra"]["default"] = default_value

pydantic_model.model_fields[field_name] = Field( # type:ignore
default=default_value,
description=field_value.description,
title=field_value.title,
json_schema_extra=json_schema_extra_dict,
)

# Detect the enum value type (int, str, etc.)
enum_member = next(iter(field_type))
enum_value_type = type(enum_member.value)

# Set annotation to Optional[type] or type depending
# on original
new_annotation = (
enum_value_type | None if is_optional else enum_value_type
)

pydantic_model.model_fields[field_name].annotation = cast(
Any, new_annotation
)

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,
]
44 changes: 44 additions & 0 deletions piccolo_admin/example/forms/enum.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import enum
from typing import Optional

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 LogLevel(int, enum.Enum):
error = 2
warn = 3
info = 4
debug = 5


class NewStaffModel(BaseModel):
username: str
email: EmailStr
superuser: bool
permissions: Permission = Permission.gallery
log_level: Optional[LogLevel]


def new_staff_endpoint(request: Request, data: NewStaffModel) -> str:
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",
)
28 changes: 10 additions & 18 deletions piccolo_admin/translations/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -1279,8 +1279,7 @@
"Edit": "Szerkesztés",
"Export CSV": "CSV exportálása",
"Filter": "Szűrés",
"For timestamps which are timezone aware, they will be displayed in this timezone by default.":
"Az időzóna-érzékeny időbélyegek alapértelmezés szerint ebben az időzónában jelennek meg.",
"For timestamps which are timezone aware, they will be displayed in this timezone by default.": "Az időzóna-érzékeny időbélyegek alapértelmezés szerint ebben az időzónában jelennek meg.",
"Form submitted": "Űrlap elküldve",
"Forms": "Űrlapok",
"Go to page": "Ugrás az oldalra",
Expand All @@ -1300,10 +1299,8 @@
"New password": "Új jelszó",
"New value": "Új érték",
"No results found": "Nincs találat",
"Note: Large data sets may take a while.":
"Megjegyzés: Nagy adathalmazok feldolgozása eltarthat egy ideig.",
"Note: They are converted to UTC when stored in the database.":
"Megjegyzés: Az adatbázisban UTC időzónára vannak konvertálva.",
"Note: Large data sets may take a while.": "Megjegyzés: Nagy adathalmazok feldolgozása eltarthat egy ideig.",
"Note: They are converted to UTC when stored in the database.": "Megjegyzés: Az adatbázisban UTC időzónára vannak konvertálva.",
"of": "/",
"page": "oldal",
"Password": "Jelszó",
Expand All @@ -1313,8 +1310,7 @@
"Seconds": "Másodpercek",
"Select a column to update": "Válasszon egy frissítendő oszlopot",
"Select a Column": "Oszlop kiválasztása",
"Select a table in the sidebar to get started.":
"A kezdéshez válasszon egy táblát az oldalsávban.",
"Select a table in the sidebar to get started.": "A kezdéshez válasszon egy táblát az oldalsávban.",
"selected result(s) on": "kiválasztott találat itt:",
"Semicolon": "Pontosvessző",
"Set Timezone": "Időzóna beállítása",
Expand All @@ -1334,7 +1330,7 @@
"Weeks": "Hetek",
"Welcome to": "Üdvözöljük itt:",
"with a matching": "egyező értékkel",
}
},
)

SLOVAK = Translation(
Expand Down Expand Up @@ -1365,8 +1361,7 @@
"Edit": "Upraviť",
"Export CSV": "Exportovať CSV",
"Filter": "Filter",
"For timestamps which are timezone aware, they will be displayed in this timezone by default.":
"Časové značky s informáciou o časovom pásme sa predvolene zobrazujú v tomto časovom pásme.",
"For timestamps which are timezone aware, they will be displayed in this timezone by default.": "Časové značky s informáciou o časovom pásme sa predvolene zobrazujú v tomto časovom pásme.",
"Form submitted": "Formulár odoslaný",
"Forms": "Formuláre",
"Go to page": "Prejsť na stránku",
Expand All @@ -1386,10 +1381,8 @@
"New password": "Nové heslo",
"New value": "Nová hodnota",
"No results found": "Neboli nájdené žiadne výsledky",
"Note: Large data sets may take a while.":
"Poznámka: Spracovanie veľkých dátových súborov môže chvíľu trvať.",
"Note: They are converted to UTC when stored in the database.":
"Poznámka: Pri ukladaní do databázy sa konvertujú na UTC.",
"Note: Large data sets may take a while.": "Poznámka: Spracovanie veľkých dátových súborov môže chvíľu trvať.",
"Note: They are converted to UTC when stored in the database.": "Poznámka: Pri ukladaní do databázy sa konvertujú na UTC.",
"of": "z",
"page": "strana",
"Password": "Heslo",
Expand All @@ -1399,8 +1392,7 @@
"Seconds": "Sekundy",
"Select a column to update": "Vyberte stĺpec na aktualizáciu",
"Select a Column": "Vybrať stĺpec",
"Select a table in the sidebar to get started.":
"Ak chcete začať, vyberte tabuľku na bočnom paneli.",
"Select a table in the sidebar to get started.": "Ak chcete začať, vyberte tabuľku na bočnom paneli.",
"selected result(s) on": "vybrané výsledky na",
"Semicolon": "Bodkočiarka",
"Set Timezone": "Nastaviť časové pásmo",
Expand All @@ -1420,7 +1412,7 @@
"Weeks": "Týždne",
"Welcome to": "Vitajte v",
"with a matching": "so zhodou",
}
},
)

TRANSLATIONS: list[Translation] = [
Expand Down
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.replace("_", " "),
"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