Validation error flask

Порядок отображения ошибок при валидации данных формы. Рассматривается коллекция errors.
  • Python
    • Основы Python
    • Python ООП
    • wxPython
    • Модули
    • Flask
    • Django
  • JavaScript
    • Основы JavaScript
    • JavaScript DOM
    • JavaScript ООП
  • Java
    • Основы Java
    • Java ООП
  • Обработка данных
    • Нейронные сети
    • ЦОС
    • Фракталы
    • Генетические алгоритмы
    • Tensorflow
    • ML
  • Еще
    • Структуры данных
  • Blueprint и полезные расширения
  • Применение WTForms для работы с формами сайта
  • Обработка ошибок во Flask-WTF
  • Blueprint — что это такое, где и как использовать
  • Blueprint — подключение к БД и работа с ней
  • Flask-SQLAlchemy: установка, создание таблиц, добавление записей
  • Операции с таблицами через Flask-SQLAlchemy
  • Главная
  • Flask
  • Blueprint и полезные расширения

Файл проекта: https://github.com/selfedu-rus/flasksite-18

Продолжаем предыдущее занятие и если мы сейчас
введем на странице неверные данные, например, некорректный пароль, то страница
просто обновится без указания каких-либо ошибок. Вместе с тем, ошибки
автоматически генерируются и доступны в шаблоне из коллекции:

form.<переменная поля>.errors

Добавим их
вывод:

{{ form.email.label() }} 
{% if form.email.errors %}
         {{ form.email(class="invalid") }}
<span class="invalid-feedback">
         {% for e in form.email.errors %}
         {{ e }}
         {% endfor %}
</span>
{% else %}
         {{ form.email() }}
{% endif %}
 
{{ form.psw.label() }}
{% if form.psw.errors %}
         {{ form.psw(class="invalid") }}
<span class="invalid-feedback">
         {% for e in form.psw.errors %}
         {{ e }}
         {% endfor %}
</span>
{% else %}
         {{ form.psw() }}
{% endif %}

Мы здесь после
отображения имени поля проверяем: существуют ли ошибки и если да, то указываем
класс стилей для отображения ошибочных полей а, затем, выводим список ошибок
(их может быть несколько). Если же ошибок нет, то поле отображается в первоначальном
виде.

Чтобы все
выглядело относительно прилично, добавим стили для отображения ошибочных
состояний:

.form-contact .invalid {
         display: inline-block;
         background: #FF9898;
}
.form-contact .invalid-feedback {
         color: #CC0000;
}

Перейдем в
браузер, наберем неверный email и увидим сообщение об ошибке:

Invalid email address.

По умолчанию в WTForms ошибки
показываются на английском языке. Чтобы прописать свои собственные, в классе LoginForm нужно их явно
прописать:

class LoginForm(FlaskForm):
    email = StringField("Email: ", validators=[Email("Некорректный email")])
    psw = PasswordField("Пароль: ", validators=[DataRequired(),
                                                Length(min=4, max=100, message="Пароль должен быть от 4 до 100 символов")])
    remember = BooleanField("Запомнить", default = False)
    submit = SubmitField("Войти")

Теперь, при
неверном вводе информации мы будем видеть указанные нами сообщения.

Формирование полей в шаблоне через цикл

Если вернуться к
нашей форме авторизации, то увидим повторяющиеся части текста, что не очень
хорошо. Чтобы этого не было, мы можем создавать однотипные поля формы через
цикл, используя параметр form как итерируемый объект:

{% for field in form if field.name not in ['csrf_token', 'remember', 'submit'] -%}
         {{ field.label() }} 
         {% if field.errors %}
                   {{ field(class="invalid") }}
         <span class="invalid-feedback">
                   {% for e in field.errors %}
                   {{ e }}
                   {% endfor %}
         </span>
         {% else %}
                   {{ field() }}
         {% endif %}
{% endfor %}

Смотрите, мы здесь выбираем все поля (field) из формы, кроме полей с именами ‘csrf_token’, ‘remember’
и ‘submit’.
Первое
поле – это специальный токен, служащий для защиты от CSRF-атак, а
последние два – это флажок «Запомнить» и кнопка «Войти». Мы их отображаем на
форме без указания списка ошибок.

Если теперь
перейти в браузер, то при обновлении страницы увидим ту же самую форму
авторизации со всеми полями. Причем, порядок полей тот же, что и порядок
переменных в классе LoginForm. Мало того, если будет добавлена еще
какая-либо типовая переменная поля, то она автоматически добавится в форму и
будет соответствующим образом обрабатываться, что очень удобно.

Форма регистрации

Давайте заменим
в нашем сайте еще одну форму для регистрации пользователей. Ее класс можно
прописать следующим образом:

class RegisterForm(FlaskForm):
    name = StringField("Имя: ", validators=[Length(min=4, max=100, message="Имя должно быть от 4 до 100 символов")])
    email = StringField("Email: ", validators=[Email("Некорректный email")])
    psw = PasswordField("Пароль: ", validators=[DataRequired(),
                                                Length(min=4, max=100, message="Пароль должен быть от 4 до 100 символов")])
 
    psw2 = PasswordField("Повтор пароля: ", validators=[DataRequired(), EqualTo('psw', message="Пароли не совпадают")])
    submit = SubmitField("Регистрация")

Мы здесь
используем еще один валидатор EqualTo для проверки совпадения паролей.
Все остальное очень похоже на форму авторизации.

Далее, шаблон register.html запишем в виде:

{% extends 'base.html' %}
 
{% block content %}
{{ super() }}
{% for cat, msg in get_flashed_messages(True) %}
<div class="flash {{cat}}">{{msg}}</div>
{% endfor %}
<form action="{{ url_for('register') }}" method="post" class="form-contact">
{{ form.hidden_tag() }}
 
{% for field in form if field.name not in ['csrf_token', 'submit'] -%}
         {{ field.label() }} 
         {% if field.errors %}
                   {{ field(class="invalid") }}
         <span class="invalid-feedback">
                   {% for e in field.errors %}
                   {{ e }}
                   {% endfor %}
         </span>
         {% else %}
                   {{ field() }}
         {% endif %}
{% endfor %}
 
{{ form.submit() }}
</form>
{% endblock %}

И обработчик register:

@app.route("/register", methods=["POST", "GET"])
def register():
    form = RegisterForm()
    if form.validate_on_submit():
            hash = generate_password_hash(request.form['psw'])
            res = dbase.addUser(form.name.data, form.email.data, hash)
            if res:
                flash("Вы успешно зарегистрированы", "success")
                return redirect(url_for('login'))
            else:
                flash("Ошибка при добавлении в БД", "error")
 
    return render_template("register.html", menu=dbase.getMenu(), title="Регистрация", form=form)

Как видите, все
достаточно просто и при этом получаем доступ к богатому функционалу модуля WTForms.

Конечно, мы
здесь рассмотрели лишь общий принцип использования расширения WTForms. Этих знаний
вполне достаточно для простых реализаций. А для более глубокого погружения в
эту тему советую почитать официальную документацию по данному модулю.

Видео по теме

  • Предыдущая
  • Следующая

In this Flask Tutorial we are going to learn about Flask-WTF Form Error Messages, so in the

previous article we have learned that how you can use Flask-WTF Form, but we have not done 

validation on our Forms. this article is basically for the form validation on Flask-WTF Forms.

you can Check the article on this link Flask Forms with Flask-WTF. 

So this is our forms.py file, and in this file we have created our simple login form and you

can see that we have added some ready validators from Flask-WTF that is InputRequired(). 

For more information about Flask-WTF you can read this article Flask Forms with Flask-WTF.

from flask_wtf import FlaskForm

from wtforms import StringField, PasswordField

from wtforms.validators import InputRequired

class LoginForm(FlaskForm):

    username = StringField(‘Username’, validators=[InputRequired()])

    password = PasswordField(‘Password’, validators=[InputRequired()])

And this is our app.py file with our required view functions and you can see that we have also

added our login view function in here. in this view function just by submitting of the form

we are going to redirect the users to the index using redirect function.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

from flask import Flask, render_template, redirect, url_for

from forms import LoginForm

#create the object of Flask

app  = Flask(__name__)

app.config[‘SECRET_KEY’] = ‘hardsecretkey’

#creating our routes

@app.route(‘/’)

def index():

    return render_template(‘index.html’)

#login route

@app.route(‘/login’ , methods = [‘GET’, ‘POST’])

def Login():

    form = LoginForm()

    if form.validate_on_submit():

        return redirect(url_for(‘index’))

    return render_template(‘login.html’, form = form)

#run flask app

if __name__ == «__main__»:

    app.run(debug=True)

This is our base.html, we have already talked about creating templates in Flask, you can

read this article Introduction to Flask Templates. you need to just create templates folder

in your working directory. 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

<!DOCTYPE html>

<html lang=«en»>

<head>

    <meta charset=«UTF-8»>

    <title>{% block title %} {% endblock %}</title>

    <! CSS Bootstrap CDN Link >

<link rel=«stylesheet» href=«https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css» integrity=«sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk» crossorigin=«anonymous»>

</head>

<body>

<nav class=«navbar navbar-expand-lg navbar-dark bg-dark»>

  <div class=«container»>

      <a class=«navbar-brand» href=«{{url_for(‘index’)}}»>CodeLoop</a>

  <button class=«navbar-toggler» type=«button» datatoggle=«collapse» datatarget=«#navbarSupportedContent» ariacontrols=«navbarSupportedContent» ariaexpanded=«false» arialabel=«Toggle navigation»>

    <span class=«navbar-toggler-icon»></span>

  </button>

  <div class=«collapse navbar-collapse» id=«navbarSupportedContent»>

    <ul class=«navbar-nav mr-auto»>

      <li class=«nav-item active»>

        <a class=«nav-link» href=«{{url_for(‘index’)}}»>Home <span class=«sr-only»>(current)</span></a>

      </li>

         <li class=«nav-item»>

        <a class=«nav-link» href=«#»>Logout</a>

      </li>

    </ul>

    <form class=«form-inline my-2 my-lg-0»>

      <input class=«form-control mr-sm-2» type=«search» placeholder=«Search» arialabel=«Search»>

      <button class=«btn btn-outline-success my-2 my-sm-0» type=«submit»>Search</button>

    </form>

  </div>

  </div>

    <a href=«{{url_for(‘Login’)}}»><button class=«btn btn-success navbar-btn»>Login</button> </a>

     <a href=«»><button class=«btn btn-success navbar-btn»>Signup</button> </a>

</nav>

<! JS, Popper.js, and jQuery >

<script src=«https://code.jquery.com/jquery-3.5.1.slim.min.js» integrity=«sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj» crossorigin=«anonymous»></script>

<script src=«https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js» integrity=«sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo» crossorigin=«anonymous»></script>

<script src=«https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js» integrity=«sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI» crossorigin=«anonymous»></script>

{% block body %}

{% endblock %}

</body>

</html>

So now this is our login.html file , basically in here we need to put our Flask-WTF validation

errors. and you can use this code for adding validation errors. this is for username, you can add

the same for password, just change the username to the password.

{% for error in form.username.errors %}

          <span style=«color:red;»>

              {{error}}

          </span>

          {% endfor %}

This is the complete login.html file.

templates/login.html

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

{% extends ‘base.html’ %}

{% block title %} Home {% endblock %}

{% block body %}

<div class=«container»>

    <h1>Home Page Welcome to codeloop.org</h1>

<h3>Tutorial Number 9 </h3>

    <br>

    <br>

    <hr>

    <h1>Please Login</h1>

    <form action=«» method=«post» novalidate>

      {{form.csrf_token}}

      <p>

          {{form.username.label}}

          {{form.username(size=32)}}

          {% for error in form.username.errors %}

          <span style=«color:red;»>

              {{error}}

          </span>

          {% endfor %}

      </p>

         <p>

          {{form.password.label}}

          {{form.password(size=32)}}

            {% for error in form.password.errors %}

          <span style=«color:red;»>

              {{error}}

          </span>

          {% endfor %}

      </p>

        <p>

            <input type=«submit» value=«Login» class=«btn btn-success»>

        </p>

    </form>

</div>

{% endblock %}

And this is our index.html file.

templates/index.html

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

{% extends ‘base.html’ %}

{% block title %} Home {% endblock %}

{% block body %}

<div class=«container»>

    <h1>Home Page Welcome to codeloop.org</h1>

<h3>Tutorial Number 9 </h3>

<p>

In this tutorial we are going to talk

about FlaskWTF forms validation errors.

</p>

</div>

{% endblock %}

Now run the project and go to http://localhost:5000/login, if you leave blank the fields and

click on login, you will see this error message.

Flask Tutorial - Flask-WTF Form Error Messages

Flask Tutorial – Flask-WTF Form Error Messages

And if you give a random password and email you will redirected to the index page. right

now we have not used any database functionality for our application, we will do this in the

next tutorials.

Flask WTF Error Messages

Flask WTF Error Messages

Also you can watch my complete 4 hours training on Flask Web Development

Thank you for visiting my website! If you enjoyed this article, please consider supporting my works on Patreon.

Время прочтения
15 мин

Просмотры 131K

blog.miguelgrinberg.com

Miguel Grinberg


<<< предыдущая следующая >>>

Эта статья является переводом третьей части нового издания учебника Мигеля Гринберга. Прежний перевод давно утратил свою актуальность.

В этом третьем выпуске серии Мега-Учебник Flask я расскажу о том, как работать с формами.

Для справки ниже приведен список статей этой серии.

Примечание 1: Если вы ищете старые версии данного курса, это здесь.

Примечание 2: Если вдруг Вы хотели бы выступить в поддержку моей(Мигеля) работы в этом блоге, или просто не имеете терпения дожидаться неделю статьи, я (Мигель Гринберг)предлагаю полную версию данного руководства упакованную электронную книгу или видео. Для получения более подробной информации посетите learn.miguelgrinberg.com.

Краткое повторение

В главе 2 я создал простой шаблон для домашней страницы приложения и использовал ложные сущности в качестве заполнителей для объектов, которых у меня еще нет, таких как пользователи или сообщения в блогах. В этой главе я расскажу об одном из многих явлений, которые у меня есть в этом приложении, в частности, как принимать входные данные от пользователей через веб-формы.

Веб-формы являются одним из самых основных строительных блоков в любом веб-приложении. Я буду использовать формы, чтобы пользователи могли отправлять сообщения в блоге, а также для входа в приложение.

Прежде чем продолжить читать эту главу, убедитесь, что у вас есть приложение microblog. Работа проделанная в предыдущей главе должна позволить запустить его без каких-либо ошибок.

Ссылки GitHub для этой главы: Browse, Zip, Diff.

Введение в Flask-WTF

Чтобы обрабатывать веб-формы в этом приложении, я собираюсь использовать расширение Flask-WTF, которое представляет собой обертку пакета WTForms и прекрасно интегрирует его с Flask. Это первое расширение Flask, которое я вам представляю, но не последнее. Расширения являются очень важной частью экосистемы Flask, поскольку они обеспечивают решения проблем.

Расширения Flask — это обычные пакеты Python, которые устанавливаются вместе с pip. Надо установить Flask-WTF в свою виртуальную среду:

(venv) $ pip install flask-wtf

Конфигурация

Пока что приложение очень простое, и по этой причине мне не нужно было беспокоиться о его конфигурации. Но для любых приложений, кроме простейших, вы обнаружите, что Flask (и, возможно, также используемые расширения Flask) предлагают некоторую свободу в том, как делать что-то, и вам нужно принять некоторые решения, которые вы передаете в качестве списка переменных конфигурации.

Для указания параметров конфигурации существует несколько форматов. Самое основное решение — определить ваши переменные как ключи в app.config, который использует стиль словаря для работы с переменными. Например, вы можете сделать что-то вроде этого:

app = Flask(__name__)
app.config['SECRET_KEY'] = 'you-will-never-guess'
# ... add more variables here as needed

Хотя вышеприведенного синтаксиса достаточно для создания параметров конфигурации для Flask, мне нравится применять принцип разделения ответственности, поэтому вместо того, чтобы ставить мою конфигурацию в том же месте, где я создаю свое приложение, я буду использовать несколько более сложную структуру, которая позволяет мне сохранить мою конфигурацию в отдельном файле.

Формат, который мне очень нравится, поскольку он расширяемый, заключается в использовании класса для хранения переменных конфигурации. Чтобы все было хорошо организовано, я собираюсь создать класс конфигурации в отдельном модуле Python. Ниже вы можете увидеть новый класс конфигурации для этого приложения, хранящийся в модуле config.py в каталоге верхнего(!!!) уровня.

import os

class Config(object):
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'

Довольно просто, не так ли? Параметры конфигурации определяются как переменные класса внутри класса Config. Поскольку для приложения требуется больше элементов конфигурации, они могут быть добавлены в этот класс, а позже, если я обнаружу, что мне нужно иметь более одного набора конфигурации, я могу создать его подклассы. Но пока об этом не беспокойтесь.

Переменная конфигурации SECRET_KEY, которую я добавил как единственный элемент конфигурации, является важной частью большинства приложений Flask. Flask и некоторые его расширения используют значение секретного ключа в качестве криптографического ключа, полезного для генерации подписей или токенов. Расширение Flask-WTF использует его для защиты веб-форм от противной атаки под названием Cross-Site Request Forgery или CSRF (произносится как «seasurf»). Как следует из его названия, секретный ключ должен быть секретным, поскольку сила токенов и подписей, генерируемых им, зависит от того, что никто за пределами круга доверенных лиц сопровождающих приложения не знает об этом.

Значение секретного ключа задается как выражение с двумя терминами, к которым присоединяется оператор OR. Первый термин ищет значение переменной среды, также называемой SECRET_KEY. Второй термин, это просто жестко закодированная строка. Это шаблон, который вы увидите, что я часто повторяю для конфигурационных переменных. Идея в том, что значение, появляющееся из переменной среды, предпочтительнее, но если среда не определяет переменную, то вместо нее используется жестко закодированная строка. При разработке этого приложения требования к безопасности невелики, поэтому можно просто игнорировать этот параметр и позволить использовать жестко закодированную строку. Но когда это приложение развертывается на рабочем сервере, я буду устанавливать уникальное и трудно угадываемое значение, так что сервер будет иметь безопасный ключ, который никто не знает.

Теперь, когда у меня есть файл конфигурации, мне нужно, чтобы Flask прочитал его и применил. Это можно сделать сразу после создания экземпляра приложения Flask с использованием метода app.config.from_object() (app__init__.py):

from flask import Flask
from config import Config

app = Flask(__name__)
app.config.from_object(Config)

from app import routes

То, как я импортирую класс Config, может показаться запутанным сначала, но если вы посмотрите, как класс Flask (в верхнем регистре «F») импортируется из flask пакета (нижний регистр «f»), вы заметите, что я делаю то же самое с конфигурацией. Строковый «config» — это имя модуля python config.py, и, очевидно, тот, который имеет верхний регистр «C», является фактическим классом.

Как я упоминал выше, элементы конфигурации могут быть доступны со словарным синтаксисом из app.config. Здесь вы можете увидеть быстрый сеанс с интерпретатором Python, где я проверяю, каково значение секретного ключа:

>>> from microblog import app
>>> app.config['SECRET_KEY']
'you-will-never-guess'

Форма входа пользователя

Расширение Flask-WTF использует классы Python для представления веб-форм. Класс формы просто определяет поля формы как переменные класса.

Еще раз имея в виду разделение проблем, я собираюсь использовать новый app/forms.py модуль для хранения классов веб-форм. Для начала определим форму входа пользователя, в которой пользователю будет предложено ввести имя пользователя и пароль. Форма также будет включать флажок «Запомнить меня» и кнопку Отправить:

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired

class LoginForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    password = PasswordField('Password', validators=[DataRequired()])
    remember_me = BooleanField('Remember Me')
    submit = SubmitField('Sign In')

Большинство расширений Flask используют соглашение об именах flask_ для импорта верхнего уровня. В этом случае Flask-WTF меняет все свои символы под flask_wtf. Здесь базовый класс FlaskForm импортируется из верхней части app/forms.py.

Четыре класса, которые представляют типы полей, которые я использую для этой формы, импортируются непосредственно из пакета WTForms, поскольку расширение Flask-WTF не предоставляет настраиваемые версии. Для каждого поля объект создается как переменная класса в классе LoginForm. Каждому полю присваивается описание или метка в качестве первого аргумента.

Дополнительный аргумент validators, который вы видите в некоторых полях, используется для привязки поведения проверки к полям. Валидатор DataRequired просто проверяет, что поле не отправлено пустым. Существует еще много доступных валидаторов, некоторые из которых будут использоваться в других формах.

Шаблоны форм

Следующим шагом является добавление формы в шаблон HTML, чтобы ее можно было визуализировать на веб-странице. Поля, определенные в классе LoginForm, знают, как визуализировать себя как HTML, поэтому эта задача довольно проста. Ниже вы можете увидеть шаблон входа в систему, который я собираюсь хранить в файле app/templates/login.html:

app/templates/login.html: Шаблон формы входа в систему
( Код исправлен в связи с изменениями в оригинале статьи 2018-06-09. Спасибо magax ! )

{% extends "base.html" %}

{% block content %}
    <h1>Sign In</h1>
    <form action="" method="post" novalidate>
        {{ form.hidden_tag() }}
        <p>
            {{ form.username.label }}<br>
            {{ form.username(size=32) }}
        </p>
        <p>
            {{ form.password.label }}<br>
            {{ form.password(size=32) }}
        </p>
        <p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
        <p>{{ form.submit() }}</p>
    </form>
{% endblock %}

Для этого шаблона я повторно использую еще один раз base.html, как показано в главе 2, через инструкцию расширенного наследования шаблона. На самом деле я сделаю это со всеми шаблонами, чтобы обеспечить единообразную компоновку, которая включает верхнюю панель навигации по всем страницам приложения.

В этом шаблоне предполагается, что объект формы, созданный из класса LoginForm, будет предоставлен в качестве аргумента, который можно увидеть как ссылку form. Этот аргумент будет отправлен функцией просмотра входа, которую я до сих пор не написал.

Элемент HTML <form> используется в качестве контейнера для веб-формы. Атрибут action формы используется для того, чтобы сообщить веб-браузеру URL-адрес, который должен использоваться при отправке информации, введенной пользователем в форму. Если для действия задана пустая строка, форма передается URL-адресу, находящемуся в данный момент в адресной строке, то есть URL-адресу, который визуализирует форму на странице. Атрибут method указывает метод HTTP-запроса, который должен использоваться при отправке формы на сервер. По умолчанию он отправляется с запросом GET, но почти во всех случаях использование POST запрос, улучшающий взаимодействие с пользователем, поскольку запросы этого типа могут отправлять данные формы в тело запроса, в то время как запросы GET добавляют поля формы к URL-адресу, загромождая адресную строку обозревателя.

Добавлено 2018-06-09 Спасибо за замечания magax !
Атрибут novalidate используется для указания веб-браузеру не применять проверку к полям в этой форме, что фактически оставляет эту задачу приложению Flask, запущенному на сервере. Использование novalidate является полностью необязательным, но для этой первой формы важно, чтобы вы установили его, потому что это позволит вам протестировать проверку на стороне сервера позже в этой главе

Аргумент шаблона form.hidden_tag()создает скрытое поле, содержащее токен, используемый для защиты формы от атак CSRF. Все, что вам нужно сделать, чтобы форма была защищена, — это включить это скрытое поле и определить переменную SECRET_KEY в конфигурации Flask. Если вы позаботитесь об этих двух вещах, Flask-WTF сделает все остальное за вас.

Если вы уже писали HTML Web Forms в прошлом, возможно, вы сочли странным, что в этом шаблоне нет HTML-полей. Это происходит потому, что поля из объекта Form знают, как визуализировать себя как HTML. Все, что мне нужно было сделать, это включить {{ form.<field_name>.label }} в месте где нужна метка поля и {{ form.<field_name>() }} где нужно само поле. Для полей, которым требуются дополнительные атрибуты HTML, они могут быть переданы в качестве аргументов. Поля username и Password в этом шаблоне принимают size размер в качестве аргумента, который будет добавлен в HTML-элемент <input> в качестве атрибута. Таким образом можно также присоединять классы CSS или идентификаторы к полям формы.

Представление формы

Заключительный шаг перед тем, как вы увидите эту форму в браузере, — это код новой функции просмотра в приложении, которая отображает шаблон из предыдущего раздела.

Итак, давайте напишем новую функцию представления, сопоставленную с URL-адресом /login, которая создаст форму, и передаст её в шаблон для рендеринга. Эта функция просмотра может находиться в модуле app/routes.py дополняя содержимое:

from flask import render_template
from app import app
from app.forms import LoginForm

# ...

@app.route('/login')
def login():
    form = LoginForm()
    return render_template('login.html', title='Sign In', form=form)

Здесь я импортировал класс LoginForm из forms.py, создал экземпляр объекта из него и отправлял его в шаблон. Синтаксис form = form может выглядеть странно, но просто передает объект формы, созданный в строке выше (и показан справа), в шаблон с формой имени (показан слева). Это все, что требуется для отображения полей формы.

Чтобы упростить доступ к форме входа, базовый шаблон должен включать в себя ссылку на панель навигации:

<div>
    Microblog:
    <a href="/index">Home</a>
    <a href="/login">Login</a>
</div>

На этом этапе можно запустить приложение и посмотреть веб-браузере что получилось. При запуске приложения введите http://localhost:5000/ в адресной строке браузера, а затем нажмите ссылку «Войти» (Sign In) в верхней панели навигации, чтобы увидеть новую форму входа. Довольно круто, не так ли?

Получение данных формы

Если вы попытаетесь нажать кнопку отправки (Sign In), браузер отобразит ошибку «Method Not Allowed» (Метод не разрешен). Это связано с тем, что функция входа в систему из предыдущего раздела выполняет половину задания. Может отображать форму на веб-странице, но у нее нет никакой логики для обработки данных, представленных пользователем. Это еще одна область, где Flask-WTF облегчает работу. Ниже приведена обновленная версия функции просмотра, которая принимает и проверяет данные, представленные пользователем:

from flask import render_template, flash, redirect

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        flash('Login requested for user {}, remember_me={}'.format(
            form.username.data, form.remember_me.data))
        return redirect('/index')
    return render_template('login.html', title='Sign In', form=form)

Первой новинкой в этой версии является аргумент methods в дизайнере маршрутов. Это сообщения для Flask, что эта функция просмотра принимает запросы GET и POST, переопределяя значение по умолчанию, которое должно принимать только запросы GET. Протокол HTTP указывает, что GET-запросы — это те, которые возвращают информацию клиенту (в этом случае веб-браузер). Все запросы в приложении до сих пор относятся к этому типу. Запросы POST обычно используются, когда браузер передает данные формы на сервер (на самом деле запросы GET также могут использоваться для этой цели, но это не рекомендуется). Ошибка «Method Not Allowed», которую браузер показал вам раньше, появляется, потому что браузер попытался отправить запрос POST, и приложение не настроено на его принятие. Предоставляя аргумент methods, вы сообщаете Flask о необходимости принимать методы запроса.

Метод form.validate_on_submit() выполняет всю обработку формы. Когда браузер отправляет запрос GET для получения веб-страницы с формой, этот метод возвращает False, поэтому в этом случае функция пропускает оператор if и переходит к отображению шаблона в последней строке функции.

Когда браузер отправляет запрос POST в результате нажатия пользователем кнопки submit, form.validate_on_submit() собирает все данные, запускает все валидаторы, прикрепленные к полям, и если все в порядке, вернет True, сообща что данные действительны и могут быть обработаны приложением. Но если хотя бы одно поле не подтвердит проверку, функция вернет False, и это приведет к тому, что форма будет возвращена пользователю, например, в случае запроса GET. Позже я собираюсь добавить сообщение об ошибке, когда проверка не удалась.

Когда form.validate_on_submit() возвращает значение True, функция входа в систему вызывает две новые функции, импортированные из Flask. Функция flash() — полезный способ показать сообщение пользователю. Многие приложения используют эту технику, чтобы сообщить пользователю, было ли какое-либо действие успешным или нет. В этом случае я буду использовать этот механизм как временное решение, потому что у меня нет всей инфраструктуры, необходимой для регистрации пользователей в реальности. Лучшее, что я могу сделать сейчас, это показать сообщение, подтверждающее, что приложение получило учетные данные.

Вторая новая функция, используемая в функции входа в систему, — redirect(). Эта функция указывает веб-браузеру клиента автоматически перейти на другую страницу, указанную в качестве аргумента. Эта функция просмотра использует ее для перенаправления пользователя на /index страницу приложения.

Когда вы вызываете функцию flash(), Flask сохраняет сообщение, но на веб-страницах не будут появляться магические сообщения. Шаблоны приложения должны отображать эти свернутые сообщения таким образом, который работает для макета сайта. Я собираюсь добавить эти сообщения в базовый шаблон, чтобы все шаблоны наследовали эту функциональность. Это обновленный базовый шаблон:

<html>
    <head>
        {% if title %}
        <title>{{ title }} - microblog</title>
        {% else %}
        <title>microblog</title>
        {% endif %}
    </head>
    <body>
        <div>
            Microblog:
            <a href="/index">Home</a>
            <a href="/login">Login</a>
        </div>
        <hr>
        {% with messages = get_flashed_messages() %}
        {% if messages %}
        <ul>
            {% for message in messages %}
            <li>{{ message }}</li>
            {% endfor %}
        </ul>
        {% endif %}
        {% endwith %}
        {% block content %}{% endblock %}
    </body>
</html>

Здесь я использую конструкцию with, чтобы назначить результат вызова get_flashed_messages() переменной messages, все в контексте шаблона. Функция get_flashed_messages() поступает из Flask и возвращает список всех сообщений, которые были зарегистрированы в flash() ранее. Условие, которое следует, проверяет, имеет ли сообщение некоторый контент, и в этом случае элемент <ul>отображается с каждым сообщением в виде элемента списка <li>. Этот стиль рендеринга выглядит не очень хорошо, но тема стилизации веб-приложения появится позже.

Интересным свойством этих flash-сообщений является то, что после их запроса один раз через функцию get_flashed_messages они удаляются из списка сообщений, поэтому они появляются только один раз после вызова функции flash().

Время, чтобы попробовать приложение еще раз и проверить, как работает форма. Убедитесь, что вы пытаетесь отправить форму с полями имени пользователя или пароля пустым, чтобы увидеть, как валидатор DataRequired останавливает процесс отправки.

Повышение эффективности проверки полей

Валидаторы, прикрепленные к полям формы, предотвращают передачу недопустимых данных в приложение. Способ, которым приложение отвечает на недопустимый ввод в поля формы, заключается в повторном отображении формы, чтобы позволить пользователю внести необходимые исправления.

Если вы пытались ввести недопустимые данные, я уверен, что вы заметили, что, хотя механизмы проверки работают хорошо, нет сообщений пользователю, что что-то не так с формой, пользователь просто получает форму обратно. Следующая задача состоит в том, чтобы улучшить взаимодействие с пользователем, добавив значимое сообщение об ошибке рядом с каждым полем, которое не удалось проверить.

Фактически, валидаторы форм уже создают эти описательные сообщения об ошибках, поэтому все, что отсутствует, — это некоторая дополнительная логика в шаблоне для их отображения.

Вот шаблон входа с добавленными сообщениями (Это поле обязательно.) проверки имени пользователя и пароля:

{% extends "base.html" %}

{% block content %}
    <h1>Sign In</h1>
    <form action="" method="post"  novalidate>
        {{ form.hidden_tag() }}
        <p>
            {{ form.username.label }}<br>
            {{ form.username(size=32) }}<br>
            {% for error in form.username.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.password.label }}<br>
            {{ form.password(size=32) }}<br>
            {% for error in form.password.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
        <p>{{ form.submit() }}</p>
    </form>
 {% endblock %}

Единственное изменение, которое я сделал, это добавил циклы сразу после поля username и password, которые отображают сообщения об ошибках, добавленные валидаторами в красном цвете. Как правило, все поля, имеющие прикрепленные проверяющие элементы, будут иметь сообщения об ошибках, добавляемые в form.<field_name>.errors. Это будет список, потому что поля могут иметь несколько прилагающихся валидаторов и предоставлений сообщений об ошибках может быть более одного для отображения пользователю.

Если вы попытаетесь отправить форму с пустым именем пользователя или паролем, вы получите красное сообщение об ошибке.

Создание связей

Форма входа в систему теперь почти закончена, но перед закрытием этой главы я хотел бы обсудить правильный способ включения связей в шаблоны и перенаправления. До сих пор вы видели несколько примеров, в которых были определены связи. Например, это текущая панель навигации в базовом шаблоне:

<div>
    Microblog:
    <a href="/index">Home</a>
    <a href="/login">Login</a>
</div>

Функция просмотра login также определяет ссылку, которая передается функции redirect():

@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
    # ...
    return redirect('/index')
    # ...

Одна из проблем с написанием ссылок непосредственно в шаблонах и исходных файлах заключается в том, что если в один прекрасный день вы решите реорганизовать свои ссылки, вам придется искать и заменять эти ссылки во всем своем приложении.

Чтобы лучше контролировать эти ссылки, Flask предоставляет функцию url_for(), которая генерирует URL-адреса, используя внутреннее отображение URL-адресов для просмотра функций. Например, url_for('login') возвращает /login, а url_for('index') возвращает '/index. Аргументом для url_for() является имя конечной точки, которое является именем функции view.

Вы можете спросить, почему лучше использовать имена функций вместо URL-адресов. Дело в том, что URL-адреса гораздо чаще меняются, чем имена функций, которые являются внутренними. Вторая причина заключается в том, что, как вы узнаете позже, некоторые URL-адреса имеют динамические компоненты, поэтому для генерации этих URL вручную потребуется объединение нескольких элементов, что является утомительным и подверженным ошибкам. url_for() также может генерировать эти сложные URL-адреса.

Поэтому с этого момента я буду использовать url_for() каждый раз, когда мне нужно создать URL приложения. В итоге панель навигации в базовом шаблоне становится такой:

<div>
    Microblog:
    <a href="{{ url_for('index') }}">Home</a>
    <a href="{{ url_for('login') }}">Login</a>
</div>

И вот обновленная функция login():

from flask import render_template, flash, redirect, url_for

# ...

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        # ...
        return redirect(url_for('index'))
# ...

<<< предыдущая следующая >>>

Проверка формы — один из наиболее важных компонентов ввода данных в веб-приложениях. Пользователи могут совершать ошибки, некоторые — злонамеренные. С помощью проверки ввода мы защищаем наше приложение от неверных данных, влияющих на бизнес-логику, и от злонамеренного ввода, предназначенного для нанесения вреда нашим системам.

Попытка обработать вводимые пользователем данные, не прошедшие проверку, может вызвать неожиданные / необработанные ошибки, если не сбой сервера. В этом контексте проверка данных означает проверку входных данных и проверку их соответствия определенным ожиданиям или критериям. Проверка данных может выполняться как на стороне клиента, так и на стороне сервера.

В этом руководстве мы узнаем, как проверить ввод данных пользователем в Flask формах с помощью расширения Flask-WTForms.

К концу этого руководства у нас будет следующая форма регистрации пользователя с критериями проверки:

Мы будем использовать Flask версии 1.1.2 и Flask-WTF с версией 0.14.3.

Настройка

Хотя в этом нет необходимости, мы рекомендуем вам создать виртуальную среду:

mkdir flask-form-validation
cd flask-form-validation
python3 -m venv .
. bin/activate

В вашей активированной виртуальной среде мы установим наши пакеты, набрав:

pip install Flask Flask-WTF

Обратите внимание: если вы хотите использовать проверку адреса электронной почты, вам также необходимо установить пакет email_validator (текущая версия — 1.1.1):

pip3 install email_validator

Теперь создадим необходимые нам файлы. Мы начнем с создания базового app.py, который для простоты будет содержать наше приложение Flask, маршруты и формы:

from flask import Flask, render_template

app = Flask(__name__, template_folder='.')
app.config['SECRET_KEY']='LongAndRandomSecretKey'

Мы создали объект Flask и установили template_folder в текущую папку. Затем мы присвоили объект Flask переменной app. Мы добавили SECRET_KEY в конфигурацию нашего объекта app.

Обычно SECRET_KEY используется для шифрования соединений с базами данных и сессий браузера. WTForms будет использовать SECRET_KEY в качестве соли для создания токена CSRF. Вы можете узнать больше о CSRF в вики.

Если ваше приложение уже использует конфигурацию SECRET_KEY для других целей, вы можете создать новую конфигурацию для WTForms. В этом случае вы можете установить WTF_CSRF_SECRET_KEY.

Создадим и добавим форму в наш текущий app.py:

from flask import Flask, render_template
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField

class GreetUserForm(FlaskForm):
    username = StringField(label=('Enter Your Name:'))
    submit = SubmitField(label=('Submit'))

# ...

Наш простой класс GreetUserForm содержит StringField. Как следует из названия, это поле ожидает и вернет строковое значение (вы всегда можете преобразовать этот ввод в другие типы данных по мере необходимости). Поля username, мы будем использовать для доступа к данным элемента формы.

Параметры label — это то, что будет отображаться на нашей странице, чтобы пользователи понимали, какие данные захватывает элемент формы. У нас также есть кнопка submit, которая попытается отправить форму, если все поля соответствуют нашим критериям проверки.

Теперь, когда у нас все настроено, давайте воспользуемся WTForms для проверки наших данных!

Проверка формы Flask с помощью Flask-WTForms

Начнем с создания маршрута для отображения и обработки нашей формы:

# ...

@app.route('/', methods=('GET', 'POST'))
def index():
    form = GreetUserForm()
    if form.validate_on_submit():
        return f'''<h1> Welcome {form.username.data} </h1>'''
    return render_template('index.html', form=form)

Наш маршрут имеет методы GET и POST. В методе GET отображает форму, в то время как метод POST по запросу обрабатывает данные формы. Мы устанавливаем путь URL-адреса / или корневой URL-адрес, чтобы он отображался как домашняя страница нашего веб-приложения. Мы визуализируем шаблон index.html и передаем объект form в качестве параметра.

Обратите внимание на эту строку: if form.validate_on_submit():. Это правило говорит: Если метод запроса — POST и если поля формы валидны. Если введенная нами форма соответствует нашим критериям проверки, на следующей странице будет отображено простое приветственное сообщение с именем пользователя. Обратите внимание, что здесь мы использовали имя поля (username) для доступа к входным данным.

Чтобы увидеть форму, нам нужно создать шаблон index.html. Создайте файл и добавьте в него следующий код:

<form method="POST" action="">
    <div class="form-row">
        <div class="form-group col-md-6">
            {{ form.csrf_token() }}
            <label for=""> {{ form.username.label }}</label>
            {{ form.username }}
        </div>
        <div class="form-group">
            {{ form.submit(class="btn btn-primary")}}
        </div>
    </div>
</form>

Мы используем наш объект form для передачи элементов WTform в Jinja2 — синтаксический анализатор шаблонов для Flask.

Примечание: csrf_token создается автоматически WTForms и изменяется каждый раз при отображении страницы. Это помогает нам защитить наш сайт от атак CSRF. По умолчанию это скрытое поле. Вы также можете использовать {{ form.hidden_field() }} для отображения всех скрытых полей, включая токен CSRF, но это не рекомендуется.

Теперь давайте перейдем к нашему терминалу, чтобы запустить приложение Flask, набрав:

FLASK_ENV=development flask run

Для удобства мы установили для переменной среды FLASK_ENV значение development во время разработки. Это позволяет приложению перезагружаться каждый раз, когда мы нажимаем «Сохранить». Для Windows вам, возможно, придется использовать set FLASK_ENV=development в своем терминале / консоли перед запуском приложения Flask.

Вот что мы увидим, если перейдем на localhost:

Введите имя в поле ввода и отправьте форму. Вы увидите приветственное сообщение, которое мы определили в нашем маршруте:

Работает как положено. Но что, если мы ничего не вводим в поле ввода? Он все равно подтвердит форму:

Давайте предотвратим это и позволим видеть следующую страницу только тем пользователям, которые ввели свое имя. Для этого нам нужно убедиться, что в нашем поле username есть входные данные.

Мы импортируем один из встроенных методов проверки WTForms: DataRequired() из wtforms.validators и передадим его в наше поле username.

# ...
from wtforms.validators import ValidationError, DataRequired

class GreetUserForm(FlaskForm):
    username = StringField(label=('Enter Your Name:'),
                           validators=[DataRequired()])
    submit = SubmitField(label=('Submit'))

# ...

Обратите внимание, что мы передаем параметр validators в виде списка. Это говорит нам о том, что у нас может быть несколько валидаторов для каждого поля.

Теперь, когда мы используем DataRequired(), поле username не будет проверяться, если нет входных данных:

Фактически, если мы щелкнем правой кнопкой мыши и проверим элемент формы, мы увидим, что WTForms автоматически добавили атрибут required в поле ввода:

Таким образом, WTForms добавляет базовую проверку в наше поле формы. Вы не сможете отправить эту форму без поля username, даже если попытаетесь опубликовать форму с помощью таких инструментов, как cURL или Postman.

Теперь предположим, что мы хотим установить новое правило проверки, которое будет разрешать только имена длиной не менее 5 символов. Мы можем использовать валидатор Length() с параметром min:

# ...
from wtforms.validators import ValidationError, DataRequired, Length

class GreetUserForm(FlaskForm):
    username = StringField(label=('Enter Your Name:'), 
    	validators=[DataRequired(), Length(min=5)])
    submit = SubmitField(label=('Submit'))

# ...

Если мы попытаемся отправить форму с входными данными длиной менее 5 символов, критерии проверки не будут выполнены, и отправка завершится ошибкой:

Нажатие на кнопку отправки ничего не делает для недопустимых данных, а также не отображает никаких ошибок для пользователя. Нам нужно предоставлять сообщения об ошибках, чтобы пользователь понимал, что происходит и как это исправить.

В нашем шаблоне index.html прямо под шаблоном {{ form.username }} добавьте следующий цикл для отображения ошибок:

 {% for field, errors in form.errors.items() %}
    <small class="form-text text-muted ">
        {{ ', '.join(errors) }}
    </small>
{% endfor %}

Теперь наша форма может отображать чистые ошибки проверки:

По какой-либо причине, если нам нужно ограничить максимальную длину данных нашего поля, мы можем сделать это, передав параметр max валидатору Length(). Также можно настроить сообщение об ошибке, передав необязательный параметр message с настраиваемой строкой ошибки.

Обновим поле username:

# ...

class GreetUserForm(FlaskForm):
    username = StringField(label=('Enter Your Name:'),
        validators=[DataRequired(), 
        Length(min=5, max=64, message='Name length must be between %(min)d and %(max)dcharacters') ])
    submit = SubmitField(label=('Submit'))

# ...

Дополнительные поля и валидаторы WTForms с формой регистрации пользователя

В нашей текущей форме есть одно поле, которое довольно скучно. WTForms предоставляет обширные критерии проверки формы и множество полей формы, поэтому давайте воспользуемся этим и создадим что-то с практическим применением.

Мы создадим форму регистрации пользователя и будем использовать встроенные валидаторы WTForms.

Мы будем использовать валидатор DataRequired() для полей, которые мы хотим убедиться, что пользователь заполняет. Мы будем проверять минимальную и максимальную длину полей с помощью валидатора Length(), проверять электронные письма с помощью валидатора Email() и проверять, содержат ли два поля одинаковые данные с помощью валидатора EqualTo().

Удалите класс GreetUserForm и замените начало кода нашей новой формой:

from flask import Flask, render_template
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, 
    SubmitField
from wtforms.validators import ValidationError, DataRequired, 
    Email, EqualTo, Length

class CreateUserForm(FlaskForm):
    username = StringField(label=('Username'), 
        validators=[DataRequired(), 
        Length(max=64)])
    email = StringField(label=('Email'), 
        validators=[DataRequired(), 
        Email(), 
        Length(max=120)])
    password = PasswordField(label=('Password'), 
        validators=[DataRequired(), 
        Length(min=8, message='Password should be at least %(min)d characters long')])
    confirm_password = PasswordField(
        label=('Confirm Password'), 
        validators=[DataRequired(message='*Required'),
        EqualTo('password', message='Both password fields must be equal!')])

    receive_emails = BooleanField(label=('Receive merketting emails.'))

    submit = SubmitField(label=('Submit'))

# ...    

В наших формах есть четыре разных поля. Последняя — обычная кнопка отправки. С помощью StringField мы устанавливаем строковый инпут от пользователей, например username и emailPasswordField скрывает текст пароля во внешнем интерфейсе. BooleanField отображается как флажок во внешнем интерфейсе, поскольку он содержит только значения True (отмечен) или False (не отмечен).

Нам нужно изменить шаблон index.html, чтобы отобразить наши новые поля формы:

<div class="container">
    <h2>Registration Form</h2>
    {% for field, errors in form.errors.items() %}
    {{ ', '.join(errors) }}
    {% endfor %}
    <form class="form-horizontal" method="POST" action="">
        {{ form.csrf_token() }}
        <div class="form-group">
            {{ form.username.label }}
            {{ form.username(class="form-control") }}
        </div>
        <div class="form-group">
            {{ form.email.label }}
            {{ form.email(class="form-control") }}
        </div>
        <div class="form-group">
            {{ form.password.label }}
            {{ form.password(class="form-control") }}
        </div>
        <div class="form-group">
            {{ form.confirm_password.label }}
            {{ form.confirm_password(class="form-control") }}
        </div>
        <div class="form-group">
            {{ form.receive_emails.label }}
        </div>
        <div class="form-group">
            {{ form.submit(class="btn btn-primary")}}
        </div>
    </form>
</div>

Поля нашей формы отображаются правильно, как вы можете видеть:

Примечание. Если на вашем веб-сайте будет несколько разных форм, вы можете использовать макросы Jinja2 вместо того, чтобы вводить каждое поле формы по одному. Использование макросов выходит за рамки этой статьи, но это значительно ускоряет процессы создания форм.

Создание собственных пользовательских валидаторов

На большинстве веб-сайтов использование определенных символов в именах пользователей запрещено. Может быть в целях безопасности, может быть в косметике. WTForms не имеет такой логики по умолчанию, но мы можем определить ее сами.

WTForms позволяет нам добавлять настраиваемые валидаторы, добавляя метод проверки к нашему классу UserRegistrationForm. Давайте внедрим эту настраиваемую проверку в нашу форму, добавив метод validate_username() прямо под кнопкой submit.

# ...

class UserRegistrationForm(FlaskForm):
    # ...
    submit = SubmitField(label=('Submit'))

    def validate_username(self, username):
        excluded_chars = " *?!'^+%&/()=}][{$#"
        for char in self.username.data:
            if char in excluded_chars:
                raise ValidationError(
                    f"Character {char} is not allowed in username.")
                
# ...

Мы можем добавить столько методов проверки, сколько захотим. WTForms будет запускать методы проверки автоматически после определения.

Класс ValidationError дает нам удобный способ определить наше настраиваемое сообщение проверки. Обратите внимание, что вам нужно будет импортировать его перед использованием wtforms.validators.

Давайте протестируем этот новый метод, введя правильные данные во все поля, кроме поля username, которое будет содержать исключенный символ — «%».

Как видите, наш собственный метод проверки работает отлично и выдает чистую ошибку проверки, которая помогает нам понять, что не так с нашими входными данными. Это значительно улучшает пользовательский опыт.

Вы можете использовать внешние библиотеки, вашу базу данных или API для объединения с WTForms и для проверки входящих данных. Если вы хотите захватить {{ form.some_field.data }} и записать в базу данных или запросить из нее, используйте валидаторы WTForms, чтобы гарантировать безопасность для сохранения.

Вывод

Проверка данных — одна из самых важных частей веб-приложений Flask. Flask-WTforms предоставляет очень мощные и простые в освоении способы обработки данных форм.

Теперь, когда вы знаете основы проверки данных с помощью Flask-WTF, вы можете продолжить и применить свою собственную логику проверки и / или реализовать свои собственные методы как для безопасности, так и для улучшения взаимодействия с пользователем.

Integrate Custom Validation in WTForms using Flask

Posted November 11, 2018

While using WTF or Flask-WTF for the form validation you would often find yourself adding custom validation messages according to the requirement of your web application. You can always use WTF’s ValidationError API however in this tutorial we will show you clever hack to add validation messages during page rendering. We will show example code with flask but you can use this method with any other framework which is using WTF for form validation.


Form Model

Lets assume that we have web app which asks users to submit Name and URL via form. We want to make URL field to accept only valid URL and field is mandatory. We can easily implement this functionality with the help of WTF’s DataRequired and URL validators.

from flask_wtf import FlaskForm
from wtforms.fields import TextField
from wtforms.fields.html5 import  URLField
from wtforms.validators import DataRequired, URL


class URLForm(FlaskForm):
    name = TextField('Name', 
                    validators=[DataRequired(message="Enter Your Name Please")])

    url = URLField('URL', 
                    validators=[DataRequired(message="Enter URL Please"), 
                    URL(message="Enter Valid URL Please.")])

Custom Validation Checker

But in addition to that we only allow URLs submitted for the restricted sites (domains). For this we will need to write our own custom validation method and need to render validation error below URL field in our web form. In flask we will write custom validation check method.

def allowed_site(url):
    allowed_sites = ["example.com", "www.example.com"]

    # Will Fetch HOST Domain From URL
    # Example : https://example.com/abc/xyz >> example.com
    host = url.split('/')[2].lower()

    if host in allowed_sites:
        return True
    else:
        return False

Handling Post and Rendering Validation Error

We will return the submitted web form page upon the post request and render the validation errors if any using form.url.errors.

form.validate() method will automatically take care of validation error raised by WTF Form. However to check and render custom error we will need to append it inside the list form.url.errors during the runtime like below.

@app.route('/submiturl', methods=["GET", "POST"])
def submiturl():
    name = request.args.get('name', '')
    url = request.args.get('url', '')

    # form.validate() will validate WTF Validaors
    if request.method == 'POST' and form.validate():
        if not allowed_site(url):
            form.url.errors.append("This site is not allowed")

Conclusion

In above example we learned how we can append custom validation messages inside error list of the wtf’s form field form.url.errors.append and implement custom validation.

Tagged Under : Flask Flask-WTF Open Source Python WTF Web

About

Techmonger is a web log which shares interesting articles about computer science, programming and web development.


Содержание:

  • Практическое применение модуля WTForm.
    • Пример определения класса HTML-формы для приложения Flask.
    • Использование класса HTML-формы в функции-представлении Flask.
    • Использование класса HTML-формы в шаблоне Jinja2.
  • Краткое описание модуля WTForm.
    • Класс WTForm.Form.
    • Наследование форм.
    • Метод validate_<fieldname> в качестве валидатора поля формы.
    • Атрибуты и методы экземпляра класса Form.
    • Определение полей формы.
    • Аргументы конструктора базового класса поля.
    • Встроенные типы полей для формы.
    • Встроенные валидаторы полей.
    • Список встроенных валидаторов, определяемых в wtforms.validators.
    • Создание собственного валидатора.

Практическое применение модуля WTForm.

Ни один веб-сайт не обходится без HTML-форм, будь это страница обратной связи, страница авторизации или даже форма для комментариев. Когда нужно работать с данными HTML-формы, то код быстро становится очень трудным для чтения. Существуют библиотеки, призванные упростить управление этим процессом. Одним из них является WTForms.

Примечание

. Расширение Flask-WTF добавляет несколько небольших помощников, которые делают работу с формами и фреймворком Flask более быстрой и удобной.

Пример определения класса HTML-формы для приложения Flask.

Пример формы для типичной страницы регистрации:

# например forms.py
from wtforms import Form, BooleanField, StringField, PasswordField, validators

class RegistrationForm(Form):
    username = StringField('Имя пользователя', [validators.Length(min=4, max=25)])
    email = StringField('Email-адрес', [validators.Length(min=6, max=35)])
    password = PasswordField('Новый пароль', [
        validators.DataRequired(),
        validators.EqualTo('confirm', message='Пароли должны совпадать')
    ])
    confirm = PasswordField('Повторите пароль')
    accept_tos = BooleanField('Я принимаю TOS', [validators.DataRequired()])

Использование класса HTML-формы в функции-представлении Flask.

В функции-представлении, обработка формы, определенной выше, выглядит так:

# например views.py
@app.route('/register', methods=['GET', 'POST'])
def register():
    # создаем экземпляр класса формы
    form = RegistrationForm(request.form)
    # если HTTP-метод POST и данные формы валидны
    if request.method == 'POST' and form.validate():
        # используя схему `SQLAlchemy` создаем объект, 
        # для последующей записи в базу данных
        user = User(form.username.data, form.email.data,
                    form.password.data)
        db_session.add(user)
        flash('Спасибо за регистрацию')
        return redirect(url_for('login'))
    # если HTTP-метод GET, то просто отрисовываем форму
    return render_template('register.html', form=form)
Обратите внимание

, что здесь представление использует модуль для работы с базой данных SQLAlchemy, но это, конечно, не является обязательным требованием. При необходимости код нужно скорректировать.

  1. Создать экземпляр класса определенной вами формы из значения request.form, если данные отправляются с помощью метода HTTP POST, и request.args, если данные отправляются как GET.
  2. Для проверки/валидации данных формы нужно вызвать метод экземпляра формы .validate(), который вернет True, если данные валидны, и False в противном случае.
  3. Для доступа к отдельным значениям из формы необходимо использовать паттерн form.<NAME>.data, где <NAME> — имя поля формы.

Использование класса HTML-формы в шаблонах Jinja2.

Модуль WTForms генерирует практически всю форму за нас. Чтобы это было еще приятнее, можно написать макрос, который отображает поле с меткой и списком ошибок, если таковые имеются.

Пример шаблона jinja2 _formhelpers.html с таким макросом:

{# _formhelpers.html #}
{% 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 %}

Этот макрос принимает пару ключевых аргументов, которые передаются в функцию поля WTForm, которая генерирует поле. Ключевые аргументы будут вставлены в качестве атрибутов HTML. Так, например, можно вызвать render_field(form.username, class='username'), чтобы добавить атрибут class в HTML-элемент <input>.

Обратите внимание

, что WTForms возвращает стандартные строки Python, поэтому необходимо сообщить Jinja2, что эти данные уже экранированы с помощью фильтра {{ val | safe }}.

Пример шаблона register.html, который использует преимущества макроса, импортируемого из шаблона _formhelpers.html, созданного ранее:

{# register.html #}
{% 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) }}
    {{ render_field(form.accept_tos) }}
  </dl>
  <p><input type=submit value=Register></p>
</form>

Краткое описание модуля WTForm.

Класс WTForm.Form

Класс Form содержит определения полей, делегирует проверку/валидацию, принимает ввод, объединяет ошибки и в целом служит связующим звеном, скрепляющим все вместе.

Чтобы определить форму, нужно создать подкласс Form и декларативно определить поля как атрибуты класса:

from wtforms import Form, StringField, validators

class MyForm(Form):
    first_name = StringField('First Name', validators=[validators.input_required()])
    last_name  = StringField('Last Name', validators=[validators.optional()])

Имена полей могут быть любыми допустимыми идентификаторами python со следующими ограничениями:

  1. Имена полей чувствительны к регистру.
  2. Имена полей не могут начинаться с символа подчеркивания '_'.
  3. Имена полей не могут начинаться c 'validate'.

Наследование форм.

Формы могут при необходимости подклассифицировать другие формы. Новая форма будет содержать все поля родительской формы, а также любые новые поля, определенные в подклассе. Повторное использование имени поля в подклассе приводит к тому, что новое определение переопределяет исходное.

class PastebinEdit(Form):
    language = SelectField('Programming Language', choices=PASTEBIN_LANGUAGES)
    code     = TextAreaField()

class PastebinEntry(PastebinEdit):
    name = StringField('User Name')

Метод validate_<fieldname> в качестве валидатора поля формы.

Чтобы обеспечить настраиваемую проверку/валидацию, для каждого поля формы можно определить метод с именем validate_<fieldname>, где fieldname — это имя поля:

class SignupForm(Form):
    age = IntegerField('Age')

    def validate_age(form, field):
        if field.data < 13:
            raise ValidationError("We're sorry, you must be 13 or older to register")

Атрибуты и методы экземпляра класса Form.

Атрибуты экземпляра класса Form

:

  • Form.data: словарь, содержащий данные для каждого поля. Обратите внимание, что он генерируется каждый раз, когда к нему обращаются. При неоднократном обращении — это может быть дорогостоящей операцией. Обычно используется, для перебора всех значений полей формы. Если нужно получить доступ к данным для известных полей, то должны использовать form.<field>.data, а не прокси form.data[field].
  • Form.errors: словарь, содержащий список ошибок (после проверки методом Form.validate()) для каждого поля формы. Будет пустой, если форма не была проверена или ошибок не было.
Методы экземпляра класса Form

:

  • Form.validate(): проверяет форму, вызвав функцию validate() для каждого поля. Возвращает True, если проверка прошла успешно. Если форма определяет метод `validate_ , то он добавляется как дополнительный валидатор для проверки поля.
  • Form.populate_obj(obj): заполняет атрибуты переданного объекта obj данными из полей формы.

    Примечание

    : Любой атрибут переданного obj с тем же именем, что и поле, будет переопределен. Используйте с осторожностью.

    Одним из распространенных способов использования:

    def edit_profile(request):
      user = User.objects.get(pk=request.session['userid'])
      form = EditProfileForm(request.POST, obj=user)
    
      if request.POST and form.validate():
          form.populate_obj(user)
          user.save()
          return redirect('/home')
      return render_to_response('edit_profile.html', form=form)
    

Определение полей формы

Поля формы отвечают за визуализацию и преобразование данных. Они делегируют полномочия валидаторам для проверки данных.

Поля декларативно определяются как атрибуты класса формы Form:

class MyForm(Form):
    name    = StringField('Full Name', [validators.required(), validators.length(max=10)])
    address = TextAreaField('Mailing Address', [validators.optional(), validators.length(max=200)])

Когда в форме определено поле, параметры построения сохраняются до тех пор, пока форма не будет создана. Во время создания экземпляра формы создается копия поля со всеми параметрами, указанными в определении. Каждый экземпляр поля хранит свои собственные данные поля и список ошибок.

Метка label и валидаторы могут быть переданы конструктору в качестве позиционных аргументов, в то время как все остальные аргументы должны передаваться в качестве ключевых аргументов. Некоторые поля (например, SelectField) также могут принимать дополнительные аргументы ключевых слов для конкретных полей. Обратитесь к справочнику по встроенным полям для получения информации о них.

Аргументы конструктора класса типа поля.

  • label: метка поля.
  • validators: последовательность встроенных валидаторов или вызываемых объектов, которые вызываются методом Form.validate() и по очереди применяются к значению поля.
  • filters: последовательность фильтров, которые запускаются для входных данных процессом.
  • description: описание поля, обычно используется для текста справки.
  • Id: — идентификатор для использования в поле. Разумное значение по умолчанию задается формой, и вам не нужно устанавливать его вручную.
  • default: значение по умолчанию для присвоения полю, если не предусмотрена форма или ввод объекта. Может быть вызываемым.
  • widget: если предоставляется, переопределяет виджет, используемый для визуализации поля.
  • render_kw: словарь, если предоставлен, то должен содержать ключевые слова (по умолчанию), которые будут переданы виджету widget во время рендеринга.

Встроенные типы полей для формы.

Встроенные типы полей обычно представляют скалярные типы данных с отдельными значениями и относятся к одному полю формы.

Во всех встроенных типах полей, аргумент field_arguments — это аргументы конструктора базового класса поля.

  • BooleanField(field_arguments, false_values=None): представляет . Устанавливает статус checked с помощью аргумента конструктора default. Любое значение default, например default=»checked», делает отметку в html-элементе и устанавливает данные в значение True.

    Необязательный аргумент false_values​: последовательность строк, каждая из которых является строкой точного соответствия тому, что считается ложным значением. По умолчанию используется кортеж (False, 'false', '',)

  • DateField(field_arguments, format='%Y-%m-%d'): текстовое поле, в котором хранится значение, соответствующее формату datetime.date.

  • DateTimeField(field_arguments, format='%Y-%m-%d %H:%M:%S'): текстовое поле, в котором хранится значение, соответствующее формату datetime.datetime.

  • DecimalField(field_arguments, places=2, rounding=None, use_locale=False, number_format=None): текстовое поле, в котором отображаются и приводятся данные типа decimal.Decimal.

    • places: На сколько знаков после запятой нужно округлить значение для отображения в форме. Если None, то значение не округляется.
    • rounding: Как округлить значение, например decimal.ROUND_UP. Если значение не установлено, то используется значение из контекста текущего потока.
    • use_locale: Если True, то форматирование чисел будет на основе локали. Для форматирования чисел на основе локали требуется пакет babel.
    • `number_format – Необязательный числовой формат для языкового стандарта. Если этот параметр опущен, то для языкового стандарта используется десятичный формат.
  • FileField(field_arguments): отображает поле загрузки файла. По умолчанию, значением будет имя файла, отправленное в данных формы.

    Модуль WTForms не занимается обработки файлов. Расширение Flask-WTF может заменить значение имени файла объектом, представляющим загруженные данные.

    class UploadForm(Form):
      image        = FileField('Image File', [validators.regexp('^[^/\].jpg$')])
      description  = TextAreaField('Image Description')
    
      def validate_image(form, field):
          if field.data:
              field.data = re.sub(r'[^a-z0-9_.-]', '_', field.data)
    
    def upload(request):
      form = UploadForm(request.POST)
      if form.image.data:
          image_data = request.FILES[form.image.name].read()
          open(os.path.join(UPLOAD_PATH, form.image.data), 'w').write(image_data)
    
  • MultipleFileField(field_arguments): то же самое, что и FileField, но позволяет выбирать несколько файлов.

  • FloatField(field_arguments): текстовое поле, за исключением того, что все вводимые данные приводятся к типу float. Ошибочный ввод игнорируется и не принимается в качестве значения. Для большинства применений DecimalField предпочтительнее FloatField, за исключением случаев, когда IEEE float абсолютно желателен вместо десятичного значения.

  • IntegerField(field_arguments): текстовое поле, за исключением всего ввода, приводится к целому числу. Ошибочный ввод игнорируется и не принимается в качестве значения.

  • RadioField(field_arguments, choices=[], coerce=unicode): подобно SelectField(), за исключением того, что отображает список переключателей.

    Итерация/цикл по полю приведет к созданию подполей (каждое из которых также содержит метку).

    {% for subfield in form.radio %}
      <tr>
          <td>{{ subfield }}</td>
          <td>{{ subfield.label }}</td>
      </tr>
    {% endfor %}
    

    Простой вывод поля без создания цикла, приведет к получению <ul> списка.

  • SelectField(field_arguments, choices=[], coerce=unicode, option_widget=None, validate_choice=True):

    Поля <select> принимают параметр choices, который представляет собой список пар (value, label). Это также может быть список только значений value, и в этом случае значение используется в качестве метки label. Значение может быть любого типа, но т.к. данные формы отправляются в браузер в виде строк, необходимо будет предоставить функцию coerce, которая преобразует строку обратно в ожидаемый тип.

    Пример поля SelectField() со статическими значениями

    :

    class PastebinEntry(Form):
      language = SelectField('Programming Language', choices=[('cpp', 'C++'), ('py', 'Python'), ('text', 'Plain Text')])
    
    Обратите внимание

    , что ключевое слово choices оценивается только один раз, поэтому, если надо создать динамический раскрывающийся список, то нужно будет назначить список вариантов для поля после создания экземпляра. Любые введенные варианты, которых нет в данном списке, приведут к сбою проверки в поле. НО можно пропустить проверку, передав аргумент validate_choice=False.

    Пример поля SelectField() со статическими значениями

    :

    class UserDetails(Form):
      group_id = SelectField('Group', coerce=int)
    
    def edit_user(request, id):
      user = User.query.get(id)
      form = UserDetails(request.POST, obj=user)
      form.group_id.choices = [(g.id, g.name) for g in Group.query.order_by('name')]
    
    Обратите внимание

    , что мы не передали варианты выбора конструктору SelectField, а создали список в функции-представлении. Кроме того, ключевое слово coerce для SelectField() говорит о том, что для приведения данных формы используется int().

  • SelectMultipleField(field_arguments, choices=[], coerce=unicode, option_widget=None): ни чем не отличается от обычного поля SelectField(), за исключением того, что оно может принимать и проверять несколько вариантов. Для этого необходимо указать HTML-атрибут size для поля <select> при рендеринге.

  • SubmitField(field_arguments): представляет <input type="submit">. Это позволяет проверить, была ли нажата данная кнопка отправки.

  • StringField(field_arguments): это поле является основой для большинства более сложных полей и представляет собой <input type="text">.

  • HiddenField(field_arguments): это строковое поле с виджетом HiddenInput. Оно будет отображаться как , но в противном случае принудительно преобразуется в строку.

    Скрытые поля похожи на любое другое поле в том смысле, что они могут принимать валидаторы и значения и быть доступны в объекте формы. Рассмотрите возможность проверки/валидации скрытых полей так же, как обычных.

  • PasswordField(field_arguments): поле ввода пароля <input type="password">, всегда преобразуется в строку. Кроме того, любое значение, принятое этим полем, не отображается обратно в браузер, как обычные поля.

  • TextAreaField(field_arguments): поле представляет собой текстовое поле <textarea> и может использоваться для многострочного ввода.


Встроенные валидаторы полей.

Поля формы, определенные в подклассе класса Form, могут проверяться встроенными валидаторами, определенными в wtform.validators. Встроенные валидаторы передаются списком, как аргумент типа поля формы.

from wtforms import Form, PasswordField
from wtforms.validators import InputRequired, EqualTo

class ChangePassword(Form):
    password = PasswordField('Новый пароль', [InputRequired(), 
                        EqualTo('confirm', message='Пароли должны совпадать')])
    confirm  = PasswordField('Повторите Пароль')

Список встроенных валидаторов, определяемых в wtforms.validators:

  • Email(message=None, granular_message=False, check_deliverability=False, allow_smtputf8=True, allow_empty_local=False):

    • message: сообщение об ошибке, которое должно появиться в случае ошибки проверки.
    • granular_messsage: сообщение об ошибке проверки из модуля email_validator (по умолчанию False).
    • check_deliverability: выполняет проверку разрешения доменных имен (по умолчанию False).
    • allow_smtputf8: вызывает ошибку проверки адресов, для которых требуется SMTPUTF8 (по умолчанию True).
    • allow_empty_local: разрешает пустую локальную часть (т. е. @example.com), например, для проверки псевдонимов (по умолчанию False).
  • EqualTo(fieldname, message=None): сравнивает значения двух полей.

    Этот валидатор можно использовать для облегчения одного из наиболее распространенных сценариев формы смены пароля:

    • fieldname: имя другого поля для сравнения.
    • message: – сообщение об ошибке, которое должно появиться в случае ошибки проверки. Можно интерполировать с помощью %(other_label)s и %(other_name)s, чтобы обеспечить более полезную ошибку.
    from wtforms import Form, PasswordField
    from wtforms.validators import InputRequired, EqualTo
    
    class ChangePassword(Form):
      password = PasswordField('Новый пароль', [InputRequired(), 
                          EqualTo('confirm', message='Пароли должны совпадать')])
      confirm  = PasswordField('Повторите Пароль')
    

    Здесь используется валидатор InputRequired(), чтобы предотвратить попытку валидатора EqualTo() проверить, не совпадают ли пароли, если пароли не были указаны вообще. Поскольку InputRequired() останавливает цепочку проверки, то EqualTo() не запускается в случае, если поле пароля остается пустым.

  • InputRequired(message=None): проверяет, что для поля были предоставлены данные. Другими словами, значение поля — не пустая строка. Этот валидатор также устанавливает флаг обязательного поля формы для заполнения.

  • IPAddress(ipv4=True, ipv6=False, message=None): проверяет IP-адрес. Аргумент Ipv4 — если True, принимать адреса IPv4 как действительные (по умолчанию True). Аргумент Ipv6 — если True, принимать IPv6-адреса как действительные (по умолчанию False)

  • Length(min=- 1, max=- 1, message=None): проверяет длину строки. Аргумент min — минимальная необходимая длина строки. Если не указан, минимальная длина проверяться не будет. Аргумент max — максимальная длина строки. Если не указан, максимальная длина проверяться не будет.

  • MacAddress(message=None): проверяет MAC-адрес. Аргумент message — сообщение об ошибке, которое будет выдано в случае ошибки проверки.

  • NumberRange(min=None, max=None, message=None): проверяет, что число имеет минимальное и/или максимальное значение включительно. Это будет работать с любым сопоставимым типом чисел, таким как числа с плавающей запятой и десятичные дроби, а не только с целыми числами.

  • Optional(strip_whitespace=True): разрешает пустой ввод (необязательное поле) и останавливает продолжение цепочки проверки. Если ввод пуст, также удаляются предыдущие ошибки из поля (например, ошибки обработки). Если аргумент strip_whitespace=True (по умолчанию), то также остановит цепочку проверки, если значение поля состоит только из пробелов.

  • Regexp(regex, flags=0, message=None): проверяет поле на соответствие регулярному выражению, предоставленному пользователем. Аргумент regex — cтрока регулярного выражения для использования. Также может быть скомпилированным шаблоном регулярного выражения. Аргумент flags — используемые флаги регулярного выражения, например re.IGNORECASE. Игнорируется, если регулярное выражение не является строкой.

  • URL(require_tld=True, message=None): простая проверка URL на основе регулярного выражения. Вероятно потребуется его проверка на доступность другими способами.

  • UUID(message=None): проверяет UUID.

  • AnyOf(values, message=None, values_formatter=None): сравнивает входящие данные с последовательностью допустимых входных данных. Аргумент values ​​- последовательность допустимых входных данных. Аргумент values_formatter — функция, используемая для форматирования списка значений в сообщении об ошибке message.

  • NoneOf(values, message=None, values_formatter=None): сравнивает входящие данные с последовательностью неверных входных данных. Аргумент values ​​- последовательность допустимых входных данных. Аргумент values_formatter — функция, используемая для форматирования списка значений в сообщении об ошибке message.

Создание собственного валидатора.

Выше было показано использование встроенного в класс Form валидатора (как метода с определенным именем) для проверки одного поля. Встроенные валидаторы хороши, но их сложно использовать повторно.

Так как тип поля формы принимает аргумент validators в качестве последовательности вызываемых объектов, то это может быть простая функция Python, которая возвращает значение bool. НО лучше использовать фабричную функцию, которая возвращает вызываемый объект:

def length(min=-1, max=-1, message=None):
    if not message:
        message = f'Must be between {min} and {max} characters long.'

    def _length(form, field):
        l = field.data and len(field.data) or 0
        if l < min or max != -1 and l > max:
            raise ValidationError(message)

    return _length

# использование валидатора `length`
class MyForm(Form):
    name = StringField('Name', [InputRequired(), length(max=50)])

Forms are an important element of any web application, but unfortunately they are quite difficult to work with. First you need to validate the data on the client side, then on the server. And even this is not enough if the application developer is concerned about security issues such as CSRF, XSS, SQL Injection, and so on. All together, it’s a lot of work. Fortunately, there is a great library called WTForms that does most of the work for the developer. Before learning more about WTForms, you should still figure out how to work with forms without libraries and packages.

Working with forms is a tricky part

First, we create a login.html template with the following code:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>

    {% if message %}
        <p>{{ message }}</p>
    {% endif %}
    
    <form action="" method="post">
        <p>
	    <label for="username">Username</label>
	    <input type="text" name="username">
	</p>
	<p>
	    <label for="password">Password</label>
	    <input type="password" name="password">
	</p>
	<p>
	    <input type="submit">
	</p>
    </form>
    
</body>
</html>

This code should be added after the books() view function in the main2.py file:

from flask import flask, render_template, request
#...
@app.route('/login/', methods=['post', 'get'])
def login():
    message = ''
   if request.method == 'POST':
	username = request.form.get('username') # request the form data
	password = request.form.get('password')

   if username == 'root' and password == 'pass':
	message = "Correct username and password"
   else:
	message = "Wrong username or password"

   return render_template('login.html', message=message)
#...

Note that the methods argument is passed to the route() decorator. By default the request handler is only called when the request. method is GET or HEAD. You can change this by passing a list of allowed HTTP methods to the methods keyword argument. From now on, the login view function will only be called when a request to /login/ is made using GET, POST or HEAD methods. If you try to access the /login/ URL using another method, you’ll get an HTTP 405 Method Not Allowed error. In the past tutorials, we’ve discussed how the request object provides information about the current web request. The information from the form is stored in the form attribute of the request object. the request.form is an immutable dictionary type object known as ImmutableMultiDict. Next, you need to start the server and go to https://localhost:5000/login/. This form will open.Working with Flask forms The request to the page was made using the GET method, so the code inside the if block of the login() function is omitted. If you try to submit the form without entering any data, the page will look like this: Working with Flask forms This time the page was sent by POST, so the code inside the if was executed. Inside this block, the application takes the username and password and sets the message for the message. Because the form was empty, an error message was displayed. If you fill in the form with the correct username and password and press Enter, the welcome message "Correct username and password" appears:Working with Flask forms This is how you can work with forms in Flask. Now we should pay attention to the WTForms package.

WTForms

WTForms is a powerful library written in Python and is independent of frameworks. It knows how to generate forms, validate them and pre-fill them with information (handy for editing) and much more. It also offers protection against CSRF. Flask-WTF is used to install WTForms. Flask- WTF is an extension for Flask that integrates WTForms into Flask. It also offers additional features such as file uploads, reCAPTCHA, internationalization (i18n), and others. To install Flask-WTF, enter the following command.

(env) [email protected]:~/flask_app$ pip install flask-wtf

Creating the Form class

We should start by defining forms as Python classes. Each form should extend the FlaskForm class from the flask_wtf package. FlaskForm is a wrapper that contains useful methods for the original wtform.Form class, which is the main class for creating forms. Inside the form class, form fields are defined as class variables. Form fields are defined by creating an object associated with the field type. The wtform package offers several classes representing the following fields: StringField, PasswordField, SelectField, TextAreaField, SubmitField, and others. First, you need to create a forms.py file inside the flask_app dictionary and add the following code to it.

from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, TextAreaField
from wtforms.validators import DataRequired, Email

class ContactForm(FlaskForm):
    name = StringField("Name: ", validators=[DataRequired()])
    email = StringField("Email: ", validators=[Email()])
    message = TextAreaField("Message", validators=[DataRequired()])
    submit = SubmitField("Submit")

Here you define a ContactForm form class with four fields: name, email, message, and sumbit. These variables will be used to render the form fields and assign and retrieve information from them. This form is created with two StringFields, a TextAreaField and a SumbitField. Each time a field object is created, certain arguments are passed to its constructor function. The first argument is a string containing the label that will be displayed inside the <label> tag when the field is rendered. The second optional argument is a list of validators (elements of the validation system), which are passed to the constructor as keyword arguments. Validators are functions or classes that determine whether the information entered into a field is correct. Multiple validators can be used for each field, separated by commas (,). The wtforms.validators module offers basic validators, but you can create them yourself. This form uses two built-in validators: DataRequired and Email. DataRequired: it checks if the user entered any information in the field. Email: it checks if the email address entered is valid. The entered data will not be accepted until the validator validates the data. Note: this is just the basis of the form fields and validators. A complete list is available at https://wtforms.readthedocs.io.

SECRET_KEY

setting By default, Flask-WTF prevents any variants of CSFR attacks. This is done by embedding a special token in the hidden <input> element inside the form. This token is then used to authenticate the request. Before Flask-WTF can generate a csrf-token, a secret key must be added. Set it in the main2.py file as follows:

#...
app.debug = True
app.config['SECRET_KEY'] = 'a really really really long secret key'

manager = Manager(app)
#...

The config attribute of the Flask object is used here. The config attribute works as a dictionary and is used to place Flask configuration parameters and Flask extensions, but you can also add them yourself. The secret key should be a string – one that is hard to figure out and preferably long. SECRET_KEY is not only used to create CSFR tokens. It is used in other Flask extensions as well. The secret key must be stored securely. Instead of storing it in your application, it is better to store it in an environment variable. How to do this will be explained in the following sections.

Console Forms

We open a Python shell with the following command:

(env) [email protected]:~/flask_app$ python main2.py shell

This will launch the Python shell inside the application context. Now you need to import the ContactForm class and create an instance of the new form object by passing the form data.

>>>
>>> from forms import ContactForm
>>> from werkzeug.datastructures import MultiDict
>>>
>>>
>>> form1 = ContactForm(MultiDict([('name', 'jerry'),('email', '[email protected]')])
>>>

Note that the data is passed as a MultiDict object because the wtforms.Form class constructor function takes a MutiDict type argument. If the form data is not defined when creating the form object instance and the form is sent using a POST request, wtforms.Form uses the data from the request.form attribute. It’s worth remembering that request.form returns an object of type ImmutableMultiDict. This is the same as MultiDict, but it is immutable. The validate() method validates the form. If the validation succeeds, it returns True, if not, it returns False.

>>>
>>> form1.validate()
False
>>>

The form failed because no data was passed to the mandatory message field when the form object was created. You can access form errors by using the errors attribute of the form object:

>>>
>>> form1.errors
{'message': ['This field is required.'], 'csrf_token': ['The CSRF token is missing.']}
>>>

Note that in addition to the error message for the message field, the output also contains an error message about a missing csfr token. This is because there is no POST request with a csfr-token in the form data. You can disable CSFR protection by passing csfr_enabled=False when creating an instance of the form class. Example:

>>> form3 = ContactForm(MultiDict([('name', 'spike'),('email', '[email protected]')]), csrf_enabled=False)
>>>
>>> > form3.validate()
False
>>>
>>> form3.errors
{'message': ['This field is required.']}
>>>
>>>

As expected, the error now appears only for the message field. Now you can create another form object, but this time pass information for all fields to it.

>>>
>>> > form4 = ContactForm(MultiDict([('name', 'jerry'), ('email', '[email protected]'), ('message', 'hello tom')]), csrf_enabled=False)
>>>
>>> > form4.validate()
True
>>>
>>> form4.errors
{}
>>>

Checking the form this time was successful. The next step is to render the form.

Rendering the form

There are two options for rendering:

  1. One by one.
  2. Using the loop

Rendering the fields one by one

Since templates have access to the form instance, you can use field names to render names, labels, and errors:

{# output the name of the field #}
{{ form.field_name.label() }}

{# output the field itself #}
{{ form.field_name() }}

{# output validation errors associated with the field #}
{% for error in form.field_name.errors %}
    {{ error }}
{% endfor %}

It’s worth testing this method in the console:

>>>
>>> > from forms import ContactForm
>>> from jinja2 import Template
>>>
>>> form = ContactForm()
>>>

Here the form object instance was created without request data. This is what happens when the form is displayed for the first time with a GET request.

>>>
>>>
>>> > Template("{{ form.name.label() }}").render(form=form)
'<label for="name">Name: </label>'
>>>
>>> > Template("{{ form.name() }}").render(form=form)
'<input id="name" name="name" type="text" value="">'
>>>
>>>
>>> > Template('{{ form.email.label() }}").render(form=form)
'<label for="email">Email: </label>'
>>>
>>> > Template("{{ form.email() }}").render(form=form)
'<input id="email" name="email" type="text" value="">'
>>>
>>>
>>> > Template('{{ form.message.label() }}").render(form=form)
'<label for="message">Message</label>'
>>>
>>> > Template("{{ form.message() }}").render(form=form)
'<textarea id="message" name="message"></textarea>'
>>>
>>>
>>> > Template("{{ form.submit() }}").render(form=form)
'<input id="submit" name="submit" type="submit" value="Submit">'
>>>
>>>

Since this is the first time the form is displayed, the fields will not have validation errors. The following code demonstrates this clearly:

>>>
>>>
>>> > Template("{% for error in form.name.errors %}{{ error }}{% endfor %}").render(form=form)
''
>>>
>>>
>>> > Template("{% for error in form.email.errors %}{{ error }}{% endfor %}").render(form=form)
''
>>>
>>>
>>> > Template("{% for error in form.message.errors %}{{ error }}{% endfor %}").render(form=form)
''
>>>
>>>

Instead of displaying validation errors for each field, you can use form.errors to access validation errors relevant to the form. forms.errors is used to display validation errors at the top of the form.

>>>
>>> > Template("{% for error in form.errors %}{{ error }}{% endfor %}").render(form=form)
''
>>>

When rendering fields and labels, you can add additional keyword arguments that will end up as key-value pairs in the HTML code. For example:

>>>
>>> > Template('{{ form.name(class="input", id="simple-input") }}').render(form=form)
'<input class="input" id="simple-input" name="name" type="text" value="">'
>>>
>>>
>>> > Template('{{ form.name.label(class='lbl') }}').render(form=form)
'<label class="lbl" for="name">Name: </label>'
>>>
>>>

Suppose the form has been submitted. Now you can try rendering the fields and see what happens.

>>>
>>> > from werkzeug.datastructures import MultiDict
>>>
>>> form = ContactForm(MultiDict([('name', 'spike'),('email', '[email protected]')])
>>>
>>> form.validate()
False
>>>
>>>
>>> > Template('{{ form.name() }}').render(form=form)
'<input id="name" name="name" type="text" value="spike">'
>>>
>>>
>>> > Template("{{ form.email() }}").render(form=form)
'<input id="email" name="email" type="text" value="[email protected]">'
>>>
>>>
>>> > Template("{{ form.message() }}").render(form=form)
'<textarea id="message" name="message"></textarea>'
>>>
>>>

Note that the value attribute has data in the name and email fields. But the <textarea> element for the message field is empty because no data was passed to it. To access the validation error for the message field you can do the following:

>>>
>>> > Template("{% for error in form.message.errors %}{{ error }}{% endfor %}").render(form=form)
'This field is required.'
>>>

Alternatively, form.errors can be used to go through all validation errors at once.

>>>
>>> s ="""
... {% for field_name in form.errors %}
... {% for error in form.errors[field_name] %}
... <li>{{ field_name }} {{ error }}}</li>
... {% endfor %}
... {% endfor %}
... """
>>>
>>> > Template(s).render(form=form)
'<li> csrf_token: The CSRF token is missing.</li>n
<li> message: This field is required.</li>n'
>>>
>>>

Note that there is no csfr-token error because the request was sent without a token. You can render the csfr field just like any other field:

>>>
>>> > Template("{{ form.csrf_token() }}").render(form=form)
'<input id="csrf_token" name="csrf_token" type="hidden" value="IjZjOTBkOWM4ZmQ0MGMzZTY3NDc3ZTNiZDIxZTFjNzMGU1YzEwOTYi.DQlFlA.GQ-PrxsCJkQfoJ5k6i5YfZMzC7k">'
>>>

Rendering fields one by one can take a long time, especially if there are several of them. A loop is used for such cases.

Rendering fields with a loop

The following code demonstrates how you can render fields using a for loop.

>>>
>>> s = " ""
...     <div>
... 	    {{ form.csrf_token }}
... 	</div>
... {% for field in form if field.name != 'csrf_token' %}
... 	<div>
... 	    {{ field.label() }}
... 	    {{ field() }}
... 	    {% for error in field.errors %}
... 		<div class="error">{{ error }}</div>
... 	    {% endfor %}
... 	</div>
... {% endfor %}
... """
>>>
>>>
>>> > print(Template(s).render(form=form))
   <div>
	<input id="csrf_token" name="csrf_token" type="hidden" value="IjZjOTBkOWM4ZmQ0MGMZZTY3NDc3ZTNiZxDIZTFjNDAzMGU1YzEwOTYi.DQlFlA.GQ-PrxsCJkQfoJ5k6i5YfZMzC7k">
	
   </div>
    
   <div>
	<label for="name">Name: </label>
	<input id="name" name="name" type="text" value="spike">
	
   </div>

   <div>
	<label for="email">Email: </label>
	<input id="email" name="email" type="text" value="[email protected]">
	
   </div>
    
   <div>
	<label for="message">Message</label>
	<textarea id="message" name="message"></textarea>
	
	   <div class="error">This field is required.</div>
	    
   </div>

   <div>
	<label for="submit">Submit</label>
	<input id="submit" name="submit" type="submit" value="Submit">
	
   </div>
>>>
>>>

It is important to note that regardless of the method used, you will need to manually add a <form> tag to wrap the form fields. Now that you know how to create, validate, and render forms, you can use what you’ve learned to create real forms. First, you need to create a contact.html template with the following code:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<form action="" method="post">

    {{ form.csrf_token() }}

    {% for field in form if field.name != "csrf_token" %}
	<p>{{ field.label() }}</p>
	<p>{{ field }}
	    {% for error in field.errors %}
		{{ error }}
	    {% endfor %}
	</p>
    {% endfor %}

</form>

</body>
</html>

The only missing piece of the puzzle is the view function, which will be created next.

Working with the form confirmation

Let’s open main2.py to add the following code after the login() view function.

from flask import Flask, render_template, request, redirect, url_for
from flask_script import Manager, Command, Shell
from forms import ContactForm
#...
@app.route('/contact/', methods=['get', 'post'])
def contact():
    form = ContactForm()
   if form.validate_on_submit():
	name = form.name.data
	email = form.email.data
	message = form.message.data
	print(name)
	print(email)
	print(message)
	# here is the database logic
	print("nData received. Now redirecting ...")
	return redirect(url_for('contact'))

   return render_template('contact.html', form=form)
#...

On line 7, the form object is created. Line 8 checks the value that the validate_on_submit() method returned to execute the code inside the if instruction. Why use validate_on_submit() instead of validate(), as it was in the console? validate() only checks if the form data is correct. It does not check if the request was submitted with a POST method. This means that if the validate() method is used, a GET request to /contact/ will run the validate form and the user will see validation errors. Generally, the validation is only executed if the data was submitted using the POST method. Otherwise it returns False. The validate_on_submit() method calls the validate() method internally. Also, note that no data is passed when a form object instance is created because when the form is submitted via POST request, WTForm reads the form data from the request.form attribute. The form fields defined in the form class become attributes of the form object. To access the field data, the form field data attribute is used:

form.name.data # access the data in the name field.
form.email.data # access the data in the emailfield.

To access all of the form data at once, you must use the data attribute to the form object:

form.data # access to all data

If you use a GET request when you visit /contact/, the validate_on_sumbit() method will return False. The code inside the if will be skipped and the user will get an empty HTML form. When the form is submitted with a POST request, validate_on_sumbit() will return True, assuming the data is correct. Calls to print() within the if block will print the user’s input and redirect() will redirect the user to /contact/. On the other hand, if validate_on_sumbit() returns False, the instructions within the if body will be skipped and a validation error will be reported. If the server is not running, you have to start it and open https://localhost:5000/contact/. The following contact form will appear:ошибка валидации формы во Flask If you try to click Submit without entering data, the following validation error messages will appear:ошибка валидации email во Flask You can now enter certain data in the Name and Message fields and invalid data in the Email field, and try to submit the form again.Working with Flask forms Note that all fields contain data from the previous query. Now you can enter the correct email in the Email field and click Submit. Now the check will be successful and the following output will appear in the shell:

Spike
[email protected]
A Message

Data received. Now redirecting ...

After the accepted data is displayed in the shell, the view function will redirect the user to /contact/. At this point, a blank form with no validation errors should be displayed as if the user had first opened /contact/ with a GET request. It is recommended to display feedback to the user after a successful submission. In Flask, this is done with popup messages.

Popup messages

Pop-up messages are another one of those features which depend on a secret key. It is needed because messages are stored in sessions. Sessions in Flask will be the subject of a separate lesson. Since the secret key has already been set up in this lesson, we can move on. The flash() function from the flask package is used to display the message. The flash() function takes two arguments: message and category (optional). The category indicates the type of the message: _success_, _error_, _warning_, and so on. The category can be used in the template to define the message type. Open main2.py again to add flash("Message Received", "success") just before calling redirect() in the contact() view:

from flask import flask, render_template, request, redirect, url_for, flash
#...
	# here's the database logic
	print("nData received. Now redirecting ...")
	flash("Message Received", "success")
	return redirect(url_for('contact'))
   return render_template('contact.html', form=form)

The message specified with flash() will only be available to the subsequent request, and then will be deleted. This is only a customization of the message. To display it, you must also change the template. To do this, open the file contact.html and change it as follows: Jinja offers the function get_flashed_messages() which returns a list of active messages without category. To get them together with a category, you need to pass with_category=True when you call get_flashed_messages(). When with_categories is True, get_flashed_messages() will return a list of form tuples (category, message). After these changes, you should open https://localhost:5000/contact again. Fill in the form and click Submit. A message about successful submission will appear at the top of the form.Working with Flask forms

Introduction

Form validation is one of the most essential components of data entry in web applications. Users can make mistakes, some users are malicious. With input validation, we protect our app from bad data that affects business logic and malicious input meant to harm our systems

Trying to process unvalidated user inputs can cause unexpected/unhandled bugs, if not a server crash. In this context, validating data means verifying input and checking if it meets certain expectations or criteria(s). Data validation can be done on both the front and back end.

In this tutorial, we will learn how to validate user input in Flask forms using the Flask-WTForms extension.

By the end of this tutorial, we will have the following user registration form with validation criteria:

We will use Flask version 1.1.2 and Flask-WTF with version 0.14.3.

Setup

While not necessary, we recommend you create a virtual environment to follow along:

$ mkdir flask-form-validation
$ cd flask-form-validation
$ python3 -m venv .
$ . bin/activate

In your activated virtual environment, we will install our packages by typing:

$ pip install Flask Flask-WTF

Note that if you want to use email validation, you’ll also need to install the email_validator package (current version is 1.1.1):

$ pip3 install email_validator

Now let’s create our necessary files. We’ll start by creating a basic app.py, which, for simplicity, will contain our Flask app, routes, and forms:

from flask import Flask, render_template

app = Flask(__name__, template_folder='.')
app.config['SECRET_KEY']='LongAndRandomSecretKey'

We created a Flask object and set template_folder to the current folder. We then assigned the Flask object into app variable. We added SECRET_KEY to our app object’s configuration.

The SECRET_KEY is commonly used for encryption with database connections and browser sessions. WTForms will use the SECRET_KEY as a salt to create a CSRF token. You can read more about CSRF on this wiki page.

If your application already uses the SECRET_KEY config for other purposes, you would want to create a new one for WTForms. In that case, you can set the WTF_CSRF_SECRET_KEY config.

Let’s create and add a form to our current app.py:

from flask import Flask, render_template
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField

class GreetUserForm(FlaskForm):
    username = StringField(label=('Enter Your Name:'))
    submit = SubmitField(label=('Submit'))

# ...

Our simple GreetUserForm class contains a StringField. As the name implies, this field expects and will return a string value (you can always convert that input to other data types as the need arises). The name of the field is username, and we’ll use this name to access data of the form element.

The label paremeters are what will be rendered on our page so that users would understand what data a form element captures. We also have a submit button, which will try to submit the form if all fields pass our validation criteria.

Now that we’re set up, let’s use WTForms to validate our data!

Flask Form Validation With Flask-WTForms

Let’s begin by creating a route to display and process our form:

# ...

@app.route('/', methods=('GET', 'POST'))
def index():
    form = GreetUserForm()
    if form.validate_on_submit():
        return f'''<h1> Welcome {form.username.data} </h1>'''
    return render_template('index.html', form=form)

Our route has GET and POST methods. The GET method displays the form, whereas the POST method processes the form data on submission. We set the URL path to /, or the root URL, so it will appear as our web app’s home page. We render the index.html template and pass the form object as a parameter.

Let’s pause and pay close attention to this line: if form.validate_on_submit():. This rule says ‘if the request method is POST and if the form field(s) are valid, then proceed. If our form input passes our validation criteria, on the next page a simple greet message will be rendered with the user’s name. Notice here we used field name (username) to access input data.

To see the form, we need to create the index.html template. Create the file and add the following code to it:

<form method="POST" action="">
    <div class="form-row">
        <div class="form-group col-md-6">
            {{ form.csrf_token() }}
            <label for=""> {{ form.username.label }}</label>
            {{ form.username }}
        </div>
        <div class="form-group">
            {{ form.submit(class="btn btn-primary")}}
        </div>
    </div>
</form>

We use our form object to pass WTform elements into Jinja2 — the template parser for Flask.

Note: The csrf_token is generated automatically by the WTForms and it changes each time the page is rendered. This helps us to protect our site against CSRF attacks. By default, it is a hidden field. You could also choose to use {{ form.hidden_field() }} to render all hidden fields, including CSRF token, but that’s not advised.

Now, let’s go to our terminal to start our Flask app by typing:

$ FLASK_ENV=development flask run

For convenience, we set the FLASK_ENV environment variable to ‘development’ while developing. This allows the app to hot-reload each time we hit save. For Windows you may have to use set FLASK_ENV=development into your terminal/console before running your flask app.

Here’s what we’ll see if we navigate to the localhost:

Type a name in the input field and submit the form. You will see the greetings message we defined in our route:

It works as expected. But what if we didn’t type anything into the input field? It would still validate the form:

Let’s prevent that from happening and only allow users who typed their names to see the next page. To do so, we need to ensure that our username field has input data.

We’ll import one of the built-in WTForms validation methods: DataRequired() from wtforms.validators and pass it into our username field.

Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!

# ...
from wtforms.validators import ValidationError, DataRequired

class GreetUserForm(FlaskForm):
    username = StringField(label=('Enter Your Name:'),
                           validators=[DataRequired()])
    submit = SubmitField(label=('Submit'))

# ...

Notice that we are passing the validators parameter as a list. This tells us that we can have multiple validators for each field.

Now that we are using DataRequired(), the username field will not be validated if there is no input data:

In fact, if we right-click and inspect the form element, we’ll see that WTForms automatically added the required attribute to the input field:

By doing so, WTForms adds a basic front-end validation to our form field. You wouldn’t be able to submit that form without the username field even if you try to post the form using tools like cURL or Postman.

Now, let’s say we want to set a new validation rule that will only allow names that are at least 5 characters long. We can use the Length() validator with min parameter:

# ...
from wtforms.validators import ValidationError, DataRequired, Length

class GreetUserForm(FlaskForm):
    username = StringField(label=('Enter Your Name:'), 
    	validators=[DataRequired(), Length(min=5)])
    submit = SubmitField(label=('Submit'))

# ...

If we try to submit the form with input data less than 5 chars long, the validation criteria will not be fulfilled, and the submission will fail:

Clicking on the submit button does nothing for invalid data, it also does not display any error to the user. We need to provide error messages so the user would understand what’s going on and how to fix it.

In our index.html template, right under the {{ form.username }}, add the following Jinja2 for-loop to display errors:

 {% for field, errors in form.errors.items() %}
    <small class="form-text text-muted ">
        {{ ', '.join(errors) }}
    </small>
{% endfor %}

Our form can render clean validation errors now:

For any reason, if we need to limit the maximum length of our field data, we can do it by passing the max parameter to the Length() validator. It’s also possible to customize the error message by passing an optional message parameter with a custom error string.

Let’s update the username field accordingly:

# ...

class GreetUserForm(FlaskForm):
    username = StringField(label=('Enter Your Name:'),
        validators=[DataRequired(), 
        Length(min=5, max=64, message='Name length must be between %(min)d and %(max)dcharacters') ])
    submit = SubmitField(label=('Submit'))

# ...

More WTForms Fields and Validators With the User Registration Form

Our current form has a single field, which is kind of dull. WTForms provides extensive form validation criteria and a variety of form fields, so let’s take advantage of it and create something with practical use.

We’ll create a user registration form and use built-in WTForms validators.

We will use the DataRequired() validator for the fields that we want to make sure that the user fills in. We’ll check the minimum and maximum length of the fields with Length() validator, validate emails with Email() validator and check if two fields contain the same data with EqualTo() validator.

Remove the GreetUserForm class and replace the beginning of your code with our new form:

from flask import Flask, render_template
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, 
    SubmitField
from wtforms.validators import ValidationError, DataRequired, 
    Email, EqualTo, Length

class CreateUserForm(FlaskForm):
    username = StringField(label=('Username'), 
        validators=[DataRequired(), 
        Length(max=64)])
    email = StringField(label=('Email'), 
        validators=[DataRequired(), 
        Email(), 
        Length(max=120)])
    password = PasswordField(label=('Password'), 
        validators=[DataRequired(), 
        Length(min=8, message='Password should be at least %(min)d characters long')])
    confirm_password = PasswordField(
        label=('Confirm Password'), 
        validators=[DataRequired(message='*Required'),
        EqualTo('password', message='Both password fields must be equal!')])

    receive_emails = BooleanField(label=('Receive merketting emails.'))

    submit = SubmitField(label=('Submit'))

# ...    

We have four different fields in our forms. The last one is a regular submit button. We used StringField to get string input from users, such as username and email. On the other hand, PasswordField hides the password text on the front-end. BooleanField renders as a checkbox on the front-end since it only contains either True (Checked) or False (Unchecked) values.

We need to modify out index.html template to render our new form fields:

<div class="container">
    <h2>Registration Form</h2>
    {% for field, errors in form.errors.items() %}
    {{ ', '.join(errors) }}
    {% endfor %}
    <form class="form-horizontal" method="POST" action="">
        {{ form.csrf_token() }}
        <div class="form-group">
            {{ form.username.label }}
            {{ form.username(class="form-control") }}
        </div>
        <div class="form-group">
            {{ form.email.label }}
            {{ form.email(class="form-control") }}
        </div>
        <div class="form-group">
            {{ form.password.label }}
            {{ form.password(class="form-control") }}
        </div>
        <div class="form-group">
            {{ form.confirm_password.label }}
            {{ form.confirm_password(class="form-control") }}
        </div>
        <div class="form-group">
            {{ form.receive_emails.label }}
        </div>
        <div class="form-group">
            {{ form.submit(class="btn btn-primary")}}
        </div>
    </form>
</div>

Our form fields rendered properly as you can see:

Note: If your website is going to have multiple different forms, you may want to use Jinja2 macros instead of typing each form field one by one. Using macros is beyond the scope of this article, but it greatly speeds up the form creation processes.

Creating Your Own Custom Validators

In most web sites, certain characters are not allowed in usernames. It can be for security purposes, it can be for cosmetics. WTForms doesn’t have that logic by default but we can define it ourselves.

WTForms allows us to add custom validators by adding a validation method to our UserRegistrationForm class. Let’s implement that custom validation into our form by adding the validate_username() method right below the submit button.

# ...

class UserRegistrationForm(FlaskForm):
    # ...
    submit = SubmitField(label=('Submit'))

    def validate_username(self, username):
        excluded_chars = " *?!'^+%&/()=}][{$#"
        for char in self.username.data:
            if char in excluded_chars:
                raise ValidationError(
                    f"Character {char} is not allowed in username.")
                
# ...

We can add as many or as few validation methods as we like. WTForms will run validation methods automatically once defined.

The ValidationError class gives us a convenient way to define our custom validation message. Note that you will need to import it from wtforms.validators before using it.

Let’s test this new method by entering proper data to all fields except the username field, which will contain an excluded character — ‘%’.

As you can see, our custom validation method runs perfectly and provides us with a clean validation error, which helps us to understand what’s wrong with our input data. Doing so, greatly improves the user experience.

You can use external libraries, your database, or APIs to combine with WTForms and to validate the incoming input data. When you want to capture {{ form.some_field.data }} and write into or query from the database, use WTForms validators to ensure it’s safe to be saved.

Note: We’ve excluded most of the HTML codes out since they are not directly related to our tutorial. The full code will be available on this GitHub repository, in case you want to check out.

Conclusion

Validating data is one of the most essential parts of the Flask web applications. Flask-WTforms provides very powerful and easy to learn ways to handle form data.

Now that you know the fundamentals of data validation with Flask-WTF, you can go ahead and apply your own validation logic and/or implement your own methods for both security and better user experience.

Понравилась статья? Поделить с друзьями:
  • Validation error data incomplete in file etc x11 xorg conf
  • Validate signature error sig error trust wallet
  • Validate signature error but it is not contained of permission
  • Valid data error перевод
  • Valiant ошибка невозможно установить