Order with Line Items¶
An order with any number of lines, each with a product, a quantity and a unit price, and a total worked out on the server.
models.py
from datetime import date
from decimal import Decimal
from pydantic import computed_field
from pydantic_schemaforms import Field, FormModel
class LineItem(FormModel):
product: str = Field(title='Product', min_length=2)
quantity: int = Field(1, title='Quantity', ge=1, le=999)
unit_price: Decimal = Field(title='Unit price', ge=0, decimal_places=2, ui_element='currency')
@computed_field
@property
def total(self) -> Decimal:
return self.quantity * self.unit_price
class OrderForm(FormModel):
customer: str = Field(title='Customer', ui_autofocus=True)
delivery_date: date = Field(title='Delivery date')
items: list[LineItem] = Field(
default_factory=list,
title='Line items',
min_length=1,
max_length=50,
ui_add_button_label='Add a line',
ui_item_title_template='{product}',
)
notes: str = Field('', title='Notes', ui_element='textarea')
main.py
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from models import OrderForm
from pydantic_schemaforms import html, render_form_html_async
app = FastAPI()
FORM_OPTIONS = {'submit_url': '/orders/new', 'include_framework_assets': True, 'asset_mode': 'cdn'}
@app.get('/orders/new', response_class=HTMLResponse)
async def new_order():
return await render_form_html_async(OrderForm, **FORM_OPTIONS)
@app.post('/orders/new', response_class=HTMLResponse)
async def create_order(request: Request):
result = OrderForm.validate(await request.form(), **FORM_OPTIONS)
if not result.is_valid:
return await result.render_with_errors_async()
order = result.instance
lines = len(order.items)
total = sum(item.total for item in order.items)
return html(t'<p>Order for {order.customer}: {lines} lines, {total:.2f} in total.</p>')
main.py
from flask import Flask, request
from models import OrderForm
from pydantic_schemaforms import html, render_form_html
app = Flask(__name__)
FORM_OPTIONS = {'submit_url': '/orders/new', 'include_framework_assets': True, 'asset_mode': 'cdn'}
@app.get('/orders/new')
def new_order():
return render_form_html(OrderForm, **FORM_OPTIONS)
@app.post('/orders/new')
def create_order():
result = OrderForm.validate(request.form, **FORM_OPTIONS)
if not result.is_valid:
return result.render_with_errors()
order = result.instance
lines = len(order.items)
total = sum(item.total for item in order.items)
return html(t'<p>Order for {order.customer}: {lines} lines, {total:.2f} in total.</p>')
How it works¶
list[LineItem]is a repeating sub-form with an "Add a line" button.min_length=1means the form starts with one empty line and can't be sent with none;max_length=50caps it.ui_item_title_template="{product}"titles each line's card with its product as the user types it.ui_element="currency"shows the price as$1,012.50while the user types. The symbol and separators are removed before validation, so the field is still aDecimal.@computed_fieldtotalisn't an input: it's worked out from the validated line, so the route never trusts a total sent by the browser.- Errors name the line. A bad price on the second line shows under that input, and in the summary as "Item #2 — Unit Price: ...".