--- 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 %} ```