---
# System prepended metadata

title: Flask - Web Form
tags: [python, ' flask', ' Web Form', ' template']

---

---
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，亦即重複送出表單資料。

![](https://i.imgur.com/RyI4x0W.png)

:::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">&times;</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 %}
            ```