Проблема с выводом flash сообщений об ошибках во Flask
Пишу приложение на Flask, есть такие формы:
class AddArticleForm(FlaskForm):
"""Класс определяет форму добавления новостей."""
name_article: str = StringField('Название статьи',
validators=[DataRequired()])
text_article: str = TextAreaField('Текст статьи',
validators=[DataRequired()])
add_photo: str = FileField('Загрузить фото')
submit = SubmitField('Добавить статью')
class AddItemForm(FlaskForm):
"""Класс определяет форму добавления товаров."""
name: str = StringField('Наименование товара',
validators=[DataRequired(),
Length(min=3, max=15)])
description: str = TextAreaField('Описание товара',
validators=[DataRequired(),
Length(min=3)])
price: int = FloatField('Цена', validators=[DataRequired(),
NumberRange(min=0)])
category: int = SelectField('Категория',
choices=[('1', 'Категория 1'),
('2', 'Категория 2'),
('3', 'Категория 3'),
('4', 'Категория 4'),
('5', 'Категория 5'),
('6', 'Категория 6'),
('7', 'Категория 7'),
('8', 'Категория 8')],
coerce=int,
validators=[DataRequired()])
photo: str = FileField('Фото товара')
submit = SubmitField('Добавить товар')
Далее есть классы, которые позволяют производить проверку входных значений (int/str):
class CheckingValue:
"""Базовый класс для различных проверок входных значений."""
def __init__(self, value: object):
"""
Инициализирует объект класса CheckingValue.
:param value: Входное значение.
"""
self.value = value
class CheckingText(CheckingValue):
"""Класс для различных проверок строковых значений."""
def check_length(self, min_length: int, max_length: int) -> bool:
"""
Проверяет длину значения в заданном диапазоне.
:param min_length: Минимальная длина значения.
:param max_length: Максимальная длина значения.
Returns:
bool: Результат проверки.
"""
if not isinstance(self.value, str):
logger.error("TypeError %s", "check_length")
return False
return (min_length <= len(self.value) <= max_length)
def check_latin(self) -> bool:
"""
Проверяет, содержит ли значение только символы латинского алфавита.
Returns:
bool: Результат проверки.
"""
if not isinstance(self.value, str):
logger.error("TypeError %s", "check_check_latin")
return False
return not all(c.isalpha() and c.isascii() for c in self.value)
class CheckingNumber(CheckingValue):
"""Класс для различных проверок числовых значений."""
def check_category(self) -> bool:
"""
Проверяет значение на соответствие одной из категорий.
Returns:
bool: Результат проверки.
"""
return int(self.value) in {1, 2, 3, 4, 5, 6, 7, 8}
Есть CSS код для 2-х страниц(вид ошибок/вид успешного добавления):
.flash.success_panel {
top: -45px;
padding: 5px;
margin-left: 3%;
position: relative;
font-family: 'Days One', sans-serif;
color: green;
font-size: 11px;
width: 370px;
}
.flash.error_panel {
top: -45px;
padding: 5px;
margin-left: 3%;
position: relative;
font-family: 'Days One', sans-serif;
color: red;
font-size: 11px;
width: 370px;
}
.flash.success_article {
top: 0px;
padding: 5px;
margin-left: 2%;
position: relative;
font-family: 'Days One', sans-serif;
color: green;
font-size: 19px;
width: 600px;
}
.flash.error_article {
top: 0px;
padding: 5px;
margin-left: 2%;
position: relative;
font-family: 'Days One', sans-serif;
color: red;
font-size: 19px;
width: 600px;
}
Так же есть главные виновники торжества, это 2 класса с обработчиками, первый:
class AdminArcticel(WorkingWithHandlers):
"""
Класс для обработки формы добавления новости в базу данных.
Returns:
После прохождении всех проверок, добавляет новость в базу данных.
"""
decorators = [login_required]
def __init__(self, name_page):
"""
Инициализирует объект класса AdminArcticel.
:param name_page: Название страницы добавления статей
"""
super().__init__()
self.name_page = name_page
def dispatch_request(self):
"""
Реализует добавление новости в базу данных.
Метод:
1. проверяет метод запроса и валидацию формы.
2. Получает данные из формы проверяет их и проверяет наличие имени фото
3. Сохраняет фото в папку uploads и сохраняет айтем в базу данных
4. Выводит сообщение об успешном добавлении, в противном случае,
на каждом этапе выводит сообщение об ошибке.
"""
app.logger.info('Входящий запрос: %s %s', request.method, request.path)
form = AddArticleForm()
if request.method == 'POST':
if form.validate_on_submit():
name = request.form['name_article']
text = request.form['text_article']
photo = request.files['add_photo']
utc_timezone = pytz.utc
utc_time = datetime.now(utc_timezone)
eet_time = utc_time.astimezone(pytz.timezone('EET'))
if photo:
filename = secure_filename(photo.filename)
photo.save(os.path.join(app.config['UPLOAD_FOLDER'],
filename))
else:
filename = None
name_length = CheckingText(name)
name_latin = CheckingText(name)
text_latin = CheckingText(text)
text_length = CheckingText(text)
error_messages = []
if not name_length.check_length(min_length=3,
max_length=100):
error_messages.append(cont_error[1])
if not name_latin.check_latin():
error_messages.append(cont_error[3])
if not text_latin.check_latin():
error_messages.append(cont_error[4])
if not text_length.check_length(min_length=3,
max_length=1000):
error_messages.append(cont_error[2])
if filename is None:
error_messages.append(cont_info[2])
if error_messages:
for error_msg in error_messages:
flash(error_msg, category='error_article')
return redirect(url_for(self.name_page))
try:
new_article = Article(name=name,
text=text,
photo=filename,
pub_date=eet_time)
db.session.add(new_article)
db.session.commit()
flash(cont_info[1], category='success_article')
logger.info(f"{cont_info[1]} |AddArticleForm|")
except Exception as ex:
logger.error("Exception %s %s",
f"{cont_error[8]}|AddArticleForm|",
ex)
return redirect(url_for(self.name_page))
return render_template(f'admin/{self.name_page}.html',
check_total=check_auth(), form=form)
И так же второй класс:
class AdminPanel(WorkingWithHandlers):
"""
Класс для обработки формы добавления товара в базу данных.
Returns:
После прохождении всех проверок, добавляет айтем в базу данных.
"""
decorators = [login_required]
def __init__(self, name_page: str):
"""
Инициализирует объект класса AdminPanel.
:param name_page: Название страницы админ панели.
"""
super().__init__()
self.name_page = name_page
def dispatch_request(self):
"""
Реализует добавление айтема в базу данных.
Метод:
1. проверяет метод запроса и валидацию формы.
2. Получает данные из формы проверяет их и проверяет наличие имени фото
3. Сохраняет фото в папку uploads и сохраняет айтем в базу данных
4. Выводит сообщение об успешном добавлении, в противном случае,
на каждом этапе выводит сообщение об ошибке.
"""
app.logger.info('Входящий запрос: %s %s',
request.method, request.path)
form = AddItemForm()
if request.method == 'POST':
if form.validate_on_submit():
name = request.form['name']
description = request.form['description']
price = request.form['price']
category = request.form['category']
photo = request.files['photo']
if photo:
filename = secure_filename(photo.filename)
photo.save(os.path.join(app.config['UPLOAD_FOLDER'],
filename))
else:
filename = None
name_length = CheckingText(name)
description_length = CheckingText(description)
name_latin = CheckingText(name)
description_latin = CheckingText(description)
check_category = CheckingNumber(category)
error_messages = []
if not name_length.check_length(min_length=3,
max_length=15):
error_messages.append(cont_error[5])
if not description_length.check_length(min_length=3,
max_length=350):
error_messages.append(cont_error[2])
if not name_latin.check_latin():
error_messages.append(cont_error[3])
if not description_latin.check_latin():
error_messages.append(cont_error[4])
if not check_category.check_category():
error_messages.append(cont_error[6])
if filename is None:
error_messages.append(cont_info[2])
if error_messages:
for error_msg in error_messages:
flash(error_msg, category='error_panel')
return redirect(url_for(self.name_page))
try:
new_item = Item(name=name,
description=description,
price=price,
category=category,
photo=filename)
db.session.add(new_item)
db.session.commit()
flash(cont_info[1], category='success_panel')
logger.info(f"{cont_info[1]} |AddItemForm|")
except Exception as ex:
logger.info("Exception %s %s",
f"{cont_error[8]}|AddItemForm|",
ex)
return redirect(url_for(self.name_page))
return render_template(f'admin/{self.name_page}.html',
check_total=check_auth(), form=form)
Проблема заключается вот в чём: у класса AdminArcticel при допущении ошибок в заполнении формы, появляются ошибки на странице(flash-сообщения), а если все проверки пройдены, то сообщение об удачном добавлении айтема в БД, а у класса AdminPanel, появляются только сообщения об успешном добавлении, а ошибки нет. Сущности "cont_info" и "cont_error" это словари со строками (конкретным текстом ошибок по типу: "Название должно состоять из 3-х - 15-ти символов..."). Я пробовал подключать логирование, оно показывало, что при допущении ошибок в AdminPanel, не проходит валидация формы, а у AdminArticcle всё проходит в любом случае (и при ошибках и при удачном добавлении). Помогите решить вопрос)))))
p.s.: Еще хочу добавить, что если я делаю подобную проверку, то на странице почему-то появляется одна ошибка, а через список (несколько) - нет.
if not name_length.check_length(min_length=3,
max_length=15):
flash(cont_error[5], category='error_panel')
return redirect(url_for(self.name_page))
if not name_latin.check_latin():
flash(cont_error[3], category='error_panel')
return redirect(url_for(self.name_page))
if not description_length.check_length(min_length=3,
max_length=350):
flash(cont_error[2], category='error_panel')
return redirect(url_for(self.name_page))
if not description_latin.check_latin():
flash(cont_error[4], category='error_panel')
return redirect(url_for(self.name_page))
if not check_category.check_category():
flash(cont_error[6], category='error_panel')
return redirect(url_for(self.name_page))
if filename is None:
flash(cont_info[2], category='error_panel')
return redirect(url_for(self.name_page))
Ответы (2 шт):
Нужно убедиться, что в случае наличия ошибок в списке error_messages происходит немедленный возврат на страницу. Давайте обновим метод dispatch_request класса AdminPanel так, чтобы это условие выполнялось.
class AdminPanel(WorkingWithHandlers):
"""
Класс для обработки формы добавления товара в базу данных.
"""
decorators = [login_required]
def __init__(self, name_page: str):
super().__init__()
self.name_page = name_page
def dispatch_request(self):
app.logger.info('Входящий запрос: %s %s', request.method, request.path)
form = AddItemForm()
if request.method == 'POST':
if form.validate_on_submit():
name = request.form['name']
description = request.form['description']
price = request.form['price']
category = request.form['category']
photo = request.files['photo']
if photo:
filename = secure_filename(photo.filename)
photo.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
else:
filename = None
name_length = CheckingText(name)
description_length = CheckingText(description)
name_latin = CheckingText(name)
description_latin = CheckingText(description)
check_category = CheckingNumber(category)
error_messages = []
if not name_length.check_length(min_length=3, max_length=15):
error_messages.append(cont_error[5])
if not description_length.check_length(min_length=3, max_length=350):
error_messages.append(cont_error[2])
if not name_latin.check_latin():
error_messages.append(cont_error[3])
if not description_latin.check_latin():
error_messages.append(cont_error[4])
if not check_category.check_category():
error_messages.append(cont_error[6])
if filename is None:
error_messages.append(cont_info[2])
if error_messages:
for error_msg in error_messages:
flash(error_msg, category='error_panel')
return redirect(url_for(self.name_page))
try:
new_item = Item(name=name, description=description, price=price, category=category, photo=filename)
db.session.add(new_item)
db.session.commit()
flash(cont_info[1], category='success_panel')
logger.info(f"{cont_info[1]} |AddItemForm|")
except Exception as ex:
logger.error("Exception %s %s", f"{cont_error[8]}|AddItemForm|", ex)
return redirect(url_for(self.name_page))
else:
for fieldName, errorMessages in form.errors.items():
for err in errorMessages:
flash(f"Error in {fieldName}: {err}", category='error_panel')
return redirect(url_for(self.name_page))
return render_template(f'admin/{self.name_page}.html', check_total=check_auth(), form=form)
Изменения:
- Проверка form.errors добавлена для случая, когда валидация формы не проходит, и для каждой ошибки создается flash-сообщение.
- Добавление возврата redirect(url_for(self.name_page)) сразу после добавления всех ошибок в список, чтобы гарантировать, что пользователь увидит ошибки, если они есть.
Для вывода кастомных ошибок можно использовать wtforms:
from wtforms import StringField, validators
class AddItemForm(FlaskForm):
name = StringField('Наименование товара', [
validators.DataRequired(message='Поле обязательно к заполнению'),
validators.Length(min=3, max=15, message='Длина названия должна быть от 3 до 15 символов')
])
description = TextAreaField('Описание товара', [
validators.DataRequired(message='Поле обязательно к заполнению'),
validators.Length(min=3, message='Описание должно быть не менее 3 символов')
])
price = FloatField('Цена', [
validators.DataRequired(message='Поле обязательно к заполнению'),
validators.NumberRange(min=0, message='Цена не может быть отрицательной')
])
# Продолжите аналогично для других полей
Использовать так:
def dispatch_request(self):
app.logger.info('Входящий запрос: %s %s', request.method, request.path)
form = AddItemForm()
if request.method == 'POST' and form.validate_on_submit():
# Здесь обработка данных формы
pass
else:
for fieldName, errorMessages in form.errors.items():
for err in errorMessages:
flash(f"{err}", category='error_panel')
return redirect(url_for(self.name_page))
return render_template(f'admin/{self.name_page}.html', check_total=check_auth(), form=form)