1

I am working on building a small survey system to meet some specific requirements for a research project I'm working on. One of those requirements is the user has to be able to save their place and be able to return to the survey later and finish their work.

I am using Flask and Flask-WTF to make this system, and I'm using a MySQL database to save the users' responses. I've run into an issue where I am not able to have default values when using a FieldList of RadioFields, where my goal is to dynamically set this default value for the questions that have been answered to allow the user to see/modify their previous work.

Here are the forms I've built, the html template, and the current output:

class Phrase(db.Model):
    __tablename__ = 'phrases'
    id            = db.Column(db.Integer, primary_key=True, nullable=False)
    phrase_text   = db.Column(db.Text, nullable=False)

...

class SinglePhraseForm(Form):
    phrase_rating = RadioField()
    phrase_cat    = SelectField("Please choose whether this phrase is more useful in written or spoken communication:", choices=[('w', 'Written'), ('s', 'Spoken')])

class AllPhrasesForm(FlaskForm):
    phrases = FieldList(FormField(SinglePhraseForm), min_entries=5)

...

@app.route("/phrases", methods=["GET", "POST"])
def phrases():
    phrases_query = Phrase.query.all()

    form = AllPhrasesForm()
    for i in range(len(phrases_query)):
        form.phrases[i].phrase_rating.label = f"Please rate how useful the following phrase is: {phrases_query[i].phrase_text}"
        form.phrases[i].phrase_rating.choices = [('1', '1 - Not useful'), ('2','2'), ('3','3'), ('4','4'), ('5','5 - Very Useful')]
        form.phrases[i].phrase_rating.default = str((i % 5) + 1)

    ### THIS CODE WORKS AND RENDERS DEFAULT CORRECTLY ###
    # form = SinglePhraseForm()
    # form.phrase_rating.label = "THIS IS A TEST LABEL"
    # form.phrase_rating.choices = [('t1', 'TEST1'), ('t2', 'TEST2'), ('t3', 'TEST3')]
    # form.phrase_rating.default = 't2'
    # form.process()

    if form.validate_on_submit():
        if form.continue_survey.data:
            return redirect(url_for("extended_responses"))

    return render_template("phrases.html", form=form)
<h1>Phrases</h1>

<form method="POST" action="/phrases">
    {{ form.hidden_tag() }}
    {% for phrase in form.phrases %}
        {% for subfield in phrase if subfield.widget.input_type != "hidden" %}
            <tr>
                <td>{{ subfield.label }}</td>
                <td>{{ subfield() }}</td>
            </tr>
        {% endfor %}
        <br>
    {% endfor %}
    <!-- TEST FOR SINGLE FORM -->
    <!-- {% for subfield in phrase if subfield.widget.input_type != "hidden" %}
        <tr>
            <td>{{ subfield.label }}</td>
            <td>{{ subfield() }}</td>
        </tr>
    {% endfor %} -->
</form>

In the phrases route, I am trying to iterate over the RadioFields and dynamically set their label, choices, and default values. The label and choices correctly render, but the default value is not showing up: Screenshot of the first two questions with dummy data. I've tried following answers like this one, but using form.process() simply removes all of my dynamic values: Screenshot of the form after using form.process(), which is not what I want. I tried exploring using my SinglePhraseForm (changing the base class from Form to FlaskForm) and am actually able to get all the values in the commented out code to render, including default, using form.process() and the commented out code in the html template. This makes me things I'm doing something wrong with the FieldList.

Any guidance on how I can correctly render dynamic default values for this FieldList of RadioFields?

BFrisch
  • 13
  • 7
  • I'm suspicious of `{% for subfield in phrase if subfield.widget.input_type != "hidden" %}` in your template. I would pull the `if...` into a child block and see what effect that has on the rendering. – James McPherson Jan 04 '22 at 23:33
  • Does this help? https://stackoverflow.com/a/34820107/12641958 – vulpxn Jan 04 '22 at 23:35
  • @JamesMcPherson it has no effect. That is there so the CSRF tag doesn't get displayed. I just tried removing it to double-check and removing it had no effect. – BFrisch Jan 05 '22 at 02:42
  • @vulpxn that is the answer I linked in my post and it does not work for this situation. It works when using the SingePhraseForm, which does not have a FieldList, but not in the AllPhrasesForm that does use a FieldList. I'm not sure why it doesn't work for this situation though. – BFrisch Jan 05 '22 at 02:43

1 Answers1

0

This looks like what you need! (I hope so)


  • The only drawback, which I still don't know how to fix, is that CTRL + R (arrow refresh button of browser) doesn't clear cache, unlike CTRL + F5 does. You will have to fix that on your own (if that even matters)
  • Also, I used a list of Query objects that emulate your database values (correct me if I did that wrong)
  • Pass coerce=int to RadioField so it will accept integer values (your database id Column is Integer)
  • Number of entries in FieldList is depending on phrases_query length (kinda dynamic)

Here is the code:

main.py:

from flask import Flask, render_template
from wtforms.fields import RadioField, SelectField, FieldList, FormField, SubmitField
from flask_wtf import FlaskForm


app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret_key'


class Query:
    def __init__(self, id, phrase_text) -> None:
        self.id = id
        self.phrase_text = phrase_text


phrases_query = [
    Query(1, 'I hope this helps'),
    Query(2, 'Yeah that works, finally'),
    Query(3, 'I spent 4 hours and its 5 am, bruh xd'),
    Query(4, 'Alarm set on 8am... (yeah, I"m ranting in my code Lorem Ipsum, like, gona stop me or smtn? :D)'),
    Query(5, 'MYRID'),
]


class SinglePhraseForm(FlaskForm):
    phrase_rating = RadioField(coerce=int)
    phrase_cat = SelectField(
        "Please choose whether this phrase is more useful in written or spoken communication:",
        choices=[
            ('w', 'Written'),
            ('s', 'Spoken'),
        ],
    )


entries_amount = len(phrases_query)


class AllPhrasesForm(FlaskForm):
    global entries_amount
    phrases = FieldList(
        FormField(SinglePhraseForm),
        min_entries=entries_amount,
    )
    submit = SubmitField(label="save all the select field values")


phrase_rating_choices = [
    (1, '1 - Not useful'),
    (2, '2'),
    (3, '3'),
    (4, '4'),
    (5, '5 - Very Useful'),
]


def generate_my_form(overwrite_data=True):
    form = AllPhrasesForm()
    for i in range(len(phrases_query)):
        form.phrases[i].phrase_rating.label = \
            f"Please rate how useful the following phrase is: {phrases_query[i].phrase_text}"
        global phrase_rating_choices
        form.phrases[i].phrase_rating.choices = phrase_rating_choices
        if overwrite_data:
            form.phrases[i].phrase_rating.data = (i % 5) + 1
    return form


@app.route("/phrases", methods=["GET", "POST"])
def phrases():
    form_for_validation = generate_my_form(overwrite_data=False)
    if form_for_validation.validate_on_submit():
        # do something with form_for_validation.data
        print(form_for_validation.data)

        # render page with chosen by user form data
        return render_template(
            "phrases.html",
            form=form_for_validation,
        )
    else:
        form_with_default_data = generate_my_form(overwrite_data=True)
        # render page with default form data
        return render_template(
            "phrases.html",
            form=form_with_default_data,
        )


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

phrases.html:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Answer</title>
</head>

<body>
    <h1>Phrases</h1>

    <form method="POST" action="/phrases">
        {{ form.hidden_tag() }}
        {% for subfield in form if subfield.widget.input_type != "hidden" %}
        <tr>
            <td>{{ subfield() }}</td>
        </tr>
        {% endfor %}
    </form>

</body>

</html>

I took this answer and adjusted it a bit. It's weird how it has so few upvotes. I've spend a lot of time trying to set default parameter before instancing a form, but it didn't work for me, eventually because of UnboundField. And FieldList prevent from looping over it's arguments.


I wonder if that helped. If not - feel free to write back, I'm curious now :D

barni
  • 436
  • 3
  • 8