Question

I've added in a MultiCheckboxField to my Flask app (as per the docs), but this field fails to validate when I use the validate_on_submit method on it, seemingly because it has no choices attribute even though I've explicitly set it.

class MultiCheckboxField(SelectMultipleField):
    """
    A multiple-select, except displays a list of checkboxes.

    Iterating the field will produce subfields, allowing custom rendering of
    the enclosed checkbox fields.
    """
    widget = ListWidget(prefix_label=False)
    option_widget = CheckboxInput()

class DocumentProcessForm(Form):
    """
    Allows the user to select which documents should be processed.
    """

    PROCESS = "0"
    DELETE = "-1"

    files = MultiCheckboxField("Select", coerce=int)
    #Custom fields I made that render <button>s:
    process_button = ButtonField("Process", name="action", value=PROCESS)
    delete_button = ButtonField("Delete",  name="action", value=DELETE)

@app.route(app.config["PROJECT_ROUTE"] + "<project_id>",
    methods=["GET", "POST"])
def project_show(project_id):
    """
    Show the files contained in a specific project. It also allows the user
    to upload a new document, much like projects().

    :param int project_id: The ID of the desired project.
    """

    process_form = forms.DocumentProcessForm(prefix="process")

    if process_form.validate_on_submit(): #This fails
        files = request.form.getlist("process-files")
        if len(files) > 0:
            if request.form["action"] == process_form.DELETE:
                delete(files)
            elif request.form["action"] == process_form.PROCESS:
                #TODO: process these files.
                pass

    # The template needs access to the ID of each file and its filename.
    process_form.files.choices = []
    file_objects = GetFiles(project_id) # database query, this works fine
    for file_object in file_objects:
        process_form.files.choices.append((file_object.id,
            os.path.split(file_object.path)[1]))

    return render_template("document_list.html",
        process_form=process_form,
        allowed_extensions=app.config["ALLOWED_EXTENSIONS"])

And in my template:

<form method="POST" role="form">
    {{ process_form.hidden_tag() }}
    <table>
        <thead>
            <tr>
                <th>Filename</th>
                <th>Select</th>
            </tr>
        </thead>
        <tbody>
        {% for file in process_form.files %}
            <tr>
                <td>
                    <label for="{{ file.id }}">
                    <a href="{{ url_for('document_show',
                        document_id=file.data,
                        project_id=project.id) }}">{{ file.label.text }}</a>
                    </label>
                </td>
                <td>
                    {{ file }}
                </td>
            </tr>
        {% endfor %}
        </tbody>
    </table>
    <h3>Selected file actions:</h3>
    {{ process_form.process_button(class="btn btn-primary") }}
    {{ process_form.delete_button(class="btn btn-danger") }}
</form>

Flask returns the error:

Traceback (most recent call last):
  File "/home/plasmasheep/prog/project/venv/lib/python2.7/site-packages/flask/app.py", line 1836, in __call__
    return self.wsgi_app(environ, start_response)
  File "/home/plasmasheep/prog/project/venv/lib/python2.7/site-packages/flask/app.py", line 1820, in wsgi_app
    response = self.make_response(self.handle_exception(e))
  File "/home/plasmasheep/prog/project/venv/lib/python2.7/site-packages/flask/app.py", line 1403, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/home/plasmasheep/prog/project/venv/lib/python2.7/site-packages/flask/app.py", line 1817, in wsgi_app
    response = self.full_dispatch_request()
  File "/home/plasmasheep/prog/project/venv/lib/python2.7/site-packages/flask/app.py", line 1477, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/home/plasmasheep/prog/project/venv/lib/python2.7/site-packages/flask/app.py", line 1381, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/home/plasmasheep/prog/project/venv/lib/python2.7/site-packages/flask/app.py", line 1475, in full_dispatch_request
    rv = self.dispatch_request()
  File "/home/plasmasheep/prog/project/venv/lib/python2.7/site-packages/flask/app.py", line 1461, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/home/plasmasheep/prog/project/app/views.py", line 84, in project_show
    if really_submitted(process_form):
  File "/home/plasmasheep/prog/project/app/views.py", line 22, in really_submitted
    return form.validate_on_submit() and form.submitted.data
  File "/home/plasmasheep/prog/project/venv/lib/python2.7/site-packages/flask_wtf/form.py", line 156, in validate_on_submit
    return self.is_submitted() and self.validate()
  File "/home/plasmasheep/prog/project/venv/lib/python2.7/site-packages/wtforms/form.py", line 271, in validate
    return super(Form, self).validate(extra)
  File "/home/plasmasheep/prog/project/venv/lib/python2.7/site-packages/wtforms/form.py", line 130, in validate
    if not field.validate(self, extra):
  File "/home/plasmasheep/prog/project/venv/lib/python2.7/site-packages/wtforms/fields/core.py", line 164, in validate
    self.pre_validate(form)
  File "/home/plasmasheep/prog/project/venv/lib/python2.7/site-packages/wtforms/fields/core.py", line 465, in pre_validate
    values = list(c[0] for c in self.choices)
TypeError: 'NoneType' object is not iterable

self.choices is None in this case. However, self.data does contain the selected checkboxes. I'm not sure what to make of this.

Was it helpful?

Solution

The problem was that I was adding to choices after I had run validate_on_submit. If I rearrange the project_show like so:

@app.route(app.config["PROJECT_ROUTE"] + "<project_id>",
    methods=["GET", "POST"])
def project_show(project_id):
    """
    Show the files contained in a specific project. It also allows the user
    to upload a new document, much like projects().

    :param int project_id: The ID of the desired project.
    """

    process_form = forms.DocumentProcessForm(prefix="process")

    # The template needs access to the ID of each file and its filename.
    # Note where this has been moved!
    process_form.files.choices = []
    file_objects = GetFiles(project_id) # database query, this works fine
    for file_object in file_objects:
        process_form.files.choices.append((file_object.id,
            os.path.split(file_object.path)[1]))

    if process_form.validate_on_submit(): #This works fine
        files = request.form.getlist("process-files")
        if len(files) > 0:
            if request.form["action"] == process_form.DELETE:
                delete(files)
            elif request.form["action"] == process_form.PROCESS:
                #TODO: process these files.
                pass

    return render_template("document_list.html",
        process_form=process_form,
        allowed_extensions=app.config["ALLOWED_EXTENSIONS"])

Then everything works fine. Further reading here and here.

OTHER TIPS

This line

 files = MultiCheckboxField("Select", coerce=int)

should be changed to something like:

 my_choices = [('0', 'PROCESS'), ('-1', 'DELETE')]
 files = MultiCheckboxField("Select", choices = my_choices, coerce=int)

You need to provide the "choices" attribute when you instantiate your files field.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top