---
title: Flask - Web Form
tags: python, flask, Web Form, template
---
# ***Flask - Web Form***
> [TOC]
>
> Reference Website:
> 1. [flask核心之應用上下文及請求上下文](https://www.itread01.com/content/1542197107.html)
> 2. [Flask-WTF 表單驗證](https://medium.com/pyladies-taiwan/flask-wtf-%E8%A1%A8%E5%96%AE%E9%A9%97%E8%AD%89-4b4423eeeb45)
> 3. [Python Flask-web表單使用詳解](https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/362941/)
> 4. [在Python的Flask框架中構建Web表單的教程](https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/368818/)
> 5. [[HTTP]Http GET、POST Method](https://dotblogs.com.tw/marcus116/archive/2011/05/29/26428.aspx)
> 6. [HTTP 請求方法](https://developer.mozilla.org/zh-TW/docs/Web/HTTP/Methods)
> 7. [淺談 HTTP Method:表單中的 GET 與 POST 有什麼差別?](https://blog.toright.com/posts/1203/%E6%B7%BA%E8%AB%87-http-method%EF%BC%9A%E8%A1%A8%E5%96%AE%E4%B8%AD%E7%9A%84-get-%E8%88%87-post-%E6%9C%89%E4%BB%80%E9%BA%BC%E5%B7%AE%E5%88%A5%EF%BC%9F.html)
> 8. [Web 技術中的 Session 是什麼?](http://fred-zone.blogspot.com/2014/01/web-session.html)
> 9. [Cookie & Session](https://ithelp.ithome.com.tw/articles/10187212)
---
## **Web Form**
* 由使用者提供資料來讓伺服器接收與處理。
* [HTML建立的Web表單](https://en.wikipedia.org/wiki/Form_(HTML)),通常使用POST request。
* Flask request物件可揭露用戶端用request送來的所有資訊,```request.form```讀取使用者資訊。
### ***[Flask-WTF](http://pythonhosted.org/Flask-WTF)***
* 內含無關特定框架的[WTForms套件](http://wtforms.simplecodes.com/)。
* Flask-WTF不需要做app層級的初始化。
* 首先使用`pip`安裝它:
```
(venv) $ pip install flask-wtf
```
* 在app組態設置密鑰(secret key):
* 防止使用者session的內容被算改。
* 避免受到跨網站請求造照(CSRF,cross-site request forgery)的攻擊。
```python=
### hello.py ###
from flask import Flask
app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'
```
> `app.config字典`是儲存Flask、擴充套件與app本身使用的組態變數的通用位置。
* 在app定義表單類別:
* 每一個Web表單都是用一個繼承FlaskForm的類別來表示。
* `類別`負責定義表單的欄位串,每一個`欄位`用一個物件來表示。每一個`欄位物件`可附加一或多個驗證函式,`驗證函式`檢查使用者送來的資料是否有效。
* [WTForms支援的標準HTML欄位 (Basic fields)](https://wtforms.readthedocs.io/en/stable/fields.html)
* [WTForms內建的驗證函式 (Built-in validators)](https://wtforms.readthedocs.io/en/stable/validators.html)
```python=
### hello.py ###
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
class NameForm(FlaskForm):
name = StringField('What is your name?', validators=[DataRequired()])
submit = SubmitField('Submit')
```
> `name`文字欄位:
> * `StringField`類別代表`type="text"屬性的HTML<input>元素`。
> * 第一個引數`'What is your name?'`將轉為`HTML<label>的元素`。
> * `validators`定義驗證函式,`DataRequired()`確保欄位非空值。
> `submit`送出按鈕:
> * `SubmitField`類別代表`type="submit"屬性的HTML<input>元素`
* 轉譯表單HTML:
```htmlmixed=
<!-- webform.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Web Form</title>
</head>
<body>
<form method="POST">
{{ form.hidden_tag() }}
{{ form.name.label }} {{ form.name() }}
{{ form.submit() }}
</form>
</body>
</html>
```
* `{{ form.hidden_tag() }}`定義一個隱藏的表單欄位,可用來實作CSRF防護。
* `{{ form.name(id='my-text-field') }}`可透過`id='my-text-field'`定義CSS樣式
* 在app的view函式中處理表單:
```python=
### hello.py ###
from flask import render_template
@app.route('/', methods=['GET', 'POST'])
def index():
name = None
form = NameForm()
if form.validate_on_submit():
name = form.name.data
form.name.data = ''
return render_template('webform.html', form=form, name=name)
```
* `validate_on_submit()`方法:在表單被送出且所有欄位驗證函式都收到資料時回傳True。
### ***Bootstrap表單模板***
* 透過匯入Bootstrap的表單模板,來轉譯Flask-WTF表單。
* Bootstrap表單模板,位於`venv/Lib/site-packages/flask_bootstrap/templates/bootstrap/bootstrap/wtf.html`
* 首先使用`pip`安裝flask-bootstrap:
```
(venv) $ pip install flask-bootstrap
```
* 在建立app實例時同時初始化:
```python=
### hello.py ###
from flask_bootstrap import Bootstrap
bootstrap = Bootstrap(app)
```
* 新增一個基礎模版`base_index.html`,其會繼承`bootstrap/base.html`
```htmlmixed=
<!-- base_index.html -->
{% extends 'bootstrap/base.html' %}
{% block head %}
{{ super() }}
<title>{% block title %}{% endblock %} - My Application</title>
{% endblock %}
{% block content %}
<div class="container">
{% block page_content %}{% endblock %}
</div>
{% endblock %}
```
* `webform.html`繼承`base_index.html`基礎模板,並套用Bootstrap表單模板
```htmlmixed=
<!-- webform.html -->
{% extends "base_index.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}webform{% endblock %}
{% block head %}
{{ super() }}
{% endblock %}
{% block page_content %}
<div class="page_header">
<h1>Hello,
{% if name %}
{{ name }}
{% else %}
Stranger
{% endif %}!</h1>
</div>
{{ wtf.quick_form(form) }}
{% endblock %}
```
* `wtf.quick_form()`接收Flask-WTF表單物件。
### ***轉址與使用者session***
* 目的:解決表單送出後,按下瀏覽器的重新整理,所出現的警告框。(如下圖)
* 原因:當要求瀏覽器重新整理網頁時,瀏覽器會重複送出上一個送出的request,亦即重複送出表單資料。

:::success
* 常見的四種回應:
1. [HTTP狀態碼](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes):Flask預設為200,代表request已被成功執行。
```python=
@app.route('/')
def index():
return '<h1>Bad Request</h1>', 400
```
2. 回應物件:以下範例建立一個回應物件,接著在裡面設置一個cookie。
```python=
from flask import make_response
@app.route('/')
def index():
response = make_response('<h1>This document carries a cookie!</h1>')
response.set_cookie('answer', '42')
return response
```
| 屬性或方法 | 說明 |
| -------- | ---- |
| status_code | HTTP 數字狀態碼 |
| headers |類似字典的物件,裡面有準備以回應傳送的所有標頭。|
| set_cookie() |將一個cookie加入回應。|
| delete_cookie() |移除一個cookie。|
| content_length |回應內文的長度。|
| content_type |回應內文的媒體類型。|
| set_data |將回應內文設成字串或byte值。|
| get_data |取得回應內文。|
3. 轉址(redirect):``` redirect()```為一種特殊的回應,常用於處理web表單。
```python=
from flask import redirect
@app.route('/')
def index():
return redirect('http://www.example.com')
```
4. [錯誤處理](https://www.cnblogs.com/luo630/p/9062739.html):```abort()```為一種特殊的回應,用於處理錯誤。
```python=
from flask import abort
@app.route('/user/<id>')
def get_user(id):
user = load_user(id)
if not user:
abort(404)
return '<h1>Hello, {}</h1>'.format(user.name)
```
:::
* 解決方法:`Post/Redirect/Get`模式
* 使用`轉址(Redirect)`來回應POST請求,轉址的內容為URL。
* 當瀏覽器收到轉址回應時,會用GET發出轉址URL,此URL就是顯示的網頁。
* 另外,當Post request一結束,表單資料即消失。因此需要透過`session(私用的存放區)`在兩次request之間"記得"它。
* 流程:
1. 使用者輸入表單內容並送出,發出`POST`請求
2. Flask_app將表單資料存在session,並發出`Redirect回應`
3. 瀏覽器收到轉址URL和session,使用`GET`顯示網頁
```python=
### hello.py ###
from flask import Flask, render_template, session, redirect, url_for
@app.route('/', methods=['GET', 'POST'])
def index():
form = NameForm()
if form.validate_on_submit():
session['name'] = form.name.data
return redirect(url_for('index'))
return render_template('webform.html', form=form, name=session.get('name'))
```
> * [`url_for(endpoint)`](https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/575272/):第一個且唯一的引數是endpoint(端點)。
> * 預設下,路由的endpoint為`view函式名稱`。
> * 呼叫url_for('index', _external=True)
> * 收到絕對URL,在本範例為http://localhost:5000/。
> * `redirect(url_for('index'))`亦可寫成`redirect('/')`
:::success
==Request==
* 用戶端發出request,伺服器會接收並呼叫view函式來處理。
* Flask以`@app.route裝飾器`或`app.add_url_rule()無裝飾器`的版本來建立URL map,`URL map`存有URL與view函式之間的對應關係。
```python=
### test.py ###
from flask import Flask
app = Flask(__name__)
@app.route('/test1')
def test1():
return '<h1>Test1</h1>'
def test2():
return '<h1>Test2</h1>'
app.add_url_rule('/test2', 'test2', test2)
# app.add_url_rule(URL, 端點名稱, view函式)
```
* Python Shell查看``` test.py```的URL map
```
(venv) $ python
>>> from test import app
>>> app.url_map
Map([<Rule '/test1' (OPTIONS, HEAD, GET) -> test1>,
<Rule '/test2' (OPTIONS, HEAD, GET) -> test2>,
<Rule '/static/<filename>' (OPTIONS, HEAD, GET) -> static>])
```
* ```(OPTIONS, HEAD, GET)```為路由要處理的request方法,通常會指明用戶端要求伺服器執行的動作。
* Flask會自動管理OPTIONS和HEAD方法。
---
* ==request物件==,封裝了從用戶端送來的HTTP request內容。
* 當Flask從用戶端收到request時,必須讓處理它的view函式能夠使用一些物件。其中之一為「request物件」。
* Flask會用一個名為request的[context變數](https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/119879/)來公開request物件。
* ==request勾點 (request hook)==:一種裝飾器,在每一個request之前或之後執行一些程式碼。
* 目的:避免每一個view函式內放入重複的程式碼。
* 例如:處理每一個request之前,需要連接資料庫或驗證發出請求的使用者。
* Flask提供四種:
1. `before_request`:註冊在每一個request之前執行的函式。
2. `before_first_request`:註冊只需要在處理第一個request之前執行的函式,適合用來加入伺服器初始化工作。
3. `after_request`:註冊需要在每一個request之後執行的函式,但是只會在沒有被處理的異常狀況時執行。
4. `teardown_request`:註冊需要在每一個request之後執行的函式,即使在有未處理的異常情況下。
:::
### ***閃現(flash)訊息***
* 顯示更新訊息,可能用於確認、警告或錯誤。
* 每當name被送出時,就會拿來和session內的name做比較。若不同,則呼叫flash()。
```python=
### hello.py ###
from flask import Flask, render_template, session, redirect, url_for, flash
@app.route('/', methods=['GET', 'POST'])
def index():
form = NameForm()
if form.validate_on_submit():
old_name = session.get('name')
if old_name is not None and old_name != form.name.data:
flash('Looks like tou have changed your name!')
session['name'] = form.name.data
return redirect(url_for('index'))
return render_template('webform.html', form=form, name=session.get('name'))
```
* 修改`base_index.html`基礎模板,以轉譯app所定義的訊息。
* 基礎模板為轉譯閃現訊息的最佳地點,因為可套用在所有網頁。
```htmlmixed=
<!-- base_index.html -->
{% block content %}
<div class="container">
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">×</button>
{{ message }}
</div>
{% endfor %}
{% block page_content %}{% endblock %}
</div>
{% endblock %}
```
> `get_flashed_messages()`得到的訊息在下次呼叫此函數時就無法取得,故只會閃現一次。
## 用 Flask-Moment 來將日期與時間當地化
* 讓瀏覽器讀取電腦的時區與地區,並用JavaScript轉換並轉譯成當地的時間。
* [Moment.js](http://momentjs.com):Flask擴充套件,需要jQuery.js。
```
(venv) $ pip install flask-moment
```
* ```hello.py```:初始化Flask-Moment
```python=
from flask_moment import Moment
moment = Moment(app)
```
* ```templates/base_index.html```:匯入Moment.js程式庫
* 若使用Flask-Bootstrap,則只需加入Moment.js即可。
```htmlmixed=
{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{% endblock %}
```
* 使用時戳(timestamp)
* ```hello.py```:添加datetime變數
```python=
from datetime import datetime
@app.route('/')
def index():
return render_template('webform.html', current_time=datetime.utcnow())
```
* ```templates/webform.html```:用Flask-Moment來轉譯時戳
```htmlmixed=
{% block content %}
<p>The local date and time is {{ moment(current_time).format('LLL') }}.</p>
<p>That was {{ moment(current_time).fromNow(refresh=True) }}</p>
{{ super() }}
{% endblock %}
```
* ```format('LLL')```會根據電腦的時區與地區來轉譯日期與時間。
* 從```'L'```到```'LLLL'```代表四個等級的詳細程度。
* ```fromNow()```會轉譯相對時戳。如:"a few seconds ago"
* [Moment.js文件:時戳格式](http://momentjs.com/docs/#/displaying/)
* 將Flask-Moment轉譯的時戳,轉為西班牙文```'es'```
* [雙字母的語言代碼](https://en.wikipedia.org/wiki/ISO_3166-1)
* ```templates/base_index.html```
```htmlmixed=
{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{{ moment.locale('es')}}
{% endblock %}
```