Web Technologies/2021-2022/Laboratory 13

From Wikiversity
Jump to navigation Jump to search

Form validation using WTForms[edit | edit source]

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