今回はFlaskでTodoアプリを作ってみましょう。
FlaskはPythonのWebフレームワークで、最小限の機能で簡潔にWebアプリを作るのに適しています。
Todoアプリはフレームワークとデータベースを使って登録・更新・削除・一覧表示を一通り学べるので、Webアプリの入門として適しています。
では始めましょう。
なお、ここでは次の環境でアプリ開発をしていきます。
・MacOS Mojave 10.14.6
・Python3.7.6
・Flask1.1.2
Todoアプリの完成像
先にアプリの完成像を見てイメージを固めましょう。
今回は次のような非常にシンプルなTodoアプリを開発します。
テキストボックスにテキストを入力してEnterを押すとタスクが登録され、下の方に順番に表示されていきます。
登録したタスクを長押しすることで完了にすることができ、タスクが白から黄色に変わります。
更新ボタンを押すとタスクがテキストボックスに変わり、内容を変更してEnterを押すことでタスクの更新が可能です。
また、削除ボタンを押すことでタスクの削除ができます。
こんなイメージのアプリを作成していきます。
Flaskをインストールする
まずはFlaskをインストールしましょう。
といってインストール自体はとても簡単です。pipで次のコマンドを実行しましょう。
pip install flask SQLAlchemy flask-sqlalchemy
Flaskをはじめ3つのパッケージをインストールしました。
flaskはFlask本体。SQLAlchemyはPythonでデータベースを扱いやすくする機能で、flask-sqlalchemyはそのSQLAlchemyをFlaskで扱いやすくするようにするために必要です。
必要なファイルの作成
実装の前に必要なファイルをあらかじめ作成しつつ、それぞれのファイルがどんな意味を持つのかを整理しておきましょう。
次のようにファイルを作成してください。
一番上のtodoフォルダは別にtodoという名前じゃなくても大丈夫です。
staticフォルダの下にはcssフォルダとjsフォルダを作成し、cssフォルダ下にはstyle.cssを作っておきましょう。jsフォルダの下にはまだ何も作りません(あとで追加します)。
templatesフォルダの下にはindex.htmlを作成します。todoアプリのhtmlを実装していくファイルです。
あとはtodoフォルダ直下にmain.pyを作成します。ここでFlaskとSQLAlchemyを使ったタスクの取得や登録・更新・削除の処理を実装していきます。
一覧表示と登録処理の実装
では実装していきましょう。まずは一覧表示から実装します。
[main.py]
from flask import Flask, render_template, request, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
import os
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret_key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///todo.sqlite'
db = SQLAlchemy(app)
class Task(db.Model):
__tablename__ = "tasks"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
text = db.Column(db.Text())
status = db.Column(db.Integer)
db.create_all()
@app.route('/')
def index():
tasks = Task.query.all()
return render_template("index.html", tasks = tasks)
app.run(debug=True, host=os.getenv('APP_ADDRESS', 'localhost'), port=8001)
いきなり長めですが、ここが一番長いので、じっくり見ていきましょう。
from flask import Flask, render_template, request, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
import os
はじめにインポート処理をしています。pipでインストールしたFlaskやflask-sqlalchemyから必要になるモジュールをインポートしています。
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret_key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///todo.sqlite'
Flask()を使ってFlaskの機能を使えるappインスタンスを作っています(__name__はおなじない)。
次にFlaskのコンフィグを設定しています。SECRET_KEYはセッション情報を暗号化するために必要な設定で、これをしないでセッションを使おうとするとエラーになります。
また、SQLALCHEMY_DATABASE_URIはデータベースの場所を指定しています。今回はSQLiteを使うので、上のような設定にしました。アプリを起動するとtodoフォルダ直下にtodo.sqliteというデータベースファイルが作成されます。
db = SQLAlchemy(app)
class Task(db.Model):
__tablename__ = "tasks"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
text = db.Column(db.Text())
status = db.Column(db.Integer)
db.create_all()
データベースを作成している部分です。
SQLAlchemyにFlask()で作ったappを渡し、データベースを操作するために必要なdbインスタンスを生成しています。
ここからはこのdbインスタンスを使ってデータベースの定義やデータの取得・登録・更新・削除をやっていきます。
次にTaskというクラスを定義しています。Taskクラスはdb.Modelを継承し、tasksというテーブルの定義を記述します。
db.Modelを継承したクラスを定義することで、あとでDB上に__tablename__で指定したテーブルを自動で作成してくれます。
tasksテーブルにはid, text, statusのカラムを定義しました。idはタスクを判別するためのid、textはタスクの内容、statusには完了・未完了を0, 1で判別するために用意しています。
最後にdb.create_all()でtodo.sqliteファイルとtasksテーブルを作成しています。
@app.route('/')
def index():
tasks = Task.query.all()
return render_template("index.html", tasks = tasks)
ルーティングとindexにアクセスされた場合の処理を記述している部分です。
@app.routeはURLルーティングを実現するためのデコレーターです。ルートにアクセスされた場合、その下のindex()を呼ぶような実装になっています。
index()はタスクの一覧をtasksテーブルから取得し、それをindex.htmlに渡す処理を実装しています。
Taskクラスはdb.Modelを継承しているので、そのModelのquery.all()を呼び出し、tasksテーブルのデータを全て取得しています。
render_template()は1つ目の引数のhtmlにデータを渡し、レンダリングするために使います。
index.htmlはこれから実装しますが、テンプレートエンジンを使うので、中身となるタスクのデータを渡してレンダリングするようなイメージです。
app.run(debug=True, host=os.getenv('APP_ADDRESS', 'localhost'), port=8001)
最後にapp.run()でアプリケーションを起動します。
今回はhost引数にlocalhost、port引数に8001を指定しているので、アプリケーション起動後はhttp://localhost:8001/でWebアプリにアクセスします。
これでmain.pyの実装は一旦完了です。次にindex.htmlに記述していきます。
[index.html]
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="/static/css/style.css">
<title>TODOアプリ</title>
</head>
<body>
<main>
<div id="title" class="title">タスク</div>
<form name="f" method="post">
<div class="text_area"><input type="text" name="new_text" id="new_text"></div>
<table>
{% for task in tasks %}
<tr>
<td
class="card"
id="task_{{task.id}}"
task_id="{{task.id}}">{{ task.text }}
</td>
</tr>
{% endfor %}
</table>
</form>
</main>
</body>
</html>
シンプルなhtmlですが、一部テンプレート機能を使っているので、そこを中心に解説します。
Flaskではデフォルトでjinja2というテンプレートエンジンを使っています。テンプレートエンジンとはテンプレート(html)とデータ(tasks)を使ってドキュメントを完成させる機能のことです。
{% for task in tasks %}と{% endfor %}で挟んだ箇所はtasksのリストデータ分ループさせて、中のtr要素を複数生成していきます。それと同時にtask変数にtasksのリストを1つずつ渡しています。
{{task.id}}や{{task.text}}はtaskリストのidやtextを埋め込んでいます。
つまりここの部分はタスク一覧を動的に生成しているんですね。
これでindex.htmlの実装も一旦完了です。最後にstyle.cssです。
[style.css]
main {
margin:50px;
}
.title {
font-size:24px;
}
.text_area {
margin: 10px 0 10px 0;
}
#new_text {
font-size:24px;
width:600px;
}
table {
border-collapse: separate;
border-spacing: 0px 5px;
}
.card {
width: 590px;
margin:20px;
padding: 10px;
border-radius: 5px;
box-shadow: 0 2px 5px #ccc;
}
.card:hover {
opacity: 0.5;
}
td.button {width:50px;text-align:right;}
.td_update_txt {display:none;}
.txt_update {
width: 98%;
height: 34px;
font-size: 17px;
padding-left: 8px;
border-radius: 5px;
}
(cssは今回の趣旨とは異なるので説明は割愛します)
これで一覧表示部分の実装は完了です。次のコマンドを実行してブラウザからhttp://localhost:8001/にアクセスしてみましょう。
python main.py
まだタスクを1件も登録していないので、テキストボックスの下には何も表示されていません。
これではつまらないので、次にタスク登録処理を実装していきましょう。
main.pyのindex()の下に次の関数を追加します。
[main.py]
@app.route('/new', methods=["POST"])
def new():
task = Task()
task.text = request.form["new_text"]
task.status = 0
db.session.add(task)
db.session.commit()
return redirect(url_for('index'))
登録処理用の関数を新たに追加しました。
@app.routeの1つ目の引数に/newを設定しているので、登録処理はlocalhost:8001/newでリクエストされたときに動作します。
また、methods引数にPOSTを指定しているので、この関数はPOSTでリクエストされることが前提となっています。
中身の処理を見ていきましょう。
まずTaskクラスのインスタンスを生成し、textとstatusにそれぞれ値を設定しています。
textにはrequest.formを使ってフォームのテキストボックスの入力値を取得しています。
あとはdb.session.add()にインスタンスを渡すことで登録できます。ただし、最後にcommit()しないと登録が確定されないので注意しましょう。
登録したらredirect()でindex()へリダイレクトし、index.htmlをレンダリングします。
これでmain.pyは完了です。次にindex.htmlを修正しましょう。
今回index.htmlではjQueryを使って登録イベントを拾うので、まずはjQueryを使えるようにします。
titleタグの下に次のコードを追加します。
[index.html]
<script
src="https://code.jquery.com/jquery-3.5.1.js"
integrity="sha256-QWo7LDvxbWT2tbbQ97B53yJnYU3WhH/C8ycbRAkjPDc="
crossorigin="anonymous"></script>
これでjQueryが使えるようになりました(インターネットに接続されていないと使えないので注意)
次にテキストボックスに値を入力してEnterを押したとき、登録処理が実行されるようにしましょう。
mainの終了タグの後に次のコードを記述します。
[index.html]
<script>
$(document).ready(function(){
$('#new_text').keydown(function(e) {
if (e.keyCode == 13) {
$('form[name="f"]').attr('action', '/new')
$('form[name="f"]').submit();
}
});
});
</script>
keyCodeの13がEnterに該当します。
Enterを押したらフォームのactionに/newを指定しサブミットします。これでEnterを押した時登録処理が動作するようになりました。
では実際に動かしてみましょう。
テキストボックスに入力してEnterを押すことで、main.pyの登録処理→一覧表示処理を経て戻ってきています。
登録したタスクは即座に画面に表示されます。
これで一覧表示と登録処理は完成です。
完了処理を実装する
次に完了処理を実装していきましょう。
今回はタスクを長押しすることでタスクの色を白から黄色に変更します。
ではmain.pyから実装していきましょう。new()の下に次のコードを追加します。
[main.py]
@app.route('/completion', methods=["POST"])
def completion():
id = request.form["id"]
task = Task.query.filter_by(id=id).first()
task.status = 1
db.session.commit()
return redirect(url_for('index'))
/completionのPOSTでリクエストされた場合に完了処理を実行します。
完了・未完了はテーブルのstatusの値で判断します(0が未完了、1が完了)。
今回はタスクのstatusを更新する必要があります。更新は対象のデータを抽出しインスタンスとして保持し、インスタンスのパラメータを変更することで更新できます。
まずhtmlのフォームからidを受け取り、Task.query.filter_by()を使って対象のタスクデータを取得します。
filter_byはSQLで言うところのwhere文に該当しますので、これは select * from tasks where id = id をやっているのと同じになります。
取得したデータはそのままインスタンスとして返却されるので、task.statusを1に変更しました。
例のごとくcommit()は忘れないようにしましょう。
これで更新完了です。最後にリダイレクトして処理は終了です。
次にindex.htmlを修正するんですが、その前に長押しを検知するためのjQueryライブラリを持ってきましょう。
次のURLにアクセスし、jquery.longpress.jsが入ったzipファイルをダウンロードします。
zipの中にjquery.longpress.jsがあるので、それを最初に作ったjsフォルダの下に配置します(todo→static→js)。
これでライブラリの設置は完了です。index.htmlを修正して読み込みましょう。
jQueryの読み込みの後に次のコードを追加します(読み込み前だと正常に動作しません)。
[index.html]
<script type="text/javascript" src="/static/js/jquery.longpress.js"></script>
これでOKです。
次にstatusが1の場合にタスクを黄色にする処理を加えます。
td要素にstyle属性を追加します。
[index.html]
<td
class="card"
id="task_{{task.id}}"
style="{% if task.status == 1 %}background-color:#FFFF00;{% else %}background-color:#FFFFFF;{% endif %}"
task_id="{{task.id}}">{{ task.text }}
</td>
statusの値に応じて分岐が必要なので、テンプレート機能のifを使っています。
task.statusが1の場合#FFFF00(黄色)、0の場合#FFFFFF(白色)が適用されるようになっています。
次に、更新処理にはidが必要なので、hiddenでidを渡します。
フォームの最後に次のコードを追加します。
[index.html]
</table>
<input type="hidden" name="id" id="id">
</form>
</main>
最後に完了処理を追加します。
[index.html]
<script>
$(document).ready(function(){
$('#new_text').keydown(function(e) {
if (e.keyCode == 13) {
$('form[name="f"]').attr('action', '/new')
$('form[name="f"]').submit();
}
});
$('.card').longpress(function() {
var id = $(this).attr("task_id");
$('#id').val(id);
$('form[name="f"]').attr('action', '/completion');
$('form[name="f"]').submit();
},
1000
);
});
</script>
longpressというイベントを使います。引数に1000を指定していますが、これはミリ秒なので1秒を表しています。
これで完了処理の実装は完了です。実際に動かしてみましょう。
長押しするとstatusが1になるので、黄色になっているのがわかります。
更新処理の実装
次に更新処理を実装していきましょう。
更新処理のデータベース操作は完了処理とほぼ一緒です。
先にmain.pyに更新処理を追加します。completion()の下に次のコードを追加します。
[main.py]
@app.route('/update', methods=["POST"])
def update():
id = request.form["id"]
text = request.form["text"]
task = Task.query.filter_by(id=id).first()
task.text = text
db.session.commit()
return redirect(url_for('index'))
更新処理ではidの他に更新時のテキストも必要です。
次にindex.htmlに処理を追加します。更新処理は「タスク横の更新ボタンを押す」「タスクがテキストボックスに変わる」「テキストを入力してEnter」という流れで進みます。
まずはタスク横の更新ボタンとテキストボックスを用意しましょう。
[index.html]
<tr>
<td
class="card"
id="task_{{task.id}}"
style="{% if task.status == 1 %}background-color:#FFFF00;{% else %}background-color:#FFFFFF;{% endif %}"
task_id="{{task.id}}">{{ task.text }}
</td>
<td class="td_update_txt" id="update_txt_{{task.id}}">
<input type="text" class="txt_update" name="update_txt" task_id="{{ task.id }}" value="{{ task.text }}">
</td>
<td class="button"><button type="button" name="btn_update" value="{{ task.id }}">変更</button></td>
</tr>
update_txtがタスクの更新用テキストボックスですが、更新ボタンを押す前までは隠しておきます(既にstyle.cssでtd_update_txtクラスを非表示にしてあります)。
次に更新用テキストボックスの内容を渡すためのhidden項目を追加します。
[index.html]
</table>
<input type="hidden" name="id" id="id">
<input type="hidden" name="text" id="text">
</form>
次に変更ボタン押下時のイベントを実装します。
longpressイベントの下に次のコードを追加します。
[index.html]
$('button[name="btn_update"]').click(function() {
var id = $(this).val();
if ($('#update_txt_' + id).is(':visible')) {
$('#update_txt_' + id).hide();
$('#task_' + id).show();
$(this).text('変更');
} else {
$('#update_txt_' + id).show();
$('#task_' + id).hide();
$(this).text('戻す');
}
});
押された変更ボタンに紐づいているタスクをテキストボックスに変え、変更ボタンを戻るボタンに変えています。
戻るボタンを押したらテキストボックスが単純なタスクに戻ります。
最後に更新処理を動作させるためのイベントを登録します。
上で追加したコードのすぐ下に次のコードを記述します。
$('input[name="update_txt"]').keydown(function(e) {
if (e.keyCode == 13) {
var id = $(this).attr("task_id");
$('#id').val(id);
var text = $(this).val();
$('#text').val(text)
$('form[name="f"]').attr('action', '/update');
$('form[name="f"]').submit();
}
});
Enterを押したら更新処理が始まるようになっています。
また、更新に必要なidとtextはhidden項目を通して渡しています。
これで更新処理は実装完了です。動かしてみましょう。
変更ボタンを押すとテキストボックスになり、変更してEnterを押すと更新されています。
削除処理を実装する
最後に削除処理を実装しましょう。といってもテーブルからタスクを削除するところ以外は既出の内容ですので、さくっと進めましょう。
まずはmain.pyに削除処理を追加します。
[main.py]
@app.route('/delete', methods=["POST"])
def delete():
id = request.form["id"]
task = Task.query.filter_by(id=id).first()
db.session.delete(task)
db.session.commit()
return redirect(url_for('index'))
更新処理の時と同じようにfilter_byを使ってタスクを取得し、db.session.deleteにインスタンスを渡して削除しています。
次にindex.htmlを修正します。まずは削除ボタンを更新ボタンの後に追加します。
[index.html]
<td class="button"><button type="button" name="btn_update" value="{{ task.id }}">変更</button></td>
<td class="button"><button type="button" name="btn_delete" value="{{ task.id }}">削除</button></td>
</tr>
最後に削除ボタンを押したときのイベントを登録して完成です。
[index.html]
$('button[name="btn_delete"]').click(function() {
var id = $(this).val();
$('#id').val(id);
$('form[name="f"]').attr('action', '/delete');
$('form[name="f"]').submit();
});
では実際に動かしてみましょう。
これで全ての機能を実装しました。
完成したソースを見ると、1つの機能ごとにURLルーティングされていて、かつ関数で分かれているので非常に見易くなっていると思います。
このレベルでシンプルに記述できるのはFlaskならではなので(Djangoだともうちょっと色々と設定が大変)、簡単なアプリケーションを作りたいときに便利です。