Custom Validators in Flask-WTF
Custom validators are defined using a specific method naming convention: validate_fieldname(self, field). Within this method, access the form field's data via field.data. If the validation passes, the method should return normally. To indicate a validation failure, raise a wtforms.validators.ValidationError exception with a descriptive error message.
Example: Login Form with Captcha Validation
Template (login_template.html)
<!DOCTYPE html>
<html>
<head>
<title>User Login</title>
</head>
<body>
<form method="POST">
<div>
<label>Username:</label>
<input type="text" name="username">
</div>
<div>
<label>Password:</label>
<input type="password" name="password">
</div>
<div>
<label>Security Code ({{ captcha }}):</label>
<input type="text" name="captcha" maxlength="4">
</div>
<div>
<input type="submit" value="Log In">
</div>
</form>
</body>
</html>
Form Class (auth_forms.py)
from flask import session
from wtforms import Form, StringField
from wtforms.validators import Length, ValidationError
class LoginForm(Form):
captcha = StringField(validators=[Length(min=4, max=4)])
def validate_captcha(self, field):
"""
Custom validator to compare entered captcha with server session.
"""
user_input = field.data
server_stored = session.get('captcha_key', '')
if user_input != str(server_stored):
raise ValidationError('Security code mismatch. Please try again.')
Application Logic (app.py)
from flask import Flask, render_template, request, session
import secrets
from auth_forms import LoginForm
app = Flask(__name__)
app.config['SECRET_KEY'] = secrets.token_hex()
@app.route('/login', methods=['GET', 'POST'])
def user_login():
if request.method == 'GET':
session['captcha_key'] = secrets.randbelow(9000) + 1000
return render_template('login_template.html',
captcha=session['captcha_key'])
form_data = LoginForm(request.form)
if form_data.validate():
return 'Authentication successful.'
return f'Validation failed. Errors: {form_data.errors}'
if __name__ == '__main__':
app.run(debug=True)
File Upload Validation with Flask-WTF
To validate file uploads, use the FileField type from WTForms along with validators from flask_wtf.file. The FileRequired validator ensures a file is uploaded, while FileAllowed restricts allowed file extensions. In the view, merge request.form and request.files using CombinedMultiDict before validation.
Example: Profile Picture Upload
Project Structure
project/
├── static/uploads/
├── templates/
│ └── upload_form.html
├── app.py
└── forms.py
Upload Form Template (upload_form.html)
<!DOCTYPE html>
<html>
<head>
<title>Upload Profile Picture</title>
</head>
<body>
<form method="POST" enctype="multipart/form-data">
<div>
<label>Profile Image:</label>
<input type="file" name="profile_image">
</div>
<div>
<label>Description:</label>
<input type="text" name="image_description">
</div>
<div>
<input type="submit" value="Upload">
</div>
</form>
</body>
</html>
Form Definition (forms.py)
from wtforms import Form, FileField, StringField
from wtforms.validators import InputRequired
from flask_wtf.file import FileRequired, FileAllowed
class ImageUploadForm(Form):
profile_image = FileField(validators=[
FileRequired(),
FileAllowed(['jpg', 'jpeg', 'png', 'gif'], 'Image files only')
])
image_description = StringField(validators=[InputRequired()])
Application with Upload Handler (app.py)
import os
from flask import Flask, request, render_template
from werkzeug.datastructures import CombinedMultiDict
from werkzeug.utils import secure_filename
from forms import ImageUploadForm
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = os.path.join('static', 'uploads')
@app.route('/upload', methods=['GET', 'POST'])
def handle_upload():
if request.method == 'GET':
return render_template('upload_form.html')
merged_data = CombinedMultiDict([request.form, request.files])
upload_form = ImageUploadForm(merged_data)
if upload_form.validate():
description_text = upload_form.image_description.data
image_file = upload_form.profile_image.data
safe_filename = secure_filename(image_file.filename)
file_path = os.path.join(app.config['UPLOAD_FOLDER'], safe_filename)
image_file.save(file_path)
return 'File saved successfully.'
return f'Upload failed: {upload_form.errors}'
if __name__ == '__main__':
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
app.run()