This is a code snippet demonstrating using WTForms without Flask-WTF in a Flask project.
I found the extension: Flask-WTF problematic. So I decided to limit the number of dependencies and take control of this part of the project.
First define a base form to get the following features:
- (csrf) cross-site request forgery protection
- convenience method
validate_on_submit()
from datetime import timedelta
from flask import current_app, request, session
from wtforms import Form
from wtforms.csrf.session import SessionCSRF
class BaseForm(Form):
class Meta:
csrf = True
csrf_class = SessionCSRF
csrf_time_limit = timedelta(minutes=15)
@property
def csrf_context(self):
# https://wtforms.readthedocs.io/en/2.3.x/csrf/?highlight=csrf#example-integration
return session
@property
def csrf_secret(self):
csrf_secret_key = current_app.config['CSRF_SECRET_KEY']
if isinstance(csrf_secret_key, str):
return bytes(csrf_secret_key, encoding='utf-8')
else:
return csrf_secret_key
def validate_on_submit(self):
return request.method == 'POST' and self.validate()
Then, define a form:
from app.forms import BaseForm # See above
from wtforms import StringField, SubmitField, PasswordField, validators
PASSWORD_LENGTH_MIN = 12
PASSWORD_LENGHT_MAX = 30
class UserRegistrationForm(BaseForm):
first_name = StringField('First Name', [
validators.DataRequired(),
validators.InputRequired(message='First name is required'),
])
last_name = StringField('Last Name', [
validators.DataRequired(),
validators.InputRequired(message='Last name is required'),
])
email = StringField('Email Address', [
validators.DataRequired(),
validators.InputRequired(message='Email is required'),
validators.Email(message='Email is invalid'),
])
email_confirm = StringField('Confirm Email Address', [
validators.DataRequired(),
validators.InputRequired(message='Email confirmation is required'),
validators.EqualTo('email', message='Emails must match'),
])
password = PasswordField('Password', [
validators.DataRequired(),
validators.InputRequired(message='Password is required'),
validators.Length(
min=PASSWORD_LENGTH_MIN,
max=PASSWORD_LENGHT_MAX,
message='Password length must be between {min} and {max} characters in length'.format(
min=PASSWORD_LENGTH_MIN,
max=PASSWORD_LENGHT_MAX,
)
),
])
submit = SubmitField('Register')
Then, define a flask endpoint:
from app.forms import UserRegistrationForm
from app.models.base import db
from app.services.users import create_user
from app.www import blueprint
from flask import flash, redirect, render_template, request, url_for
@blueprint.route('/register', methods=['GET', 'POST'])
def register():
form = UserRegistrationForm(request.form)
if form.validate_on_submit():
user = create_user(
session=db.session,
first_name=form.first_name.data,
last_name=form.last_name.data,
email=form.email.data,
password=form.password.data)
db.session.commit()
flash('Thanks for registering')
return redirect(url_for('www.login'))
elif form.csrf_token.errors:
# TODO: Log suspected attack
pass
return render_template('register.html', form=form)
And finally, define the template:
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Register{% endblock %}</h1>
{% endblock %}
{% block content %}
{% if form.errors %}
<ul class="errors">
{% for field_name, field_errors in form.errors|dictsort if field_errors %}
{% for error in field_errors %}
<li>{{ form[field_name].label }}: {{ error }}</li>
{% endfor %}
{% endfor %}
{% if form.csrf_token.errors %}
<li>You have submitted an invalid CSRF token</li>
{%endif%}
</ul>
{% endif %}
<form method="post" action="{{ url_for('www.register') }}">
{% for field in form %}
{{ field.label }}
{{ field }}
{% endfor %}
</form>
{% endblock %}