1

I want to create a form with fields for each of the last 10 years. So for 2020-2011 I want, say, an IntegerField for each year with the variable names year_2020, year_2019, year_ 2018, ... It'd also be nice if they had the appropriate labels too, i.e. 2020, 2019...

I could do this by writing something out for each year individually but I thought it'd be nicer and more efficient (?) to generate them using a for loop. Is this possible?

I saw this question about generating fields in the template using a for loop but I'm wondering how to generate fields in the python form class itself (not sure what to call it; please excuse my ignorance). I've seen this question but I can't seem to get it to work. I'm also not fond of that solution since it doesn't give the fields descriptive names like year_2020.

This is my code so far; apologies for any errors.

The python:

forms.py
    class YearForm(FlaskForm):
        year = IntegerField(validators=[NumberRange(min=0,max=100000,message='Please enter an integer above 0.'),
                            InputRequired(message='Please enter a value.')])
    
    class RentForm(FlaskForm):
        years = FieldList(FormField(YearForm), min_entries=10)

The template:

form.html
    for year in rentForm.years:
        <p>{{ year.label }}: {{ year(size=32) }}
            {% for error in year.errors %}
                <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>

It looks like I'm accessing the fields incorrectly in the template. What is the correct way? And how can I give the fields more descriptive labels and variable names?

Any help would be greatly appreciated; thanks!

EDIT: if this could be more easily done in another language please let me know. I just started learning Flask/Python and am not married to either.

emdash
  • 21
  • 6

2 Answers2

1

I am doing something similar. Perhaps my example will help. I am extending the form, in your case RentForm.years in my Flask handler in Python, NOT in my template. Here is my form...

class TemplateFormRow(FlaskForm):
    col = StringField(label='Column Name')
    data_type = SelectField(label='Data Type',
                            choices=[("boolean", "boolean"), ("datetime", "datetime"),
                                     ("integer", "integer"), ("decimal", "decimal"), ("string", "string")])
    sequence = HiddenField()
    delete = SubmitField(label='Delete')


class TemplateForm(FlaskForm):
    rows = FieldList(unbound_field=FormField(TemplateFormRow))
    add_row = SubmitField(label='Add Row', render_kw={'class': "btn btn-primary"})
    confirm = SubmitField(label='Save', render_kw={'class': "btn btn-primary"})
    btn_cancel = SubmitField(label='Cancel', render_kw={'formnovalidate': True, 'class': "btn btn-primary"})

Notice that in my case I put, on the parent form, a button that allows the user to add another row. You'd handle it a bit differently if you always want 10 rows.

Here is part of the Python code that works with this form. The append_entry line is particularly important to you...

if request.method == 'POST':

if form.btn_cancel.data:
    return redirect(url_for('admin'))

if form.add_row.data:  # User pressed the add row button
    form.rows.append_entry()

and here is the Python code that renders the template...

return render_template(template_name_or_list='generic_entry_page.html', page_hdr='Template',
                       show_form=form, form_msg=msg)

Finally, here is the part where my template processes this...

                {% for element in show_form %}
                    {% if element is iterable %}  {# This is a vertical set of forms #}
                        <table>
                            <tr>
                                {% for field in element[0] %}
                                    {#                                      {% for field in element.rows[0] %}#}
                                    {% if field.type != 'HiddenField' and field.label.text != 'CSRF Token' %}
                                        <th>{{ field.label }}</th>
                                    {% else %}
                                        <th>{{ field }}</th>
                                    {% endif %}
                                {% endfor %}
                            </tr>
                            {% for row in element %}
                                <tr>
                                    {% for field in row %}
                                        <td>
                                            {% if field.type == 'SubmitField' %}
                                                <button {{ field }} {{ field.label.text }} </button>
                                            {% else %}
                                                {{ field }}
                                            {% endif %}
                                        </td>
                                    {% endfor %}
                                </tr>
                            {% endfor %}
                        </table>
                        <br>

The resultant screen looks like this... Hit add row button to add a row

Does this help?

Ben
  • 4,798
  • 3
  • 21
  • 35
  • Hey thanks for the answer! I tried to copy+paste your code and make it work but couldn't. I think I had to close a for loop and an if statement but I still get a blank screen. Can you explain what show_form is? That might help me understand what's going wrong. – emdash Jul 29 '20 at 19:13
  • Sure! I just edited my answer to add the part where I render my template. You'll see that show_form is just a variable that I send to the template. It contains the actual form that I want to show on the screen (hence the name show_form). Does that help clear it up? Feel free to ask for clarification, I know it's a touch complex. – Ben Jul 29 '20 at 21:02
  • That does clear things up, but it doesn't seem to have changed anything. In routes.py I have `testForm = TestForm() ... return render_template('test.html', title='Test Form', show_form=testForm) ` In forms.py I have `class TemplateFormRow(FlaskForm): (everything you wrote) class TestForm(FlaskForm): (everything you wrote, but with a different class name) ` In test.html (the template) I have the code you put up. I only closed a for loop and an if statement.... but it still renders as a blank page. I'm stumped. – emdash Jul 29 '20 at 22:01
  • Looks like you got something to work. Congrats! You should mark your answer as the correct one. – Ben Jul 30 '20 at 00:29
1

Okay I was able to make a satisfactory toy example work using the information from this question.

The python:

class ImageForm(FlaskForm):
    frequency = SelectField(choices=[('monthly', 'Monthly'),('weekly', 'Weekly')])
    caption = StringField('Caption')
    credit = StringField('Credit')

class TestForm(FlaskForm):
    images = FieldList(FormField(ImageForm), min_entries=10)

The template:

<form action="" method="post">

    {{ testForm.hidden_tag() }}

    <table>
    {% for image in testForm.images %}
        <tr>
            <td> {{ image['frequency'] }} </td>
            <td> {{ image['caption'] }} </td>
            <td> {{ image['credit'] }} </td>
        </tr>
    {% endfor %}
    <table>
    
</form>

The result: Rendered HTML (ignore all the tabs; I have a problem)

I think this should suit my needs nicely.

PS: And you can access individual fields like so:

testForm.images[0]['frequency']

For the first dropdown menu.

emdash
  • 21
  • 6