Files
awesome-copilot/skills/create-web-form/references/python-flask.md
jhauga 27149859a4 Add skill to create web forms
Add skill to create web forms
2026-02-09 19:22:54 -05:00

433 lines
12 KiB
Markdown

# Python Flask Forms Reference
> Source: <https://testdriven.io/courses/learn-flask/forms/>
This reference covers how to work with forms in Flask, including handling GET and POST requests, using WTForms for form creation and validation, implementing CSRF protection, and managing file uploads.
---
## Overview
Flask provides tools for handling web forms through:
- The `request` object for accessing submitted form data
- **Flask-WTF** and **WTForms** for declarative form creation, validation, and CSRF protection
- Jinja2 templates for rendering form HTML
- Flash messages for user feedback
---
## Basic Form Handling in Flask
### Handling GET and POST Requests
```python
from flask import Flask, render_template, request, redirect, url_for, flash
app = Flask(__name__)
app.secret_key = 'your-secret-key'
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if username == 'admin' and password == 'secret':
flash('Login successful!', 'success')
return redirect(url_for('dashboard'))
else:
flash('Invalid credentials.', 'error')
return render_template('login.html')
```
### The `request.form` Object
The `request.form` is an `ImmutableMultiDict` that contains parsed form data from POST and PUT requests.
| Method | Description |
|--------|-------------|
| `request.form['key']` | Access a value; raises `400 Bad Request` if missing |
| `request.form.get('key')` | Access a value; returns `None` if missing |
| `request.form.get('key', 'default')` | Access a value with a fallback default |
| `request.form.getlist('key')` | Returns a list of all values for a key (for multi-select fields) |
### The `request.method` Attribute
Used to distinguish between GET (displaying the form) and POST (processing the submission):
```python
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
# Process the form submission
pass
# GET: Display the form
return render_template('register.html')
```
---
## Flask-WTF and WTForms
### Installation
```bash
pip install Flask-WTF
```
Flask-WTF is a Flask extension that integrates WTForms. It provides:
- CSRF protection out of the box
- Integration with Flask's `request` object
- Jinja2 template helpers
- File upload support
### Configuration
```python
from flask import Flask
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key' # Required for CSRF
app.config['WTF_CSRF_ENABLED'] = True # Enabled by default
```
---
## Defining Forms with WTForms
### Basic Form Class
```python
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Email, Length, EqualTo
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[
DataRequired(),
Length(min=3, max=25)
])
email = StringField('Email', validators=[
DataRequired(),
Email()
])
password = PasswordField('Password', validators=[
DataRequired(),
Length(min=6)
])
confirm_password = PasswordField('Confirm Password', validators=[
DataRequired(),
EqualTo('password', message='Passwords must match.')
])
submit = SubmitField('Register')
```
### Common Field Types
| Field Type | Description |
|-----------|-------------|
| `StringField` | Single-line text input |
| `PasswordField` | Password input (masked characters) |
| `TextAreaField` | Multi-line text input |
| `IntegerField` | Integer input with built-in type coercion |
| `FloatField` | Float input with built-in type coercion |
| `BooleanField` | Checkbox (True/False) |
| `SelectField` | Dropdown select menu |
| `SelectMultipleField` | Multiple-select dropdown |
| `RadioField` | Radio button group |
| `FileField` | File upload input |
| `HiddenField` | Hidden input field |
| `SubmitField` | Submit button |
| `DateField` | Date picker input |
### Common Validators
| Validator | Description |
|-----------|-------------|
| `DataRequired()` | Field must not be empty |
| `Email()` | Must be a valid email format |
| `Length(min, max)` | String length must fall within range |
| `EqualTo('field')` | Must match another field's value |
| `NumberRange(min, max)` | Numeric value must fall within range |
| `Regexp(regex)` | Must match the provided regular expression |
| `URL()` | Must be a valid URL |
| `Optional()` | Field is allowed to be empty |
| `InputRequired()` | Raw input data must be present |
| `AnyOf(values)` | Must be one of the provided values |
| `NoneOf(values)` | Must not be any of the provided values |
---
## Using Forms in Routes
### Route with WTForms
```python
@app.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
# form.validate_on_submit() checks:
# 1. Is the request method POST?
# 2. Does the form pass all validation?
# 3. Is the CSRF token valid?
username = form.username.data
email = form.email.data
password = form.password.data
# Process the data (e.g., save to database)
flash(f'Account created for {username}!', 'success')
return redirect(url_for('login'))
return render_template('register.html', form=form)
```
### `validate_on_submit()` Method
This method combines two checks:
1. `request.method == 'POST'` -- ensures the form was actually submitted
2. `form.validate()` -- runs all validators on the form fields and checks the CSRF token
Returns `True` only if both conditions are met.
---
## Rendering Forms in Templates
### Basic Template Rendering
```html
<form method="POST" action="{{ url_for('register') }}">
{{ form.hidden_tag() }}
<div>
{{ form.username.label }}
{{ form.username(class="form-control", placeholder="Enter username") }}
{% for error in form.username.errors %}
<span class="error">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.email.label }}
{{ form.email(class="form-control", placeholder="Enter email") }}
{% for error in form.email.errors %}
<span class="error">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.password.label }}
{{ form.password(class="form-control") }}
{% for error in form.password.errors %}
<span class="error">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.confirm_password.label }}
{{ form.confirm_password(class="form-control") }}
{% for error in form.confirm_password.errors %}
<span class="error">{{ error }}</span>
{% endfor %}
</div>
{{ form.submit(class="btn btn-primary") }}
</form>
```
### Key Template Elements
| Element | Purpose |
|---------|---------|
| `{{ form.hidden_tag() }}` | Renders the hidden CSRF token field |
| `{{ form.field.label }}` | Renders the `<label>` element for the field |
| `{{ form.field() }}` | Renders the `<input>` element for the field |
| `{{ form.field(class="...") }}` | Renders the input with additional HTML attributes |
| `{{ form.field.errors }}` | List of validation error messages for the field |
| `{{ form.field.data }}` | The current value of the field |
---
## CSRF Protection
### How CSRF Protection Works
Flask-WTF automatically includes CSRF protection:
1. A unique token is generated per session and embedded as a hidden form field.
2. When the form is submitted, Flask-WTF verifies the token matches the session token.
3. If the token is missing or invalid, the request is rejected with a `400 Bad Request`.
### Including the CSRF Token
In your template, always include one of these:
```html
<!-- Option 1: Hidden tag (includes CSRF + all hidden fields) -->
{{ form.hidden_tag() }}
<!-- Option 2: CSRF token only -->
{{ form.csrf_token }}
```
### CSRF for AJAX Requests
For JavaScript-based form submissions:
```html
<meta name="csrf-token" content="{{ csrf_token() }}">
```
```javascript
fetch('/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify(data)
});
```
---
## Custom Validators
### Inline Custom Validator
Define a method on the form class with the naming pattern `validate_<fieldname>`:
```python
from wtforms import ValidationError
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
email = StringField('Email', validators=[DataRequired(), Email()])
def validate_username(self, field):
if field.data.lower() in ['admin', 'root', 'superuser']:
raise ValidationError('That username is reserved.')
def validate_email(self, field):
# Check if email already exists in database
if User.query.filter_by(email=field.data).first():
raise ValidationError('That email is already registered.')
```
### Reusable Custom Validator
```python
from wtforms import ValidationError
def validate_no_special_chars(form, field):
if not field.data.isalnum():
raise ValidationError('Field must contain only letters and numbers.')
class MyForm(FlaskForm):
username = StringField('Username', validators=[
DataRequired(),
validate_no_special_chars
])
```
---
## File Uploads
### Form with File Upload
```python
from flask_wtf.file import FileField, FileAllowed, FileRequired
class UploadForm(FlaskForm):
photo = FileField('Profile Photo', validators=[
FileRequired(),
FileAllowed(['jpg', 'png', 'gif'], 'Images only!')
])
submit = SubmitField('Upload')
```
### Handling File Uploads in Routes
```python
import os
from werkzeug.utils import secure_filename
UPLOAD_FOLDER = 'static/uploads'
@app.route('/upload', methods=['GET', 'POST'])
def upload():
form = UploadForm()
if form.validate_on_submit():
file = form.photo.data
filename = secure_filename(file.filename)
filepath = os.path.join(UPLOAD_FOLDER, filename)
file.save(filepath)
flash('File uploaded successfully!', 'success')
return redirect(url_for('upload'))
return render_template('upload.html', form=form)
```
### Multipart Form Encoding
File upload forms must use `enctype="multipart/form-data"`:
```html
<form method="POST" enctype="multipart/form-data">
{{ form.hidden_tag() }}
{{ form.photo.label }}
{{ form.photo() }}
{{ form.submit() }}
</form>
```
---
## Flash Messages
### Setting Flash Messages
```python
from flask import flash
flash('Operation successful!', 'success')
flash('An error occurred.', 'error')
flash('Please check your input.', 'warning')
```
### Displaying Flash Messages in Templates
```html
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
```
---
## Key Takeaways
1. **Use Flask-WTF** for form handling -- it provides CSRF protection, validation, and clean form definitions.
2. **`validate_on_submit()`** is the primary method for checking both submission and validation in one call.
3. **Always include `{{ form.hidden_tag() }}`** in templates to enable CSRF protection.
4. **Use WTForms validators** for clean, declarative server-side validation.
5. **Custom validators** can be defined inline on the form class or as reusable functions.
6. **Flash messages** provide user feedback for form actions.
7. **File uploads** require `enctype="multipart/form-data"` and should use `secure_filename()` for safety.
8. **Set a `SECRET_KEY`** in your Flask config -- it is required for CSRF tokens and session management.