[Flask]フォームとの連携

Python

このページでは、Flaskを使ってフォームからのリクエストを受け取り、それをブラウザで表示させる、ということを行います。

そのなかで、テンプレートの継承、具体的なデータの取り出し方、を見ていきます。

参考ページ
Flaskへようこそ ―― Flask Documentation
Jinja ―― Jinja Documentation

使用した環境
WSL2 Ubuntu - 20.04
Python 3.10.0
Flask 2.1.3

 Androidアプリを作成しました。
 感情用のメモ帳です。

スポンサーリンク
スポンサーリンク

テンプレートの継承

まずはモジュールを作成し、ルーティングを行います。

from flask import Flask, render_template


app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')

if __name__ == '__main__':
    app.run(debug=True)

つぎに継承元と継承先のテンプレートを作成します。

継承元――親となるテンプレート。必要なHTMLを書きます。

継承先――子となるテンプレート。親の内容を引き継ぎ、重複している部分を省略できます。ここで記述するのは、親で示された「ブロック」のところです。

親テンプレートの名前は「base.html」とし、子テンプレートはルーティングで指定した「index.html」。

これらを作成します。

$ tree .
.
├── app.py
└── templates
    ├── base.html
    └── index.html

1 directory, 3 files

これから「base.html」を書きますが、親テンプレートでは、必要なHTMLに加え、「ここが〇〇ブロックですよ」といった目印をつけます。

ブロックの明示はつぎのような形で行います。

{% block ブロックの名前 %}

{% endblock %}

ブロック名は、わかりやすいものを付けます。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    {% block head %}
    <title>Document</title>
    {% endblock %}
  </head>
  <body>
  {% block content %}
  {% endblock %}
  </body>
</html>

2つのブロックがあり、「block head」と「block content」です。

今度は子となる「index.html」で、この2つのブロック部分の内容を書いていきます。

冒頭に「extends」文を追加し、つぎのような書き方をします。

{% extends 継承元のファイル名 %}

{% block ブロックの名前 %}
<!-- ここに内容を書く -->
{% endblock %}

{% extends 'base.html' %}
{% block head %}
<title>Index Page</title>
{% endblock %}
{% block content %}
<h1>インデックスページ</h1>
<p>インデックスです</p>
{% endblock %}

blockとendblockは2つでセットなので、閉じ忘れに注意しましょう。

それではサーバーを起動して確認します。

$ python app.py 
 * Serving Flask app 'app' (lazy loading)
 * Environment: development
 * Debug mode: on
 * Running on http://127.0.0.1:5000 (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
インデックスページ

継承できているようですが、念のため、ページのソースコードを表示し、それをコピペします。


<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    
<title>Index Page</title>

  </head>
  <body>
  
<h1>インデックスページ</h1>
<p>インデックスです</p>

  </body>
</html

インデントや空行は目立ちますが、動作に問題はなく、ちゃんと継承できています。

空行になっているのは、開始と終了ブロックの宣言文です。
これが気になるという人は以下を参照してください。

Whitespace Control - Jinja Documentation

フォームの作成

フォームのページを「/form」に設定します。

from flask import Flask, render_template


app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/form')
def form():
    return render_template('form.html')

if __name__ == '__main__':
    app.run(debug=True)

つぎは「form.html」を作成します。

HTMLでのフォーム作成と、FlaskでCSSファイルを適用させる方法は、それぞれ以前やったので、以下を参照してください。

{% extends 'base.html' %}

{% block head %}
<title>フォーム</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
{% endblock %}

{% block content %}
<h1>プロフィールの入力</h1>
<form action="#" method="post">
  <div>
    <label for="user-name">名前</label>
    <input type="text" id="user-name" name="name" required>
  </div>
  <div>
    <label for="user-age">年齢</label>
    <input type="number" id="user-age" name="age" min="0" max="100" required>
  </div>
  <div>
    <span>性別</span>
    <input type="radio" id="gender_choice1" name="gender" value="male">
    <label for="gender_choice1">男性</label>

    <input type="radio" id="gender_choice2" name="gender" value="female">
    <label for="gender_choice2">女性</label>

    <input type="radio" id="gender_choice3" name="gender" value="another" checked>
    <label for="gender_choice3">その他</label>
  </div>
  <div class="e-mail">
    <label for="email">e-mail</label>
    <input type="email" id="email" name="e-mail" required>
  </div>
  <div>
    <label for="message">メッセージ</label><br>
    <textarea name="msg" id="message" cols="50" rows="10" maxlength="200" placeholder="ひと言どうぞ!"></textarea>
  </div>
  <button>送信</button>
</form>
{% endblock %}

用意した入力欄は5つ。
あとでデータを取り出すときにname属性の値が必要になります。

  • 名前(name="name"
  • 年齢(name="age"
  • 性別(name="gender"
  • e-mail(name="e-mail"
  • メッセージ(name="msg"

合わせてCSSファイルを「static」配下に置きます。

h1 {
    text-align: center;
}

form {
    margin: 0 auto;
    width: 400px;
    padding: 20px;
    border: 1px solid;
}

#user_age {
    width: 40px;
}

.e-mail {
    margin-bottom: 7px;
}

ディレクトリの構成

$ tree .
.
├── app.py
├── static
│   └── style.css
└── templates
    ├── base.html
    ├── form.html
    └── index.html

2 directories, 5 files

ここで動作確認します。

$ python app.py 
 * Serving Flask app 'app' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://127.0.0.1:5000 (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!

「http://127.0.0.1:5000/form」にアクセス。

ブラウザでプロフィールの入力ページを表示したところ

見た目はまずまずです。

ただし、遷移先は仮の措置としてこのページのトップにしています。

この「/form」では「GETメソッド」のみの対応であるため、送信ボタンを押したとしても「405 Method Not Allowed」が出ます。

つぎの項で送信先のページを作成し、あわせてPOSTに対応させるようにします。

データの取り出し方

フォームからのデータを取り出すにはrequestをインポートします。

このrequestに対し、

GETメソッドなら「args」、POSTメソッドなら「form」を使って、データを取り出します。

ちなみにrequestはFlaskで定義されている変数、argsやformはプロパティです。

GETメソッド
request.args[key]
request.args.get(key)

POSTメソッド
request.form[key]
request.form.get(key)

キーに使うものは、HTMLの<input>や<textarea>などのname属性の値です。

取り出し方には、それぞれ二通りの方法があります。

辞書と同様の取り出し方では、もしキーが存在しなかった場合、「400 Bad Request」エラーになります。

getの場合、キーがなかったときは「None」を返します。

次の項目では実際にPOSTでの取り出しをやってみます。

送信先ページの設定

ルーティング

「app.py」に追記します。

from flask import Flask, render_template, request


app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/form')
def form():
    return render_template('form.html')

@app.route('/print-values', methods=['GET', 'POST'])
def print_values():
    if request.method == 'POST':
        user_data = request.form
        return render_template('values.html', user_data=user_data)
    else:
        return render_template('no-value.html')

if __name__ == '__main__':
    app.run(debug=True)
@app.route('/print-values', methods=['GET', 'POST'])

フォームのメソッドをPOSTに設定しているので、引数のmethodsに「POST」を追加します。
※デフォルト値は['GET']のみです。


if request.method == 'POST':

requestを通してそのメソッドを判断。


user_data = request.form

今回のメソッドがPOSTのため「form」を使います。GETの場合なら「args」。

user_dataにはImmutableMultiDict型」のデータが入ります。


    if request.method == 'POST':
        user_data = request.form
        return render_template('values.html', user_data=user_data)
    else:
        return render_template('no-value.html')

POSTだったときは「values.html」と「user_data」を渡します。

ここでフォームの入力データをひとつずつ変数に入れ、render_templateに渡すこともできます。(例・name = request.form['name'])

しかし、キーと値の両方を使いたいので、ImmutableMultiDict型の「user_data」をそのまま渡し、テンプレート内で処理したいと思います。

GETだったときは「no-value.html」を渡します。

データの取り出し

フォームのメソッドがPOSTだった場合のテンプレート「values.html」を追加。

{% extends 'base.html' %}

{% block head %}
<title>データの出力</title>
{% endblock %}

{% block content %}
<h1>キーとバリュー</h1>
<div>
  <h2>キー一覧</h2>
  <ul>
  {% for key in user_data.keys() %}
    <li>{{ key }}</li>
  {% endfor %}
  </ul>
</div>
<div>
  <h2>入力されたデータ</h2>
  <p>名前: {{ user_data['name'] }}</p>
  <p>年齢:{{ user_data['age'] }}</p>
  <p>性別:{{ user_data['gender'] }}</p>
  <p>メール:{{ user_data.get('e-mail') }}</p>
  <p>メッセージ:{{ user_data.get('msg') }}</p>
</div>
{% endblock %}

「user_data」から値を取り出しました。

キーとなる<input>や<textarea>のname属性はつぎのようなものでしたね。

  • 名前(name="name"
  • 年齢(name="age"
  • 性別(name="gender"
  • e-mail(name="e-mail"
  • メッセージ(name="msg"

これでフォームデータの送信先ページが作成できました。
「form.html」のデータの送信先を'#'から変更します。<form>タグの「action」の値をつぎのように変更してください。

<form action="{{ url_for('print_values') }}" method="post">

URLを直接書くのではなく、url_forの引数に関数名を渡し、URLを取り出します。

つぎはフォームのメソッドがGETだったときの「no-value.html」です。

{% extends 'base.html' %}
{% block head %}
{{ super() }}
{% endblock %}
{% block content %}
<h1>値がありません</h1>
<p>GETメソッドです</p>
{% endblock %}

{{ super() }}で親テンプレートの内容をそのまま使います。

つまり「base.html」の{ % block head %}の中身です。
このようになっていました。

    <title>Document</title>

ぜんたいの最終的なディレクトリ構成です。

$ tree .
.
├── app.py
├── static
│   └── style.css
└── templates
    ├── base.html
    ├── form.html
    ├── index.html
    ├── no-value.html
    └── values.html

2 directories, 7 files

最終確認

それでは実行し、追加したURLにアクセスしてみましょう。

http://127.0.0.1:5000/print-values

値がありません、GETメソッドです、と表示されている

URLを手打ちして直接アクセスすると、GETメソッドとなるため、想定どおりの表示です。

http://127.0.0.1:5000/form

名前欄に「日本花子」、年齢「26」、性別「女性」、e-mail「hoge@example.com」、メッセージ「よろしくね!」

上の画像のようにプロフィールを埋めて送信すると↓

http://127.0.0.1:5000/print-values に遷移します。

フォームで入力されたデータが表示されている

今度の場合はPOSTだったため、valuse.htmlが呼ばれました。

入力されたデータも間違いありません。

まとめ

今回は自分でいちからフォームを作成しました。

フォームに関しては「WTForms」というライブラリもあります。

バリデーション(入力値のチェック)やCSRF対策がされていて、フォームを扱いやすくするライブラリです。

WTFormsを使ったフォーム検証 — Flask Documentation (2.0.x)

CSRFとは[e-words]

<< 前の記事
FizzBuzzの結果を表示する

次の記事>>
データベースとの連携

タイトルとURLをコピーしました