Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple forms in a single page using flask and WTForms

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?

like image 211
iamgopal Avatar asked Aug 17 '13 15:08

iamgopal


3 Answers

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>
like image 196
Grey Li Avatar answered Nov 19 '22 15:11

Grey Li


I've been using a combination of two flask snippets. The first adds a prefix to a form and then you check for the prefix with validate_on_submit(). I use also Louis Roché's template to determine what buttons are pushed in a form.

To quote Dan Jacob:

Example:

form1 = FormA(prefix="form1")
form2 = FormB(prefix="form2")
form3 = FormC(prefix="form3")

Then, add a hidden field (or just check a submit field):

if form1.validate_on_submit() and form1.submit.data:

To quote Louis Roché's:

I have in my template :

<input type="submit" name="btn" value="Save">
<input type="submit" name="btn" value="Cancel">

And to figure out which button was passed server side I have in my views.py file:

if request.form['btn'] == 'Save':
    something0
else:
    something1
like image 42
AlexLordThorsen Avatar answered Nov 19 '22 13:11

AlexLordThorsen


A simple way is to have different names for different submit fields. For an example:

forms.py:

class Login(Form):

    ...
    login = SubmitField('Login')


class Register(Form):

    ...
    register = SubmitField('Register')

views.py:

@main.route('/')
def index():

    login_form = Login()
    register_form = Register()


    if login_form.validate_on_submit() and login_form.login.data:
        print "Login form is submitted"

    elif register_form.validate_on_submit() and register_form.register.data:
        print "Register form is submitted"

    ...
like image 23
Hieu Avatar answered Nov 19 '22 14:11

Hieu