本記事では、PythonのWebアプリケーションフレームワークであるFlaskでwebアプリ等を作る際に、プログラムのパラメータに即時反映されるトグルボタンの機能を実装する方法について記述します。
トグルボタンとは、ユーザーがON/OFFを切り替える用途で使われることの多いボタンで、身近な例だとiPhoneの各アプリケーションの通知機能の有無やwifi接続の有無を切り替える際のボタンとしてよく用います。
今回はwebページにトグルボタンを実装し、トグルボタンの切り替えと連動してバックサイドのプログラムの1パラメータの2値を切り替える機能を設ける簡易手順を示します。
以下画像が今回作成したトグルボタンです。
対応するパラメータとして今回は config.yaml というYAMLファイルの notification キーにonとoffのどちらかを設定することとしました。上画像のようにトグルボタンがOFFの時は config.yaml の notification 値が常にoffとなるように、ONの時はonとなるように実装します。
notification: 'off'
また今回はもう1つの機能として、以下のようにトグルボタンを押した際に確認用のダイアログが表示される機能を実装しました。
環境
- MacBook Pro
- Python3.10.6
- Flask 2.2.2
- Bootstrap 4
実装方法
ディレクトリ構成
ディレクトリは以下の様に構成します。今回はBootstrapというCSSのフレームワークを用います。
Flaskで作るWebアプリケーションにBootstrapを適用する場合は以下のように static ディレクトリと css ディレクトリの中にそれぞれ対応したcssファイルとjsファイルを配置しておく必要があります。
.
├─ app.py
├─ config
│ └config.yaml
├─ config_utils.py
├─ static
│ ├─css
│ │ └ bootstrap.min.css
│ └─ js
│ ├ bootstrap.bundle.min.js
│ └ jquery-3.5.1.min.js
└─ templates
└ test_toggle.html
コード
app.py と test_toggle.html の内容について、今回はトグルボタンと確認ダイアログの書き方のみを説明するためその他は極めて間便なものとします。
まず app.py は以下のように作成しました。Bootstrapを適用するといっても特別なモジュールをimportする必要はありません。
引数に config.yaml を与え、 app.py の中から__config.yaml__ の値の読み取り、編集を行います。そのための関数として update_notify 関数と update_notify 関数を設けます。
ページングはデフォルトのページとwebページからPOSTが行われた際の処理を行うページを用意します。
import argparse
from flask import Flask, render_template, redirect, url_for, request
from config_utils import ConfigUtils
parser = argparse.ArgumentParser()
parser.add_argument('--config',default='config/config.yaml',type=str)
args = parser.parse_args()
CONFIG_FILEPATH = args.config
def update_notify(yamlfile):
status = ConfigUtils.load(args.config)['notification']
if status == "on":
ConfigUtils.update(args.config,'notification', 'off')
elif status == "off":
ConfigUtils.update(args.config,'notification', 'on')
else:
raise Exception('Invalid value in yaml file')
def read_notify(yamlfile):
config = ConfigUtils.load(args.config)
return config['notification']
app = Flask(__name__)
@app.route("/")
def home():
return render_template('test_toggle.html', notify=read_notify(CONFIG_FILEPATH))
@app.route('/', methods=['POST'])
def post():
if request.form.get('confirm'):
update_notify(CONFIG_FILEPATH)
return redirect(url_for('test'))
return redirect(url_for('test'))
if __name__ == "__main__":
app.run(port=5000)
ConfigUtils クラスは以下の config_utils.py にて作成してimportします。
yamlモジュールの safe_load 関数を用いてYAMLファイルの読み取りと書き込みを行うという極めてシンプルなものとなっています。
import yaml
class ConfigUtils:
@staticmethod
def load(filename):
with open(filename, 'r', encoding='utf-8') as f:
dict_ = yaml.safe_load(f)
return dict_
def update(filename, key, new_value):
with open(filename, 'r+', encoding='utf-8') as f:
dict_ = yaml.safe_load(f)
dict_[key] = new_value
f.truncate(0)
f.seek(0)
yaml.safe_dump(dict_, f, indent=4, sort_keys=False)
次に test_toggle.html は以下のように作成しました。
<!doctype html>
<html lang="ja">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
<title>toggle_test</title>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light p-1" style="background-color: #e3f2fd;">
<div>
<ul>
<form class="form-inline" method="post" action="/">
<div style="display: inline-block" class="custom-control custom-switch">
{% if notify=="on" %}
<input type="checkbox" checked name="notification" class="custom-control-input" id="customSwitch1" data-toggle="modal" data-target="#confirm-change2">
{% else %}
<input type="checkbox" name="notification" class="custom-control-input" id="customSwitch1" data-toggle="modal" data-target="#confirm-change2">
{% endif %}
<label class="custom-control-label" for="customSwitch1">通知 </label>
</div>
<div style="display: inline-block" class="custom-control custom-switch">
</div>
<div class="modal fade" id="confirm-change2" data-backdrop="static" data-keyboard="false" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">確認</h5>
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
{% if notify=="on" %}
通知をOFFにしてもよろしいですか?
{% else %}
通知をONにしてもよろしいですか?
{% endif %}
</div>
<div class="modal-footer">
{% if notify=="on" %}
<button id="dataConfirmCancel_on" type="button" class="btn btn-secondary" data-dismiss="modal">キャンセル</button>
{% else %}
<button id="dataConfirmCancel_off" type="button" class="btn btn-secondary" data-dismiss="modal">キャンセル</button>
{% endif %}
<button type="submit" class="btn btn-success" name="confirm" value="{{ notify }}">OK</button>
</div>
</div>
</div>
</div>
</form>
</ul>
</div>
</div>
</nav>
<script src="/static/js/jquery-3.5.1.min.js"></script>
<script src="/static/js/bootstrap.bundle.min.js"></script>
</body>
<script>
$('#dataConfirmCancel_on').click(function() {
$('input[name=notification]').prop('checked', true);
});
</script>
<script>
$('#dataConfirmCancel_off').click(function() {
$('input[name=notification]').prop('checked', false);
});
</script>
</html>
最初の form タグの中でボタンの切り替えの処理が記述されてます。
トグルボタンは input タグの checkbox にて描画されます。その際 app.py の中の home 関数にてページを表示する際に notify という引数を設け、 read_notify 関数で読み取った現時点でのconfigの値を常にhtmlに渡しています。
test_toggle.html の中ではその notify 引数の値を場合分けしてONの状態のトグルボタンかOFFの状態のトグルボタンかのどちらを描画するかが場合分けされています。ONかOFFかはinputタグの中で checked を入れるか入れないかの違いとなっています。
{% if notify=="on" %}
<input type="checkbox" checked name="notification" class="custom-control-input" id="customSwitch1" data-toggle="modal" data-target="#confirm-change2">
{% else %}
<input type="checkbox" name="notification" class="custom-control-input" id="customSwitch1" data-toggle="modal" data-target="#confirm-change2">
{% endif %}
またここで input タグの中の data-target に #confirm-change2 を記入し、以降の確認ダイアログ処理とトグルボタンの処理を連結します。これによりトグルボタンが押下されたら確認ダイアログの処理に移るようになります。
確認ダイアログはそれぞれ div タグの modal-dialog クラス、 modal-content クラス、 modal-header クラス、 modal-body クラスで挟むようにして記述します。今回は既に通知ボタンがONだったらOFFにして良いかの確認、既に通知ボタンがOFFだったらONにして良いかの確認を行う都合上、同じように notify の値で場合分けを行なって表示される文章を変更します。
<div class="modal-body">
{% if notify=="on" %}
通知をOFFにしてもよろしいですか?
{% else %}
通知をONにしてもよろしいですか?
{% endif %}
</div>
トグルボタンによってconfigの値の変更が送信されてるように見えてますが、実際は以下で作成した確認ダイアログのOKボタンを押すことで変更がバックサイドにsubmitされconfigの値の更新処理に移ることができます。
<button type="submit" class="btn btn-success" name="confirm" value="{{ notify }}">OK</button>
ここでsubmitする際にボタンの name を指定することで app.py 側では post() 内で request.form.get() を行うことで、submitされたかどうかを常に確認することができます。今回は2値なので今のconfigの値に関わらず、確認ダイアログのOKボタンが押下された時点で今のconfigの値を逆に更新することで期待通りの挙動を実現することができます
最後にtest_toggle.html最後の以下部分について解説します。
<script>
$('#dataConfirmCancel_on').click(function() {
$('input[name=notification]').prop('checked', true);
});
</script>
<script>
$('#dataConfirmCancel_off').click(function() {
$('input[name=notification]').prop('checked', false);
});
</script>
上記までで内部のconfigと連動したトグルボタンと確認ダイアログは実装することができましたが、1つ問題点としてこのままだと例えば通知OFFの状態からトグルボタンを押して確認ダイアログを表示した後にOKでなくキャンセルボタンを押した場合、config自体はoffからoffのままで変更ありませんが、web上のトグルボタンは押下された状態で見た目がONになったままとなってしまいます。これは確認ダイアログの画面にてキャンセルをして処理を完了した際にwebのページ更新処理が行われないためです。確認ダイアログ画面のキャンセルを押下した後に逐一ブラウザのリフレッシュボタンを押すなどすればトグルボタンの見た目が反映されますが、それだと煩雑になってしまうので今回は以下の script タグを入れました。
これは確認ダイアログ画面のキャンセルボタンにあらかじめ id を割り振っておき、キャンセルボタンが押下された際に指定の function が実行されるようにしたものです。前述した通りトグルボタンのONOFFの見た目は input タグの中の checked のステータスに依存します。
そこで元々通知ONの状態から確認ダイアログを押してキャンセルした場合に指定の input タグの checked ステータスをtrueに戻すようにすることでキャンセル時に見た目もONの状態を維持するようにしました。元々通知OFFの状態も同様に記載することで対応しました。
結果
notification: 'off'
トグルボタンがONに切り替わりconfigの内容も更新されていました。
notification: 'on'