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:

../_images/forms_sidebar.png ../_images/form.png

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",
)
class piccolo_admin.endpoints.FileResponse(contents: 'Union[io.StringIO, io.BytesIO]', file_name: 'str', media_type: 'str')[source]