Multi-Step Application (Wizard)¶
A job application in four steps: about you, the position, past jobs and a cover letter. It is one form and one POST; the wizard only shows it a step at a time.
models.py
from datetime import date
from typing import Literal
from pydantic import EmailStr
from pydantic_schemaforms import Field, FormModel
class Applicant(FormModel):
full_name: str = Field(title='Full name', min_length=2)
email: EmailStr = Field(title='Email')
phone: str = Field('', title='Phone', ui_element='tel')
class Position(FormModel):
role: Literal['Engineer', 'Designer', 'Product manager'] = Field(title='Role')
start_date: date = Field(title='Earliest start date')
remote: bool = Field(False, title='I want to work remotely')
class Job(FormModel):
employer: str = Field(title='Employer')
job_title: str = Field(title='Job title')
years: int = Field(1, title='Years', ge=0, le=50)
class Motivation(FormModel):
cover_letter: str = Field(
title='Why do you want to join?', min_length=50, ui_element='textarea'
)
consent: bool = Field(title='You may keep my application on file for 12 months')
class ApplicationForm(FormModel):
applicant: Applicant = Field(title='About you')
position: Position = Field(title='Position')
experience: list[Job] = Field(
default_factory=list,
title='Experience',
max_length=10,
ui_add_button_label='Add a job',
ui_item_title_template='{employer}',
)
motivation: Motivation = Field(title='Motivation')
main.py
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from models import ApplicationForm
from pydantic_schemaforms import html, render_form_html_async
app = FastAPI()
FORM_OPTIONS = {
'submit_url': '/apply',
'layout': 'wizard',
'wizard_options': {'next_label': 'Continue'},
'include_framework_assets': True,
'asset_mode': 'cdn',
}
@app.get('/apply', response_class=HTMLResponse)
async def show_application():
return await render_form_html_async(ApplicationForm, **FORM_OPTIONS)
@app.post('/apply', response_class=HTMLResponse)
async def apply(request: Request):
result = ApplicationForm.validate(await request.form(), **FORM_OPTIONS)
if not result.is_valid:
return await result.render_with_errors_async()
name = result.data['applicant']['full_name']
role = result.data['position']['role']
return html(t'<p>Thanks, {name}. We received your application for {role}.</p>')
main.py
from flask import Flask, request
from models import ApplicationForm
from pydantic_schemaforms import html, render_form_html
app = Flask(__name__)
FORM_OPTIONS = {
'submit_url': '/apply',
'layout': 'wizard',
'wizard_options': {'next_label': 'Continue'},
'include_framework_assets': True,
'asset_mode': 'cdn',
}
@app.get('/apply')
def show_application():
return render_form_html(ApplicationForm, **FORM_OPTIONS)
@app.post('/apply')
def apply():
result = ApplicationForm.validate(request.form, **FORM_OPTIONS)
if not result.is_valid:
return result.render_with_errors()
name = result.data['applicant']['full_name']
role = result.data['position']['role']
return html(t'<p>Thanks, {name}. We received your application for {role}.</p>')
How it works¶
- Each sub-form is a step, titled by its field's
title, in the order the fields are declared. The repeatingexperiencelist is a step of its own. layout="wizard"is passed tovalidate()too, so a failed submit comes back as a wizard, opened at the first step with an error.- Next checks the current step in the browser before moving on, and the server checks everything on submit.
wizard_optionsrenames the Next button here; it can also rename Back, hide the progress header or let users jump between steps. See Layouts.- The consent box has no default, so it's required: the form can't be sent without it.
Since no state is kept between steps, there is nothing to store in a session, and the browser's back button and a page reload behave like on any form.