Custom Forms¶
Piccolo Admin lets you easily add custom forms, which can be useful for many different purposes e.g. running tasks and downloading reports.
Here’s an example booking form:
All that is required is a Pydantic model, and an endpoint, and Piccolo Admin will generate all of the UI for you, without you having to write any frontend code.
Endpoint¶
The endpoint is a sync or async function that either returns a string, or a
FileResponse.
string¶
If a string is returned, it is shown to the user once the form has been submitted.
Here’s a really simple example:
from pydantic import BaseModel
from starlette.requests import Request
from piccolo_admin.endpoints import FormConfig
class CalculatorModel(BaseModel):
number_1: int
number_2: int
def calculator(request: Request, data: CalculatorModel):
"""
A very simple example of a form which adds numbers together.
"""
return f"The answer is {data.number_1 + data.number_2}."
FORM = FormConfig(
name="Calculator",
pydantic_model=CalculatorModel,
endpoint=calculator,
description=("Adds two numbers together."),
)
Here’s a more advanced example where we send an email, then return a string:
import datetime
import smtplib
from pydantic import BaseModel, EmailStr
from starlette.requests import Request
from piccolo_admin.endpoints import FormConfig
class BookingModel(BaseModel):
movie: str = "Star Wars: Episode IV - A New Hope"
email: EmailStr
name: str
tickets: int
starts_at: datetime.datetime
notes: str = "N/A"
def booking_endpoint(request: Request, data: BookingModel) -> str:
"""
An example form function which sends an email.
"""
sender = "info@example.com"
receivers = [data.email]
message = f"""From: Bookings <info@example.com>
To: Customer <{data.email}>
Subject: {data.name} booked {data.tickets} ticket(s) for {data.starts_at}.
{data.notes}
"""
try:
smtpObj = smtplib.SMTP("localhost:1025")
smtpObj.sendmail(sender, receivers, message)
print("Successfully sent email")
except (smtplib.SMTPException, ConnectionRefusedError):
print("Error: unable to send email")
return "Booking complete"
FORM = FormConfig(
name="Booking form",
pydantic_model=BookingModel,
endpoint=booking_endpoint,
description="Make a booking for a customer.",
form_group="Text forms",
)
FileResponse¶
This allows you to return a file to the user - for example, an image or CSV file.
Hint
This is intended for small to medium sized files only (i.e. no 1 GB video files!). We will add support for large files in the future.
CSV example¶
import csv
import io
from pydantic import BaseModel, field_validator
from starlette.requests import Request
from piccolo_admin.endpoints import FileResponse, FormConfig
from piccolo_admin.example.tables import Movie
class DownloadMoviesModel(BaseModel):
director_name: str
@field_validator("director_name")
def validate_director_name(cls, v):
if v == "":
raise ValueError("The value can't be empty.")
return v
async def download_movies(
request: Request, data: DownloadMoviesModel
) -> FileResponse:
"""
An example custom form function which downloads a CSV file.
"""
movies = await Movie.select(Movie.name, Movie.release_date).where(
Movie.director._.name == data.director_name
)
output_file = io.StringIO(None)
writer = csv.DictWriter(
f=output_file,
fieldnames=["name", "release_date"],
)
writer.writeheader()
writer.writerows(movies)
return FileResponse(
contents=output_file,
file_name="director_movies.csv",
media_type="text/csv",
)
FORM = FormConfig(
name="Download director movies",
pydantic_model=DownloadMoviesModel,
endpoint=download_movies,
description="Download a list of movies for the director as a CSV file.",
form_group="Download forms",
)
Image example¶
import datetime
import io
import os
import shutil
from pydantic import BaseModel
from starlette.requests import Request
from piccolo_admin.endpoints import FileResponse, FormConfig
class DownloadScheduleModel(BaseModel):
date: datetime.date
def download_schedule(
request: Request, data: DownloadScheduleModel
) -> FileResponse:
"""
An example form function which downloads an image file.
"""
file_name = "movie_listings.jpg"
with open(
os.path.join(os.path.dirname(__file__), "files", file_name),
"rb",
) as f:
output_file = io.BytesIO()
shutil.copyfileobj(f, output_file)
return FileResponse(
contents=output_file,
file_name=file_name,
media_type="image/jpeg",
)
FORM = FormConfig(
name="Download schedule",
pydantic_model=DownloadScheduleModel,
endpoint=download_schedule,
description=("Download the schedule for the day."),
form_group="Download forms",
)
Full Example¶
Here’s a full example of how to integrate a form with an ASGI framework like FastAPI:
from fastapi import FastAPI
from fastapi.routing import Mount
from pydantic import BaseModel
from starlette.requests import Request
from piccolo_admin.endpoints import FormConfig, create_admin
# Pydantic model for the form
class CalculatorModel(BaseModel):
number_1: int
number_2: int
# Endpoint for handling the form
def calculator(request: Request, data: CalculatorModel):
"""
A very simple example of a form which adds numbers together.
"""
return f"The answer is {data.number_1 + data.number_2}."
FORM = FormConfig(
name="Calculator",
pydantic_model=CalculatorModel,
endpoint=calculator,
description=("Adds two numbers together."),
form_group="Text forms",
)
app = FastAPI(
routes=[
Mount(
"/admin/",
create_admin(forms=[FORM]),
),
],
)
# For Starlette it is identical, just `app = Starlette(...)`
Source¶
- class piccolo_admin.endpoints.FormConfig(name: str, pydantic_model: type[PydanticModel], endpoint: Callable[[Request, PydanticModel], str | FileResponse | None | Coroutine[None, None, str | FileResponse | None]], description: str | None = None, form_group: str | None = None)[source]¶
Used to specify forms, which are passed into
create_admin.- Parameters:
name – This will be displayed in the UI in the sidebar.
pydantic_model – This determines which fields to display in the form, and is used to deserialise the responses.
endpoint – Your custom handler, which accepts two arguments - the FastAPI / Starlette request object, in case you want to access the cookies / headers / logged in user (via request.user). And secondly an instance of the Pydantic model. If it returns a string, it will be shown to the user in the UI as the success message. For example
'Successfully sent email'. The endpoint can be a normal function or async function.description – An optional description which is shown in the UI to explain to the user what the form is for.
form_group – If specified, forms can be divided into groups in the form menu. This is useful when you have many forms that you can organize into groups for better visibility.
Here’s a full example:
class MyModel(pydantic.BaseModel): message: str = "hello world" def my_endpoint(request: Request, data: MyModel): print(f"I received {data.message}") # If we're not happy with the data raise a ValueError # The message inside the exception will be displayed in the UI. raise ValueError("We were unable to process the form.") # If we're happy with the data, just return a string, which # will be displayed in the UI. return "Successful." config = FormConfig( name="My Form", pydantic_model=MyModel, endpoint=my_endpoint, form_group="Text forms", )