Web Technologies/2021-2022/Laboratory 13

Form validation using WTForms

edit

Validating form is going to be one of the most essential steps when building web applications.

Today we will see an approach of using WTFormsand Flask-login in order to implement a register/login mechanism.

We will start off with creating a simple User model:

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(25))
    email = db.Column(db.String(35))
    password = db.Column(db.String(120))

    def __init__(self, username, email, password):
        self.username = username
        self.email = email
        self.password = password

Then we will create two classes for our Registration and Login forms. WTForms lets us define fields such as StringField, PasswordField together with validators such as Length, Email, DataRequired:

class RegistrationForm(FlaskForm):
    username = StringField('Username', [validators.Length(min=4, max=25)])
    email = StringField('Email Address', [validators.Length(min=6, max=35), validators.Email()])
    password = PasswordField('New Password', [
        validators.DataRequired(),
        validators.EqualTo('confirm', message='Passwords must match')
    ])
    confirm = PasswordField('Repeat Password')


class LoginForm(FlaskForm):
    email = StringField('Email',
                        validators=[validators.DataRequired(),
                                    validators.Length(1, 64),
                                    validators.Email()])
    password = PasswordField('Password', validators=[validators.DataRequired()])
    submit = SubmitField('Log In')
    

    def validate(self, extra_validators):
        initial_validation = super(LoginForm, self).validate()
        if not initial_validation:
            return False
        user = User.query.filter_by(email=self.email.data).first()
        if not user:
            self.email.errors.append('Unknown email')
            return False
        if not user.verify_password(self.password.data):
            self.password.errors.append('Invalid password')
            return False
        return True

We will have to implement routes for login/register:

@app.route('/register', methods=['GET', 'POST'])
def register():
    form = RegistrationForm(request.form)
    if request.method == 'POST' and form.validate():
        user = User(form.username.data, form.email.data,
                    form.password.data)
        db.session.add(user)
        db.session.commit()
        flash('Thanks for registering')
        return redirect(url_for('login'))
    return render_template('register.html', form=form)

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm(request.form)
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data).first()
        if user is not None and user.verify_password(form.password.data):
            login_user(user)
            redirect_url = request.args.get('next') or url_for('main.login')
            return redirect(redirect_url)
    return render_template('login.html', form=form)

We will write a Jinja2 macrofor our views. This macro will help us render each individual field of a form, containing our labels and displaying errors if any.

{% macro render_field(field) %}
  <dt>{{ field.label }}
  <dd>{{ field(**kwargs)|safe }}
  {% if field.errors %}
    <ul class="errors">
    {% for error in field.errors %}
      <li>{{ error }}</li>
    {% endfor %}
    </ul>
  {% endif %}
  </dd>
{% endmacro %}

So for example this is what our register.html template will look like with the help of render_field macro:

<h1>Register</h1>

{% from "_formhelpers.html" import render_field %}
<form method="POST">
  <dl>
    {{ render_field(form.username) }}
    {{ render_field(form.email) }}
    {{ render_field(form.password) }}
    {{ render_field(form.confirm) }}
  </dl>
  <p><input type="submit" value="Register">
</form>

Respectively, this will be our login.html form:

<h1>Login</h1>

{% from "_formhelpers.html" import render_field %}
<form id="loginForm" method="POST" role="form">
    {{ form.hidden_tag }}
    {{ render_field(form.email, placeholder="email") }}<br>
    {{ render_field(form.password, placeholder="password") }}<br>
    <p><input type="submit" value="Login"></p>
</form>

<a href="/">index</a><br>
<a href="/register">register</a><br>

The following imports and additional configurations steps needed in main.py:

import os

from flask import Flask, render_template, redirect, flash, url_for
from flask_sqlalchemy import SQLAlchemy
from flask import request, json

from flask_wtf import FlaskForm
import email_validator
from wtforms import BooleanField, StringField, PasswordField, SubmitField, validators

from flask_login import LoginManager
from flask_login import UserMixin

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
db = SQLAlchemy(app)
login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'main.login'


@login_manager.user_loader
def load_user(user_id):
    return User.get(user_Id)

We will have to configure our login manager:

if __name__ == '__main__':
    app.secret_key = 'test123'
    app.config['SESSION_TYPE'] = 'filesystem'

    login_manager.init_app(app)

    app.run()

In the end, you should have a directory structure similar to:

.
├── app
│   ├── instance
│      └── test.db
│   ├── main.py
│   └── templates
│       ├── _formhelpers.html
│       ├── index.html
│       ├── login.html
│       └── register.html
├── requirements.txt
└── venv