單個頁面多個表單
除了在單個表單上實現多個提交按鈕,有時還需要在單個頁面上創建多個表單。比如,在程序的主頁上同時添加登錄和注冊表單。當在同一個頁面上添加多個表單時,我們需要解決的問題是在視圖函數中判斷當前被提交的是哪個表單。
單視圖處理
創建兩個表單,並在模板中分別渲染比較容易,但當提交某個表單是,就會遇到問題,Flask-WTF根據請求方法判斷表單是否提交,但並不是判斷是哪個表單被提交,所以我們需要手動判斷,我們知道被單擊的提交字段最終的data屬性值是布爾值,即True或False。而解析后的表單數據使用input字段的name屬性值作為鍵匹配字段數據,就是說,如果兩個表單的提交字段名稱都是submit,那么無法判斷是哪個表單的提交字段被單擊。
解決問題的第一步就是為兩個表單的提交字段設置不同的名稱,示例程序中的兩個表單如下所示:
forms.py:為兩個表單設置不同的提交字段名稱
from wtforms.validators import Email class SigninForm(FlaskForm): username = StringField('Username',validators=[DataRequired(),Length(1,20)]) password = PasswordField('Password', validators=[DataRequired(),Length(8,128)]) submit1 = SubmitField('Sign in') class RegisterForm(FlaskForm): username = StringField('Username', validators=[DataRequired(), Length(1,20)]) email = StringField('Email', validators=[DataRequired(), Email(), Length(1,254)]) password = PasswordField('Password', validators=[DataRequired(), Length(8,128)]) submit2 = SubmitField('Register')
在視圖函數中,我們分別實例化這兩個表單,根據提交字段的值來區分被提交的表單,如下所示:
app.py:
from forms import SigninForm, RegisterForm @app.route('/multi-form', methods=['GET', 'POST']) def multi_form(): signin_form = SigninForm() register_form = RegisterForm() #validate()逐個對字段調用字段實例化時定義的驗證器,返回表示驗證結果的布爾值 if signin_form.submit1.data and signin_form.validate(): username = signin_form.username.data flash('%s, you just submit the Signin Form.' % username) return redirect(url_for('index')) if register_form.submit2.data and register_form.validate(): username = register_form.username.data flash('%s, you just submit the Register Form.' % username) return redirect(url_for('index')) return render_template('2form.html', signin_form = signin_form,register_form = register_form)
在視圖函數中,我們為兩個表單添加了各自的if判斷,在if語句的內如,分別執行各自的邏輯,以Signinform的if判斷為例,如果signin_form.submit1.data的值是True,那么說明用戶提交了登錄表單,這時手動調用signin_form.validate()對表單進行驗證。
這兩個表單類實例通過不同的變量名稱傳入模板,以便在模板中渲染對應的表單字段,如下所示:
2form.html:
{% extends 'base.html' %} {% from 'macros.html' import form_field %} {% block content %} <h1>Multiple Form in One Page with One View</h1> <h3>Login Form</h3> <form method = 'post'> {{ signin_form.csrf_token }} {{ form_field(signin_form.username) }} {{ form_field(signin_form.password) }} {{ signin_form.submit1 }} </form> <h3>Register Form</h3> <form method="post"> {{ register_form.csrf_token }} {{ form_field(register_form.username) }} {{ form_field(register_form.email) }} {{ form_field(register_form.password) }} {{ register_form.submit2 }} </form>
{% endblock %}
訪問127.0.0.1:5000/multi-form頁面,提交某個表單后,會在重定向后的頁面的提示消息中看到提交表單的名稱。
多視圖處理
除了通過提交按鈕判斷,更簡潔的方法是通過分離表單的渲染和驗證實現。這時表單的提交字段可以使用同一個名稱,在視圖函數中處理表單時也只需要使用我們熟悉的form.validate_on_submit()方法。
我們在同一個視圖函數內處理兩類公國:渲染包含表單的模板(GET請求)、處理表單請求(POST請求)。如果想解耦這部分功能,也可以分離成兩個視圖函數處理。當處理多個表單時,我們可以把表單的渲染在單獨的視圖函數中處理,如下所示:
@rouge('/multi-form-multi-view') def multi_form_multi_view(): signin_form = SigninForm() register_form = RegisterForm() return render_template('2form2view.html', signin_form=signin_form, register_form=register_form)
這個視圖只負責Get請求,實例化兩個表單類並渲染模板。另外再為每一個表單單獨創建一個視圖函數來處理驗證工作。處理表單提交請求的視圖僅監聽POST請求。如下所示:
app.py:使用單獨的視圖函數處理表單提交的POST請求
@app.route('/handle-signin', methods=['POST']) # 僅傳入POST到methods中 def handle_signin(): signin_form = SigninForm() register_form = RegisterForm() if signin_form.validate_on_submit(): username = signin_form.username.data flash('%s , yoou just submit the Signin Form.' % username) return redirect(url_for('index')) return render_template('2form2view.html', signin_form = signin_form, register_form = register_form) @app.route('/handle-register', methods=['POST']) def handle_register(): signin_form = SigninForm() register_form = RegisterForm() if register_form.validate_on_submit(): username = register_form.username.data flash('%s, you just submit the Register Form.' % username) return redirect(url_for('index')) return render_template('2form2view.html', signin_form = signin_form, register_form = register_form)
在模板中,表單提交請求的目標URL通過action屬性設置,為了讓表單提交時將請求發送到對應的URL,我們需要設置action屬性,如下所示:
2form2view.html:
{% extends 'base.html' %} {% from 'macros.html' import form_field %} {% block content %} <h2>Multiple Form in One Page with Multiple View</h2> <h3>Login Form</h3> <form meghod="post" action="{{ url_for('handle_signin') }}"> {{ signin_form.csrf_token }} {{ form_field(signin_form.username) }} {{ form_field(signin_form.password) }} {{ signin_form.submit }} </form> <h3>Register Form</h3> <form method="post" action="{{ url_for('handle_register') }}"> {{ register_form.csrf_token }} {{ form_field(register_form.username) }} {{ form_field(register_form.email) }} {{ form_field(register_form.password) }} {{ register_form.submit }} </form> {% endblock %}
例中index視圖:
@app.route('/index') def index(): return render_template('index.html') index.html: {% extends 'base.html' %} {% block content %} <h1>Forms</h1> <ul> <li><a href="{{ url_for('basic') }}">Basic Form</a></li> <li><a href="{{ url_for('upload') }}">File Upload</a></li> <li><a href="{{ url_for('multi_upload') }}">Multiple Files Upload</a></li> <li><a href="{{ url_for('two_submits') }}">Multiple Submit Buttons</a></li> <li><a href="{{ url_for('multi_form') }}">Multiple Form</a></li> <li><a href="{{ url_for('multi_form_multi_view') }}">Multiple form Multiple view</a></li> </ul> {% endblock %}
訪問127.0.0.1:5000/2form2view.html,填入必填內容,點擊提交,在重定向后的頁面上提示提交表單的名稱
雖然現在可以正常工作,但是有一個缺點,如果驗證未通過,需要將錯誤消息的form.errors字典傳入模板中。如:
{% for message in register_form.username.errors %} <small class="error">{{ message }}</small><br>
處理錯誤處理
在處理表單的視圖中傳入表單錯誤信息,就意味着需要再次渲染模板,但是如果視圖函數中還涉及大量要傳入模板的變量操作,那么這種方式會帶來大量的重復。
對於這個問題,一般的解決方式是通過其他方式傳遞錯誤消息,然后統一重定向到渲染表單頁面的視圖。比如使用flash()函數迭代form.errors字典發送錯誤消息的函數:
def flash_errors(form):
for field, errors in form.errors.items():
for error in errors:
flash(u"Error in the %s field - %s" % (getattr(form, field).label.text, error))
如果你希望像往常一樣在表單字段下渲染錯誤消息,可以直接將錯誤消息字段form.errors存儲到session中,然后重定向到用來渲染表單的multi_form_multi_view視圖。在模板中渲染表單字段錯誤時添加一個額外的判斷,從session獲取並遍歷錯誤消息。
在本例中,錯誤是在宏里面渲染的:
{% macro form_field(field) %} {{ field.label }}<br> {{ field(**kwargs) }}<br> {% if field.errors %} {% for error in field.errors %} <small class="error">{{ error }}</small><br> {% endfor %} {% endif %} {% endmacro %}