Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flask + WTForms, dynamically generated list of fields

I am making a Flask application that is essentially form-based and so I'm using WTForms and Flask-wtf.

I am currently refactoring my code so my whole form uses WTForms and there is a very dynamic part of one of the forms that I am unable to implement using WTForms. I have no clue how to do it, my initial ideas didn't work, I can't find references or tutorials covering my problem and so this is why I ask for help.

So, the form in question allows users to submit objects that consist of:

  • A label (StringField, easy)
  • A Description (TextAreaField, also easy; although I had trouble to make a default value work)
  • A list of property of the form (predicate, object), where predicate is taken from a pre-built list and object can basically be anything but each predicate will generate a specific object (for instance, the predicate "related to" will expect another object (that comes from a dropdown list) and the predicate "resource" will expect a http link of some sort). This list can be empty.

As you can guess I have trouble with the list. The way the code works right now, I get the label and description using wtforms, and the property list is generated using a config constant (that is used throughout the code so I only have one place to edit if I want to add new properties) and a dynamic menu in javascript that creates (here, for predicates) fields, that I can then get using flask.request.form object in the view function. All the hidden fields for predicates have the same name attribute and all the hidden fields for objects have the same name attribute.

Here is what the view of the form looks like, initialized with a few properties:

https://i.sstatic.net/g7g40.png

Under the "Propriétés" label you have a dropdown to select the predicate, the second field is displayed or hidden depending on the selected predicate (can be a dropdown or a text field), and it is only when you click on "Ajouter propriété" ("Add property") that a new line is added in the tab below and the fields are generated.

I'd like not to have to change anything on this side because it works very well, makes the form very intuitive and is basically exactly what I want it to be from the user's end.

This is what my custom Form looks like right now (it doesn't work and properties stays empty whatever the number of fields I submit with the form):

class PropertyForm(Form):
    property_predicate = HiddenField(
        validators=[AnyOf(values=app.config["PROPERTY_LIST"].keys())]
    )
    property_object = HiddenField(
        validators=[DataRequired()]
    )

class CategoryForm(Form):
    """
        Custom form class for creating a category with the absolute minimal
        attributes (label and description)
    """
    label = StringField(
        "Nom de la categorie (obligatoire)",
        validators=[DataRequired()]
    )
    description = TextAreaField(
        "Description de la categorie (obligatoire)",
        validators=[DataRequired()]
    )
    properties = FieldList(FormField(PropertyForm),validators=[Optional()])

And here is what I'd love to do in my views.py code (that I am currently refactoring):

def cat_editor():
    cat_form = CategoryForm()
    if request.method == "GET":
        # Do GET stuff and display the form
        return render_template("cateditor.html", form=cat_form, varlist=template_var_list)
    else if request.method == "POST":
        if cat_form.validate_on_submit():
            # Get values from form
            category_label = cat_form.label.data
            category_description = cat_form.description.data
            category_properties = cat_form.properties.data
            # Do POST stuff and compute things
            return redirect(url_for("index"))
        else:
            # form didn't validate so we return the form so the template can display the errors
            return render_template("cateditor.html", form=cat_form,
                                    template_var_list = template_var_list)

The basic structure works perfectly, it's just that damn dynamic list I can't get to work properly.

Getting label and description from the WTForms CategoryForm instance works fine, but properties always return an empty list. Ideally I'd love to be able to get a list of the form [(predicate1, property1), (predicate2, object2) ... ] when calling cat_form.properties.data (this is why I have a FieldList of FormFields with two HiddenField in each) but I'd have no problem having to build such a list from two list as long as it's using WTForms. Any idea? Thanks a lot :)

like image 274
Elred Avatar asked Oct 21 '25 17:10

Elred


1 Answers

I found out what the problem was by playing around with FieldList objects and append_entry() to see what HTML code would Flask-wtf generate if I was to make a prepopulated property list.

My Javascript was generating hidden fields with all the same name, as from what I understood that WTForms is able to aggregate fields with the same name to create lists. Problem is, those similarly named fields were part of a FormField itself nested in a FieldList object name properties.

In order for the WTForms Form object to discern a set of hidden fields from another, when you nest FormFields inside a FieldList it prefixes the FormFields field names with "FieldList_name-index-". Which means what WTForms was expecting was something like

<input type="hidden", name="properties-0-property_predicate" value=...>
<input type="hidden", name="properties-0-property_object" value=...>

<input type="hidden", name="properties-1-property_predicate" value=...>
<input type="hidden", name="properties-1-property_object" value=...>

<input type="hidden", name="properties-2-property_predicate" value=...>
<input type="hidden", name="properties-2-property_object" value=...>

I modified my javascript so it generates the appropriate names. Now when I call cat_form.properties.data I have something that looks like:

[{"property_predicate": "comment", "property_object":"bleh"},
 {"property_predicate": "comment", "property_object": "bleh2"}]

And that is exactly what I need. For some reason the form doesn't validate but at least I know how to make WTForms extract data my javascript-generated hidden fields, which is what the problem was.

Edit: Form validation happens because you have to insert a CSRF hidden input with your csrf to every subform you generate with the FormField.

like image 181
Elred Avatar answered Oct 24 '25 08:10

Elred