flaskflask-wtformswtforms

How to render Dynamic forms with flask-wtf


I am trying to render a dynamic form with flask-wtf (WTForms), where it would have the following dynamic data / output.

  1. List of Boxes (n items)
  2. Select Element with (Target Collection, m choices, repeated each row)
  3. Checkbox for each row, if it should be transferred
Box Name Box Description Box Location Target Collection Transfer?
Box 1 Desc 1 Annex repeated select field w/ same choices [ ] - Checkbox
Box 2 Desc 2 Annex repeated select field w/ same choices [ ] - Checkbox
Box 3 Desc 3 Annex repeated select field w/ same choices [ ] - Checkbox
... ... ... repeated select field w/ same choices [ ] - Checkbox
class MultiCheckboxField(SelectMultipleField):
    widget = ListWidget(prefix_label=False)
    option_widget = CheckboxInput()

class FindingAidsOptions(FlaskForm):
    finding_aid_choices = SelectField("", coerce=int, validators=[DataRequired()])


class FindingAidTransferForm(FlaskForm):
    target_findingaid = FieldList(FormField(FindingAidsOptions), min_entries=1, max_entries=5)
    physicalobjects = MultiCheckboxField('Label',coerce=int)
    submit = SubmitField('Transfer Selected Objects')

Flask Route

    form = forms.FindingAidTransferForm()

    form.target_findingaid.choices = findingaid_choices #This does not seem to work, as I never can iterate over the values below
    form.physicalobjects.choices = physical_objects # this works, and can iterate over it

    return render_template(
        "atom-migrate-physical-object-listing.jinja2",
        title="Accessions Phyiscal Object Migrations",
        template="atom-caia-template",
        atom_records=accession_record,
        findingaids_physical_objects=findingaids_physical_objects,
        form=form
    )
# Example values for the choice data

findingaid_choices = [(26958, 'Records Choice 1'), (142451, 'Records Choice 2')]
physical_objects = [(107267, 'Box 5, TOM'), (130235, 'Box 1, A83412001377'), (139827, 'Box 2, A83411997169')]

Example template.

<form class="form" method="post" role="form">
    {{ form.hidden_tag() }}
    <table class="table table-striped table-sm table-responsive">
        <thead>
            <th>Object Name</th>
            <th>Description</th>
            <th>Location</th>
            <th>Destination Finding Aid</th>
            <th>Transfer to Findingaid?</th>
        </thead>
        <tbody>
            {% for po in form.physicalobjects %}
            <tr>
                <td>{{ findingaids_physical_objects[po.data]['physical_object_name'] if findingaids_physical_objects[po.data]['physical_object_name'] is not none }}</td>
                <td>{{ findingaids_physical_objects[po.data]['physical_object_desc'] if findingaids_physical_objects[po.data]['physical_object_desc'] is not none }}</td>
                <td>{{ findingaids_physical_objects[po.data]['physical_object_loc'] if findingaids_physical_objects[po.data]['physical_object_loc'] is not none }}</td>
                <td>Select Box with finding_aid_choices goes here</td>
                <td>Check box if it should be transferred from physicalobjects</td>
            </tr>
            {% endfor %}
        </tbody>
    </table>

        {% for item in form.target_findingaid %}
         <pre>{{ item.id }}</pre>
         {# This only seems to output target_findingaid-0, even though I have multiple. If I change the min_entries I do get an incremented value.
        {% endfor %}
        
        {{ formTextInput(form.submit) }}
</form>

Solution

  • I ended up figuring it out last night. I had to move some pieces around, but below is what I ended up having to do to get it working.

    Form Classes:

    class PhysicalObjects(Form):
        box_id = HiddenField('Physical Object ID')
        name = StringField('Name')
        location = StringField('Location')
        transfer = BooleanField('Transfer Object')
        target_findingaid = SelectField("Target Findingaid", validators=[DataRequired()],coerce=int)
    
    class FindingAidTransferForm(FlaskForm):
        physicalobjects = FieldList(FormField(PhysicalObjects), min_entries=1)
        submit = SubmitField('Transfer Selected Objects')
    
    

    Template for loop

                {% for po in form.physicalobjects.entries %}
                <tr>{{po.form.box_id }}
                    <td>{{ po.form.name.data if po.form.name.data is not none }}</td>
                    <td>{{ po.form.location.data if po.form.location.data is not none }}</td>
                    <td>{{ po.form.target_findingaid }}</td>
                    <td>{{ po.form.transfer() }}</td>
                </tr>
                {% endfor %}
    

    Route code

        findingaid_choices = []
        physical_objects = []
        phys_objects_group = namedtuple("Physical_Objects", ['box_id', 'name', 'location'])
        for _, rec in findingaids_physical_objects.items():
            if rec['related_item_type'] == "findingaid":
                findingaid_choices.append((rec['related_item_id'], rec['findingaid_title']))
            else: 
                physical_objects.append(phys_objects_group(
                    rec['related_item_id'],
                    rec['physical_object_name'],
                    rec['physical_object_loc']
                ))
    
        data = {'physicalobjects':  physical_objects}
        form = forms.FindingAidTransferForm(data=data)
    
        for entry in form.physicalobjects.entries:
            entry.target_findingaid.choices = findingaid_choices
    
        if form.validate_on_submit():
    
            ... do stuff
    
        else:
            return render_template(
                "atom-migrate-physical-object-listing.jinja2",
                title="Accessions Physical Object Migrations",
                template="atom-caia-template",
                atom_records=accession_record,
                findingaids_physical_objects=findingaids_physical_objects,
                form=form
            )