Сейчас мы напишем простенький функционал, дающий представление реализации работы форм, на примере страницы регистрации в нашем приложении. Углубиться же в своих познаниях лучше вооружившись документацией (ссылка будет) и тематическими статьями.
Создадим каркас приложения.
from flask import Flask, request
app = Flask(__name__)
@app.route('/registration', methods=['POST'])
def registration():
pass
if __name__ == '__main__':
app.run(debug=True)
Приложение имеет единственный эндпойнт, который будет работать с POST-запросами. По идее, на странице domen.ru/registration юзера будет ждать форма с обязательными и необязательными полями, который он будет заполнять, чтобы продолжить пользоваться функционалом. И для реализации такой формы нет необходимости придумывать велосипед, можно просто воспользоваться готовым функционалом расширения Flask-WTF. Но для начала потребуется его установить консольной командой:
pip install Flask-WTF
Сразу оставлю ссылку на официальную документацию расширения.
Делаем импорт объекта FlaskForm и напишем саму форму в виде класса, наследованного от FlaskForm.
from flask_wtf import FlaskForm
class RegistrationForm(FlaskForm):
email = StringField()
phone = IntegerField()
name = StringField()
address = StringField()
index = IntegerField()
comment = StringField()
Функцию registration() заполним следующим содержимым:
@app.route('/registration', methods=['POST'])
def registration():
form = RegistrationForm()
if form.validate_on_submit():
ame = form.name.data
print(form)
print(email, phone, name, password)
return f'Hello {name} welcome to our site!'
return f'{form.errors}', 404
Созданный экземпляр form содержит в себе все переданные пользователем сведения и они легко могут быть вызваны. Сейчас обязательных полей нет, поэтому данные могут быть пустыми. Принты в код добавлены для наглядности и тестов.
Так же дополним наше приложение строкой, которая отключит проверку безопасности WTF, чтобы спокойно тестить. В коде это будет выглядеть так:
if __name__ == '__main__':
app.config["WTF_CSRF_ENABLED"] = False #отключаем проверку безопасности WTForms
app.run(debug=True)
Код нашего приложения должен выглядеть так:
from flask import Flask, request
from flask_wtf import FlaskForm
app = Flask(__name__)
class RegistrationForm(FlaskForm):
email = StringField()
phone = IntegerField()
name = StringField()
address = StringField()
index = IntegerField()
comment = StringField()
@app.route('/registration', methods=['POST'])
def registration():
form = RegistrationForm()
if form.validate_on_submit():
email, phone, name, address = form.email.data, form.phone.data, form.name.data, form.address.data
print(form)
print(email, phone, name, address)
return f'Hello {name} welcome to our site!'
return f'{form.errors}', 404
if __name__ == '__main__':
app.config["WTF_CSRF_ENABLED"] = False #отключаем проверку безопасности WTForms
app.run(debug=True)
Чтобы протестировать работу приложения, нам нужно как-то отправить по адресу domen.ru/registration POST-запросы при запущенном приложении. Для этого потребуется удобная консольная утилита Curl. Устанавливается командой:
sudo apt install curl
Теперь запустите приложение, а в терминале пропишите:
curl -X POST http://127.0.0.1:5000/registration -F email=mymail@mail.ru -F phone=123 -F name=Artem -F address=City
В терминале вы получите ответ на свой запрос, т.е. приветственную строку из нашей функции. А в ранере (там где пописывается работа программы, ошибки и т.д.) увидите все наши принты с объектом формы и данными из четырех полей формы. Однако если в своем запросе вы намеренно укажите некорректный email, абсолютно ничего не поменяется, так как наша программа не может распознавать эту самую корректность (валидность). И чтобы сделать некоторые поля обязательными к заполнению, а так же для проверки валидности введенных пользователем данных, нужны валидаторы.
Добавим импорты:
from wtforms.fields.numeric import IntegerField
from wtforms.fields.simple import StringField
from wtforms.validators import InputRequired, Email, NumberRange
И немного модернизируем наш класс:
class RegistrationForm(FlaskForm):
email = StringField(validators=[InputRequired(), Email()])
phone = IntegerField(validators=[InputRequired(), NumberRange(min=10000000000, max=999999999)])
name = StringField(validators=[InputRequired()])
address = StringField()
index = IntegerField()
comment = StringField()
Теперь поле email обязательное к заполнению и проверяется на валидность введенных данных (если валидатор Email не встал, поставьте командой: pip install email_validator), поле phone тоже обязательное и проверяется на корректность введенных чисел в диапазоне от 10000000000 до 999999999, а поле name просто обязательное к заполнению.
Вот теперь уже можно поэкспериментировать, добавив в запрос невалидные значения email или номера телефона. На всякий случай, код у нас теперь выглядит так:
from flask import Flask, request
from flask_wtf import FlaskForm
from wtforms.fields.numeric import IntegerField
from wtforms.fields.simple import StringField
from wtforms.validators import InputRequired, Email, NumberRange
app = Flask(__name__)
class RegistrationForm(FlaskForm):
email = StringField(validators=[InputRequired(), Email()])
phone = IntegerField(validators=[InputRequired(), NumberRange(min=10000000000, max=999999999)])
name = StringField(validators=[InputRequired()])
address = StringField()
index = IntegerField()
comment = StringField()
@app.route('/registration', methods=['POST'])
def registration():
form = RegistrationForm()
if form.validate_on_submit():
email, phone, name, address = form.email.data, form.phone.data, form.name.data, form.address.data
print(form)
print(email, phone, name, address)
return f'Hello {name} welcome to our site!'
return f'{form.errors}', 404
if __name__ == '__main__':
app.config["WTF_CSRF_ENABLED"] = False #отключаем проверку безопасности WTForms
app.run(debug=True)
Не будем ускучнять статью, просто экспериментируйте. Вооружитесь официальной докой, гуглом и шпаргалкой по валидаторам:
- 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
.
Создание собственных валидаторов
У встроенных валидаторов имеется ограниченный, минимально необходимый функционал. Для его расширения проще всего писать собственные валидаторы и определять в них кастомную логику. Давайте напишем кастомный валидатор для поля phone нашей формы, чтобы ограничить длину симоволов. Сделаем это несколькими способами и в отдельном файле, откуда потом импортируем:
from flask_wtf import FlaskForm
from wtforms import Field, ValidationError
def easy_number_length(form: FlaskForm, field: Field, message='Значение не в диапазоне 10 знаков.'):
'''Самое элегантное решение поставленной задачи. Проверяет не только длину, но и на "только цифры".'''
data = field.data
if not (str(data).isdigit() and len(str(data)) == 10):
raise ValidationError(message)
def number_length(min=10, max=10, message=None):
'''Имеет настраиваемый диапазон по количеству знаков.'''
if not message:
message = f'Значение должно быть от {min} до {max} знаков.'
def _length(form, field):
data = field.data
if max < len(str(data)) or len(str(data)) < min:
raise ValidationError(message)
return _length
class NumberLength:
'''По сути тоже самое, но оформлено в виде класса.'''
def __init__(self, min=10, max=10, message=None):
self.min = min
self.max = max
if not message:
message = f'Значение должно быть от {min} до {max} знаков.'
self.message = message
def __call__(self, form, field):
data = field.data
if self.max < len(str(data)) or len(str(data)) < self.min:
raise ValidationError(self.message)
Остается только сделать нужный импорт и добавить валидатор к полю. Я добавлю последний.
phone = IntegerField(validators=[InputRequired(message='Обязательное поле!'), NumberLength(min=10, max=10)])
Ссылка на документацию с кастомными валидаторами.
Тестирование валидаторов с Unittest
В тестировании я далеко не спец, но и статья пишется для таких же нулей, чтобы просто иметь общее представление. Для тестирвования валидаторов полей написанной выше формы я буду использовать тестовый клиент нашего приложения, посылая POST запросы к эндпойнту. Тесты, конечно же, нужно писать в отдельной директории (пакете с файлом __init__.py). Ниже представлен код тестов, который наверняка можно дополнить, что я рекомендуб сделать.
from app import app
import unittest
class TestApp(unittest.TestCase):
def setUp(self):
app.config['TESTING'] = True
app.config['DEBUG'] = True
app.config["WTF_CSRF_ENABLED"] = False
self.app = app.test_client()
self.base_url = '/registration'
fields = {
'email': 'mail@mail.ru',
'phone': 9506006000,
'name': 'Artem',
'address': 'My City',
'index': 603600,
'comment': 'Ok',
}
def test_path_registration(self):
response = self.app.post(self.base_url, data=self.fields)
self.assertEqual(response.request.path, '/registration')
def test_correct_registration(self):
response = self.app.post(self.base_url, data=self.fields)
response_text = response.data.decode('utf-8')
self.assertIn('Successfully registered', response_text)
def test_invalid_email(self):
bad_value = ('', 112, 'asasas.ru')
for item in bad_value:
with self.subTest(item=item):
email = {'email': item}
self.fields.update(email)
response = self.app.post(self.base_url, data=self.fields)
response_text = response.data.decode('utf-8')
self.assertIn('Invalid', response_text)
def test_invalid_phone(self):
bad_value = ('', 112, '9503330455', 89500770744)
for item in bad_value:
with self.subTest(item=item):
new_phone = {'phone': item}
self.fields.update(new_phone)
response = self.app.post(self.base_url, data=self.fields)
response_text = response.data.decode('utf-8')
self.assertIn('Invalid', response_text)
def test_invalid_name(self):
self.fields.update({'name': ''})
response = self.app.post(self.base_url, data=self.fields)
response_text = response.data.decode('utf-8')
self.assertIn('Invalid', response_text)
def test_invalid_address(self):
self.fields.update({'address': ''})
response = self.app.post(self.base_url, data=self.fields)
response_text = response.data.decode('utf-8')
self.assertIn('Invalid', response_text)
def test_invalid_index(self):
bad_values = ('', '123')
for item in bad_values:
with self.subTest(item=item):
self.fields.update({'index': item})
response = self.app.post(self.base_url, data=self.fields)
response_text = response.data.decode('utf-8')
self.assertIn('Invalid', response_text)
def test_comment(self):
values = ('', '123', 333)
for item in values:
with self.subTest(item=item):
self.fields.update({'comment': item})
response = self.app.post(self.base_url, data=self.fields)
response_text = response.data.decode('utf-8')
self.assertIn('Successfully registered', response_text)
if __name__ == "__main__":
unittest.main()
А напоследок пример симпатичной формы регистрации с кастомным валидатором и HTML кодом для страницы.
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')) # кнопка отправки формы
Валидатор для юзернейма:
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.")
Теперь поле username будет проверяться еще и на наличие запрещенных символов из списка, и в случае обнаружения такового, функция вернет ValidationError.
А это HTML код для страницы с нашей формой. Но лучше всё же использовать шаблоны Jinja и наследование от базового шаблона.
<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>
На этом всё. Если есть вопросы, пишите в чат.