Сейчас мы напишем простенький функционал, дающий представление реализации работы форм, на примере страницы регистрации в нашем приложении. Углубиться же в своих познаниях лучше вооружившись документацией (ссылка будет) и тематическими статьями.
Создадим каркас приложения.
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>На этом всё. Если есть вопросы, пишите в чат.