Проблема с настройкой динамического списка подчиненных форм с помощью flask, wtforms и jinja2 - PullRequest
0 голосов
/ 30 июня 2019

Я довольно новичок в мире Python (планирую сделать переключение и покинуть CurlyBracesCamelCaseWorld), и я работаю над простым приложением go get exp во всем начальном стеке (база данных, сервер, обработка html-страниц и ресурсов) и т. д.).

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

Но я столкнулся с проблемой, от которой просто не могу пройти.

Что я пытаюсь сделать сейчас:

  • Иметь главную wtform со статическим набором полей
  • Добавить динамически генерируемый список подформ - предварительно заполненный значениями и пользовательскими метками

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

У меня есть полный список вопросов здесь

1) Основная проблема в том, что я не могу прочитать данные подчиненных форм в .validate () при отправке формы

2) Другое дело, что я не могу заставить метки отображать пользовательские значения, которые я хочу установить динамически

3) Мне нужно больше читать об обработке csfr в ​​подчиненных формах и о том, как обойти это также

4) И последнее - как проверить подчиненные формы - для обязательных полей, длины и т. Д.

1 & 2 - моя главная проблема сейчас, и у меня есть ощущение, что проблемы имеют одну и ту же первопричину Мои интуитивные ощущения говорят мне, что идентификатор сломанного элемента имеет смысл («контент» для каждого поля строки подчиненной формы вместо индексированных «записей-0-контент» - которые я вижу при отправке)

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

Итак, сервер ->

from flask import Flask, redirect, url_for, render_template
from flask_wtf import FlaskForm
from wtforms import StringField, FieldList, FormField, SubmitField, HiddenField, Label
from wtforms.validators import DataRequired

app = Flask(__name__, template_folder='flaskblog/templates')
app.config['SECRET_KEY'] = 'SECRET_KEY-SECRET_KEY-SECRET_KEY'


# subforms
class SubForm(FlaskForm):
    # how to handle hidden id that I can use to properly commit that on submit?
    # entry_type_id = HiddenField()

    # validators for subforms don't work, but that's something I'll try to address later
    content = StringField(validators=[DataRequired()])

    # I use custom __init__ set custom label for the field - or rather I try, as it doesn't work..
    def __init__(self, custom_label=None, *args, **kwargs):
        # not sure if safe - even just for the subform! #
        # Without that, I get 'TypeError: argument of type 'CSRFTokenField' is not iterable' on main_form.validate_on_submit()
        kwargs['csrf_enabled'] = False
        FlaskForm.__init__(self, *args, **kwargs)

        if custom_label is not None:
            self.content.label = Label(self.content.id, custom_label)
            print(f'INIT // id: [{self.content.id}] // content.data: [{self.content.data}] // label: [{self.content.label.text}]')


# main forms
class MainForm(FlaskForm):
    title = StringField('title')
    entries = FieldList(FormField(SubForm))
    submit = SubmitField('Post')


@app.route("/test", methods=['GET', 'POST'])
def test_route():
    # the main form
    main_form = MainForm(title='title')

    # sub forms, created before validate_on_submit()
    sub_form_1 = SubForm(content='Default answer 1', custom_label='Question 1')
    sub_form_2 = SubForm(content='Default answer 2', custom_label='Question 2')

    main_form.entries.append_entry(sub_form_1)
    main_form.entries.append_entry(sub_form_2)

    if main_form.validate_on_submit():
        for entry in main_form.entries.entries:
            print(f'LOOP // id: [{entry.content.id}] // content.data: [{entry.content.data}] // label: [{entry.content.label.text}]')

        return redirect(url_for('test_route'))

    print(f'INSTANCE_1 // id: [{sub_form_1.content.id}] // content.data: [{sub_form_1.content.data}] // label: [{sub_form_1.content.label.text}]')
    print(f'INSTANCE_2 // id: [{sub_form_2.content.id}] // content.data: [{sub_form_2.content.data}] // label: [{sub_form_2.content.label.text}]')

    return render_template('test_form.html', title='Test Form', main_form=main_form, legend='Test Form')


if __name__ == '__main__':
    app.run(debug=True)

И HTML-шаблон ->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title> Confused -.-' </title>
</head>
<body>
<div class="content-section">

    <form action="" method="post">
        {{ main_form.hidden_tag() }}
        {{ main_form.title.label(class="form-control-label") }}: {{ main_form.title(class="form-control form-control-lg") }}

        {% for entry_line in main_form.entries %}
            <div class="form-group">
                {{ entry_line.content.label(class="form-control-label") }}
                {{ entry_line.content.data(class="form-control form-control-lg") }}
            </div>
        {% endfor %}

        {# For the main form I use main_form.title(), main_form.submit(), etc - without .data(). #}
        {# If I try to do main_form.title.data() I get the ex that I can't call on 'str' #}

        {# But, for entry_lines, if I don't add .data() and just use entry_line.content()  #}
        {# I can see the input field, but it's prepopulated with HTML for that input instead of the value (that I see in that html) #}

        <div class="form-group">
            {{ main_form.submit(class="btn btn-outline-info") }}
        </div>
    </form>

</div>
</body>
</html>


Отладка по GET:

INIT // id: [content] // content.data: [Default answer 1] // label: [Question 1]
INIT // id: [content] // content.data: [Default answer 2] // label: [Question 2]
INSTANCE_1 // id: [content] // content.data: [Default answer 1] // label: [Question 1]
INSTANCE_2 // id: [content] // content.data: [Default answer 2] // label: [Question 2]

Отладка по почте:

INIT // id: [content] // content.data: [my ans 1] // label: [Question 1]
INIT // id: [content] // content.data: [my ans 1] // label: [Question 2]
LOOP // id: [entries-0-content] // content.data: [<input id="content" name="content" type="text" value="my ans 1">] // label: [Content]
LOOP // id: [entries-1-content] // content.data: [<input id="content" name="content" type="text" value="my ans 1">] // label: [Content]
INIT // id: [content] // content.data: [Default answer 1] // label: [Question 1]
INIT // id: [content] // content.data: [Default answer 2] // label: [Question 2]
INSTANCE_1 // id: [content] // content.data: [Default answer 1] // label: [Question 1]
INSTANCE_2 // id: [content] // content.data: [Default answer 2] // label: [Question 2]

Очевидно, что существует некоторая проблема с идентификаторами (2x контент против записей-0-контент), и я получаю первый результат дважды .. (value = "my ans 1")

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

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

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

Вставьте ссылки, если вы предпочитаете

РЕДАКТИРОВАТЬ - рабочий код

Благодаря @Nick K9!

from flask import Flask, redirect, url_for, render_template, request
from flask_wtf import FlaskForm
from wtforms import StringField, FieldList, FormField, SubmitField, HiddenField, Label
from wtforms.validators import DataRequired

app = Flask(__name__, template_folder='flaskblog/templates')
app.config['SECRET_KEY'] = 'SECRET_KEY-SECRET_KEY-SECRET_KEY'

subform_datasource = {
    0: {'question': 'Question 1', 'answare': 'Answare 1'},
    1: {'question': 'Question 2', 'answare': 'Answare 2'}
}


# subforms
class SubForm(FlaskForm):
    entry_type_id = HiddenField()
    content = StringField(validators=[DataRequired()])


# main forms
class MainForm(FlaskForm):
    title = StringField('title')
    entries = FieldList(FormField(SubForm))
    submit = SubmitField('Post')


@app.route("/test", methods=['GET', 'POST'])
def test_route():
    main_form = MainForm()
    if main_form.validate_on_submit():
        for entry in main_form.entries.entries:
            entry_message = (
                f'POST // wtform id: [{entry.content.id}] '
                f' //  entry_type_id id: [{entry.entry_type_id.data}]'
                f' //  content.data: [{entry.content.data}]'
                f' //  label: [{entry.content.label.text}]'
            )
            print(str(entry_message))

        return redirect(url_for('test_route'))
    elif request.method == 'GET':
        # You can indeed set the default values, but you need to pass the dict, not the SubForm instance!
        for key, subform in subform_datasource.items():
            main_form.entries.append_entry({'content': subform['answare'], 'entry_type_id': key})

    # Moved out from the constructor - on subform failed validation labels reset to the default value 'Content'
    # I guess that matching what was send to the form does not cast back the labels but creates the fresh instances with just the value
    # What, of course, makes sense - it's an edge case, no point in affecting performance for everyone
    for entry in main_form.entries.entries:
        entry.content.label.text = subform_datasource[entry.entry_type_id.data]['question']

    return render_template('test_form.html', title='Test Form', main_form=main_form, legend='Test Form')


if __name__ == '__main__':
    app.run(debug=True)

2 ссылки, которые также помогли мне

1 Ответ

0 голосов
/ 30 июня 2019

У вас правильная идея, но есть несколько проблем с тем, что вы сделали:

  1. Вы не должны явно создавать экземпляры SubForm.Передайте словарь в append_entry(), чтобы заполнить поля в подчиненной форме.( Редактировать: Извлечь неверную информацию о передаче экземпляров формы в append_entry(). Это должен быть объект словаря.)
  2. Вы должны вызвать append_entry() после блок validate_on_submit(), не раньше.Когда запрос POST возвращается с вашей формой, он уже имеет достаточное количество созданных подчиненных форм.Вы сделали это, когда страница была построена.Вам просто нужно прочитать все содержимое формы и извлечь / сохранить ваши данные перед перенаправлением.
  3. Вы упомянули пропущенные данные и невостребованную проверку.У меня есть догадка, что в настоящее время вы перезаписываете данные формы перед вызовом метода проверки.Так что эта проблема может разрешиться сама собой.
  4. Вы упомянули CSRF.Вам необходимо включить {{ entry_line.hidden_tag() }} в цикл подчиненной формы for.Это должно быть все, что вам нужно, чтобы CSRF работал с подчиненной формой.

Попробуйте и посмотрите, начинает ли работать ваша форма.

...