Проблема с выводом 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 шт):

Автор решения: Andrey Soloviev

Нужно убедиться, что в случае наличия ошибок в списке 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)

Изменения:

  1. Проверка form.errors добавлена для случая, когда валидация формы не проходит, и для каждой ошибки создается flash-сообщение.
  2. Добавление возврата redirect(url_for(self.name_page)) сразу после добавления всех ошибок в список, чтобы гарантировать, что пользователь увидит ошибки, если они есть.
→ Ссылка
Автор решения: Andrey Soloviev

Для вывода кастомных ошибок можно использовать 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)


→ Ссылка