Multiple forms in a single page using flask and WTForms

flask, flask-wtforms, forms

Solution

The solution above have a validation bug, when one form cause a validation error, both forms display an error message. I change the order of `if` to solve this problem.

First, define your multiple `SubmitField` with different names, like this:

class Form1(Form):
    name = StringField('name')
    submit1 = SubmitField('submit')

class Form2(Form):
    name = StringField('name')
    submit2 = SubmitField('submit')

....

Then add a filter in `view.py`:

....
form1 = Form1()
form2 = Form2()
....

if form1.submit1.data and form1.validate(): # notice the order 
....
if form2.submit2.data and form2.validate(): # notice the order 
....

Now the problem was solved.

If you want to dive into it, then continue read.

Here is `validate_on_submit()`:

def validate_on_submit(self):
    """
    Checks if form has been submitted and if so runs validate. This is
    a shortcut, equivalent to ``form.is_submitted() and form.validate()``
    """
    return self.is_submitted() and self.validate()

And here is `is_submitted()`:

def is_submitted():
    """Consider the form submitted if there is an active request and
    the method is ``POST``, ``PUT``, ``PATCH``, or ``DELETE``.
    """
    return _is_submitted()  # bool(request) and request.method in SUBMIT_METHODS

When you call `form.validate_on_submit()`, it check if form is submitted by the HTTP method no matter which submit button was clicked. So the little trick above is just add a filter (to check if submit has data, i.e., `form1.submit1.data`).

Besides, we change the order of `if`, so when we click one submit, it only call `validate()` to this form, preventing the validation error for both form.

The story isn't over yet. Here is `.data`:

@property
def data(self):
    return dict((name, f.data) for name, f in iteritems(self._fields))

It return a dict with field name(key) and field data(value), however, our two form submit button has same name `submit`(key)!

When we click the first submit button(in form1), the call from `form1.submit1.data` return a dict like this:

temp = {'submit': True}

There is no doubt when we call `if form1.submit.data:`, it return `True`.

When we click the second submit button(in form2), the call to `.data` in `if form1.submit.data:` add a key-value in dict first, then the call from `if form2.submit.data:` add another key-value, in the end, the dict will like this:

temp = {'submit': False, 'submit': True}

Now we call `if form1.submit.data:`, it return `True`, even if the submit button we clicked was in form2.

That's why we need to define this two `SubmitField` with different names. By the way, thanks for reading(to here)!

Update

There is another way to handle multiple forms on one page. You can use multiple views to handle forms. For example:

...
@app.route('/')
def index():
    register_form = RegisterForm()
    login_form = LoginForm()
    return render_template('index.html', register_form=register_form, login_form=login_form)

@app.route('/register', methods=['POST'])
def register():
    register_form = RegisterForm()
    login_form = LoginForm()

    if register_form.validate_on_submit():
        ...  # handle the register form
    # render the same template to pass the error message
    # or pass `form.errors` with `flash()` or `session` then redirect to /
    return render_template('index.html', register_form=register_form, login_form=login_form)


@app.route('/login', methods=['POST'])
def login():
    register_form = RegisterForm()
    login_form = LoginForm()

    if login_form.validate_on_submit():
        ...  # handle the login form
    # render the same template to pass the error message
    # or pass `form.errors` with `flash()` or `session` then redirect to /
    return render_template('index.html', register_form=register_form, login_form=login_form)

In the template (index.html), you need to render both forms and set the `action` attribute to target view:

<h1>Register</h1>
<form action="{{ url_for('register') }}" method="post">
    {{ register_form.username }}
    {{ register_form.password }}
    {{ register_form.email }}
</form>

<h1>Login</h1>
<form action="{{ url_for('login') }}" method="post">
    {{ login_form.username }}
    {{ login_form.password }}
</form>

Problem

I have multiple form on the same page that send post request to same handler in flask. I am generating forms using wtforms. what is the best way to identify which form is submitted ? I am currently using `action="?form=oneform"`. I think there should be some better method to achieve the same?

Original source

Related problems