Mastering Flask: A Comprehensive Web Development Series for Python Enthusiasts - Form Validations

Mastering Flask: A Comprehensive Web Development Series for Python Enthusiasts - Form Validations

Article 6: Form Validations

·

11 min read

Be Sure to Complete the Series

In this Flask web development series, we'll dive deeper into Flask and cover topics that build on the basics introduced in the first article. We'll explore more advanced features of Flask, including database integration, user authentication, and RESTful APIs. We'll also show you how to use Docker to containerize your Flask application, simplify deployment, and set up CI/CD with GitHub Actions to automate testing and deployment. By following this series, you'll gain a comprehensive understanding of Flask and its ecosystem, and easily build robust, scalable web applications. So follow the entire series to get the most out of your Flask learning journey.

Learning Outcome

After reading this article, learners will be able to use WTForms to easily generate input fields for HTML forms and customize the validation of user input using built-in and custom validators. This article provides a comprehensive guide on how to use WTForms for form validation. By the end of this article, learners will have a clear understanding of how to generate input fields using WTForms, handle form submissions, and display error messages using Flask and WTForms. They will also learn to implement security features such as CSRF protection using WTForms. Overall, this article is a valuable resource for developers looking to improve the robustness and security of their web applications.

If you feel stuck please check the github repo to get some help.

Link: github.com/ritwikmath/Mastering-Flask/tree..

Input Validation

Input validation is the process of ensuring that data entered by users is correct, complete, and secure before it is used by an application. It is a critical aspect of application security, as improperly validated user input can lead to a wide range of vulnerabilities such as injection attacks, cross-site scripting (XSS), and more. Input validation typically involves checking data for consistency with a defined set of rules, such as data type, length, format, and range. By validating user input, applications can ensure that only trusted and valid data is processed, thereby preventing errors, vulnerabilities, and other security issues.

WTForms

WTForms serves as a form validation and rendering library, which is widely supported by various Python web frameworks. Along with built-in validation capabilities, developers can also create custom validations using WTForms. Additionally, the library provides support for CSRF protection and internationalization via i18n.

Official Page: https://wtforms.readthedocs.io/en/3.0.x/

Problem Statement

The problem is to ensure the validity of input data that is submitted through login and registration forms. This involves verifying that the data entered by the user meets specific criteria, such as length, format, and data type. Additionally, the forms need to be rendered using the rendering capabilities of the WTForms library, which will allow for a user-friendly and consistent presentation of the forms. The goal is to create a secure and reliable user authentication system that can be integrated into a web application.

Install Dependency

To incorporate WTForms into a project, the first step is to include it in the requirements file. Next, activate the Flask environment and install the necessary modules using pip3, by running the command "pip install -r requirements.txt". After installation, running "pip3 freeze" will generate a list of all the installed modules and their respective versions.

This is helpful to ensure that the correct versions of the required modules are being used, and can be used for reproducibility purposes. Finally, the requirements file can be updated with the new data.

Check article one: https://ritwikmath.hashnode.dev/mastering-flask-a-comprehensive-web-development-series-for-python-enthusiasts

Defining Forms

Create a new folder named forms in your Flask application. Inside the forms folder, create a new Python file named LoginForm.py. In the LoginForm.py file, import the necessary classes from the WTForms library. Define LoginForm class by subclassing Form and adding the necessary form fields. In this example, we define email and password fields with required validation rules, regular expression match, and length validators.

from wtforms import Form, EmailField, validators, PasswordField

class LoginForm(Form):
    email = EmailField('Email', validators=[
        validators.input_required(message='Email is required'), 
        validators.regexp('[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}', message='Invalid email')
    ])
    password = PasswordField('Password', validators=[
        validators.input_required(message='Password is required'),
        validators.length(min=8, message="Password must be a minimum of 8 characters")
    ])

This code is a Python class named LoginForm that extends a Form class from the wtforms module. The LoginForm class defines two fields: email and password.

The email field is an EmailField that represents an email input in a form. It has two validators: input_required and regexp. The input_required validator ensures that the field is not left empty, and if it is, it displays a message "Email is required". The regexp validator checks if the email entered is valid according to a regular expression pattern. If the email is invalid, it displays a message "Invalid email".

The password field is a PasswordField that represents a password input in a form. It also has two validators: input_required and length. The input_required validator ensures that the field is not left empty, and if it is, it displays a message "Password is required". The length validator checks if the password entered is at least 8 characters long. If the password is too short, it displays a message "Password must be a minimum of 8 characters".

Overall, this code creates a form with email and password fields and defines the validation rules for each field using the wtforms module.

Validation

In order to validate form input with the LoginForm class, you need to import it into the route/auth.py file. Within the login view, declare a variable named form and set it equal to an instance of the LoginForm object, passing request.form as an argument to perform the validation. Next, form.validate() method is called to check the input using the predefined validators for each field in the LoginForm class. If the validation fails, store the errors in a session and redirect the user to the login page.

***
from forms.LoginForm import LoginForm

auth_bp = Blueprint('auth', __name__, url_prefix='/auth')

@auth_bp.post('/login')
def login():
    try:
        form = LoginForm(request.form)
        if not form.validate():
            session['errors'] = form.errors
            return redirect(url_for('home.loginForm'))
***

Display Errors

Edit loginForm view in route/home.py. Declare a variable named errors and set it to None. Check if the 'errors' key exists in the session by calling the session.get() method. If the 'errors' key is found, set the errors variable to the value stored in the session, and then delete the 'errors' key from the session. Call the render_template() function to render the 'login.html' template, passing the errors variable as a parameter.

@home_bp.get('/login')
def loginForm():
    if session.get('loggedin_user'):
        return redirect(request.referrer or url_for('home.dashboard'))

    errors = None
    if session.get('errors'):
        errors = session['errors']
        del session['errors']
    return render_template('login.html', errors=errors)

Then edit login.html file. Use an if statement with the condition if errors and errors.email to check if there are any validation errors for the email field in the errors object. If there are validation errors for the email field, display an error message using the span tag with the class "input-error". Use the {{ errors.email[0] }} expression to display the first error message associated with the email field. Add an input field for the email with the name "email", and the type attribute set to "text". You can also add a placeholder attribute to provide an example of the expected input. Use another if statement with the condition if errors and errors.password to check if there are any validation errors for the password field in the errors object. If there are validation errors for the password field, display an error message using the span tag with the class "input-error". Use the {{ errors.password[0] }} expression to display the first error message associated with the password field. Add an input field for the password with the name "password", and the type attribute set to "password". You can also add a placeholder attribute to provide an example of the expected input. To visually indicate that there is an error with a field, add inline CSS styles using the style attribute. For example, you can add the style "border: red 1px solid" to the email and password fields if the corresponding errors are present. You can use the and operator to check if there are both validation errors and errors for a specific field.

{% extends 'base.html' %}

{% block title %}Login{% endblock %}

{% block content %}
<div class="login form">
    <header>Login</header>
    <form method="POST" action="{{url_for('auth.login')}}">
      {% if errors and errors.email %}
        <span class="input-error">{{ errors.email[0] }}</span>
      {% endif %}
      <input name="email" type="text" placeholder="Enter your email" style="{{ferrors and errors.email and 'border: red 1px solid'}}">
      {% if errors and errors.password %}
        <span class="input-error">{{ errors.password[0] }}</span>
      {% endif %}
      <input name="password" type="password" placeholder="Enter your password"  style="{{ferrors and errors.password and 'border: red 1px solid'}}">
      <a href="#">Forgot password?</a>
      <input type="submit" class="button" value="Login">
    </form>
    <div class="signup">
      <span class="signup">Don't have an account?
       <a href="{{url_for('home.registerForm')}}">Signup</label>
      </span>
    </div>
</div>
{% endblock %}
.input-error {
  display: inline-block;
  font-size: 0.75rem;
  color: red;
}

Render Form

In the login form view, you need to import the LoginForm class. Then, declare a variable form as an instance of the LoginForm class. Finally, pass the form variable as an argument through the render_template function.

@home_bp.get('/login')
def loginForm():
    if session.get('loggedin_user'):
        return redirect(request.referrer or url_for('home.dashboard'))

    form = LoginForm()
    errors = None
    if session.get('errors'):
        errors = session['errors']
        del session['errors']
    return render_template('login.html', form=form, errors=errors)

You can replace the email and password fields with {{ form.email(placeholder="Enter your email", style=errors and errors.email and 'border: red 1px solid' | safe }} and {{ form.password(placeholder="Enter your password", style=errors and errors.password and 'border: red 1px solid') | safe }} respectively. The placeholder parameter is used to show a hint inside the field when it is empty. The style parameter is used to add CSS style to the field. The safe filter is used to tell Jinja not to escape the HTML output.

{% extends 'base.html' %}

{% block title %}Login{% endblock %}

{% block content %}
<div class="login form">
    <header>Login</header>
    <form method="POST" action="{{url_for('auth.login')}}">
      {% if errors and errors.email %}
        <span class="input-error">{{ errors.email[0] }}</span>
      {% endif %}
      {{ form.email(placeholder="Enter your email", style=errors and errors.email and 'border: red 1px solid') | safe }}
      {% if errors and errors.password %}
        <span class="input-error">{{ errors.password[0] }}</span>
      {% endif %}
      {{ form.password(placeholder="Enter your password", style=errors and errors.password and 'border: red 1px solid') | safe }}
      <a href="#">Forgot password?</a>
      <input type="submit" class="button" value="Login">
    </form>
    <div class="signup">
      <span class="signup">Don't have an account?
       <a href="{{url_for('home.registerForm')}}">Signup</label>
      </span>
    </div>
</div>
{% endblock %}

Custom Validators

The purpose of the custom validators in this case is to verify if the email entered is already registered and if the entered password matches the registered password. By implementing these validators within the LoginForm class, the validation process can be shifted from the login view to the LoginForm class itself.

validate_email: To validate email, write a public method validate_email. The method checks if the entered email is already registered in the database or not. It does this by querying the database for a developer document that matches the entered email. If a match is not found, the function raises a validation error with the message 'Email is not registered'.

validate_password: It queries the database to find the user with the email entered in the form's email field. Then, it checks if the entered password matches the hashed password stored in the database for the corresponding user using the check_password_hash function from the werkzeug.security module. If the passwords do not match, a ValidationError is raised with the message 'Password did not match'.

from wtforms import Form, EmailField, validators, PasswordField, ValidationError
from database.mongo import Database as Mongo
from werkzeug.security import check_password_hash

class LoginForm(Form):
    email = EmailField('Email', validators=[
        validators.input_required(message='Email is required'), 
        validators.regexp('[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}', message='Invalid email')
    ])
    password = PasswordField('Password', validators=[
        validators.input_required(message='Password is required'),
        validators.length(min=8, message="Password must be a minimum of 8 characters")
    ])

    def validate_email(form, field):
        developer = Mongo().db.developers.find_one({
            'email': field.data
        })
        if not developer:
            raise ValidationError('Email is not registred')

    def validate_password(form, field):
        developer = Mongo().db.developers.find_one({
            'email': form.email.data
        })
        if not developer:
            return
        match = check_password_hash(developer.get('password'), field.data)
        if not match:
            raise ValidationError('Password did not match')

Remove the validation from route/auth.py login view.

@auth_bp.post('/login')
def login():
    try:
        form = LoginForm(request.form)
        if not form.validate():
            session['errors'] = form.errors
            return redirect(url_for('home.loginForm'))
        developer = Mongo().db.developers.find_one({
            'email': request.form['email']
        })
        del developer['password']
        session['loggedin_user'] = json.loads(json.dumps(developer, default=str))
        return redirect(url_for('home.dashboard'))
    except Exception as ex:
        flash(ex.__str__())
        return redirect(url_for('home.loginForm'))

Extend LoginForm

To create validations for the registration form, create a new Python file named RegisterForm in the forms folder. Inside the RegisterForm file, create a new class named RegisterForm. Rather than extending the Form class, extend the LoginForm class because the LoginForm already has the email and password fields defined. Additionally, create a new method named validate_email, which will check if the email entered during registration already exists in the database. If the email exists, the method will throw a validation error.

from wtforms import validators, StringField, ValidationError
from database.mongo import Database as Mongo
from forms.LoginForm import LoginForm

class RegisterForm(LoginForm):
    skills = StringField('Skills', validators=[
        validators.input_required(message="Skills is required")
    ])

    def validate_email(form, field):
        developer = Mongo().db.developers.find_one({
            'email': field.data
        })
        if developer:
            raise ValidationError('Email is already registred')

Great job so far! Don't be afraid to continue building your skills and knowledge by taking on new challenges like implementing the registration validation. You've got this! If you get stuck, don't hesitate to reference the previous code and documentation to help guide you through the process. Keep up the great work!

Conclusion

In conclusion, we have covered the basics of form validation using the WTForms library in a Flask web application. We started by creating a LoginForm with email and password fields and added predefined validators to each field. We then imported the LoginForm class in our authentication route, declared a form variable with LoginForm object, and used the form.validate method to validate input. If validation fails, we stored errors in a session and redirected the user to the login page. We also added custom validators to check if the email is already registered and if the password entered matches the password stored in the database. Finally, we discussed how to create a RegisterForm and its validation by extending the LoginForm class and creating a validate_email method to check if the email already exists. By following these steps, you can ensure that your web application's form input is properly validated, enhancing its security and usability.