はじめに
Pythonで書くWebアプリケーションフレームワークFlaskの練習として、簡単なToDoリストを作成しました。
概要
- ルートページ(
index.html
)にはToDoリストの一覧を表示 - それぞれの項目に詳細ページ(
show.html
) - 他に、項目編集ページ(
edit.html
)と新規項目追加フォームページ(new.html
) - 見た目はBootstrapを使いました
環境
OS | macOS mojave |
---|---|
Python | 3.7.3 |
Flask | 1.0.3 |
アプリの作成
まずはアプリの根幹になるファイルapp.py
を作成します。
モジュールのインポートなど
from flask import Flask, render_template, request, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///todo.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
db = SQLAlchemy(app)
db.create_all()
作業ディレクトリを整理
以下は完成したディレクトリ構造。
*
はbootstrapのファイルです。
その他ページの見た目をhtmlでtemplatesに、cssでstaticに作成しました。
.
├── Procfile
├── app.py
├── requirements.txt
├── static
│ ├── css
│ │ └── bootstrap.css *
│ └── js
│ ├── bootstrap.js *
│ └── jquery-3.4.1.js *
├── templates
│ ├── edit.html
│ ├── favicon.png
│ ├── index.html
│ ├── layout.html
│ ├── new.html
│ └── show.html
└── todo.db
データベースの作成
SQLAlchemyというモジュールを用いて、SQL文を書かずとも操作ができるスタイルで作ります。
まずはtouch todo.db
でデータベースファイルを作り、以下のクラスに対応するようにテーブルを作成します。
class Post(db.Model):
__tablename__ = "posts"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
date = db.Column(db.Text())
title = db.Column(db.Text())
content = db.Column(db.Text())
commit = db.Column(db.Integer)
このクラスを外部から呼び出すことでデータベースへの操作を行うことができます。
ルートページの作成
app.py
@app.route('/') # ルート(/)ページURLにリクエストが送られた時の処理
def index():
posts = Post.query.all()
return render_template("index.html", posts = posts)
- 変数postsにPostクラスの全てのクエリを代入
-
index.html
のテンプレートを呼び出してレンダリングし、表示。
index.html
続いてindex.html
を編集します。
layout.html
で共通部分を作成し、{% block content %}
と{% endblock %}
で挟み込んだ所のみを記述することで余計にコードを書かないようにします。
{% extends "layout.html" %}
{% block content %}
<div class="container">
<h1>ToDo</h1>
<table class="table table-hover table-responsive">
<tr><th>Last Update</th><th>Title</th><th></th><th></th><th></th></tr>
{% for post in posts %}
<div class="row">
<tr>
<td>{{ post.date }}</td>
{% if post.commit == 1 %}
<td><del>{{ post.title }}</del></td>
{% else %}
<td>{{ post.title }}</td>
{% endif %}
<td><a href="/show/{{ post.id }}" class="btn btn-success far fa-file-alt"> 詳細 </a></td>
<td><a href="/done/{{ post.id }}" class="btn btn-info fas fa-check-circle"> Done </a></td>
<td><a href="/undone/{{ post.id }}" class="btn btn-success fas fa-undo-alt"> Undone </a></td>
</tr>
</div>
{% endfor %}
</table>
<p>
<a href="/new" class="btn btn-primary far fa-file"> 新規 </a>
<a href="/destroy/alldone" class="btn btn-danger fas fa-trash" onclick="return confirm('完了済みの項目を全て削除します。よろしいですか?')"> 完了済みをすべて削除 </a>
</p>
</div>
{% endblock %}
- forループとtableタグを使ってクエリの一覧表示
- Postクラス変数commitが1の時完了を、0の時未完了を表し、1の時は横線を入れるよう条件分岐で記述
- 各種ボタンを作成
詳細ページの作成
app.py
/show/(Postクラスのインスタンスのid)
にアクセスした時の振る舞いをapp.py
に記述します。
@app.route('/show/<int:id>')
def show(id):
post = Post.query.get(id)
return render_template("show.html", post = post)
-
/show
の後に記述されたint型の値を変数idに格納 - この変数idとDBのidカラムのデータを変数postに代入
-
show.html
をテンプレートとしてれ呼び出し、レンダリング
show.html
{% extends "layout.html" %}
{% block content %}
<div class="container">
<div class="card bg-light">
<div class="card-header">
<h2>{{ post.title }}</h2>
</div>
<div class="card-body">
<blockquote class="blockquote mb-0">
<p>{{ post.content }}</p>
<footer class="blockquote-footer"><cite title="Source Title">(Last Update: {{ post.date }})</cite></footer>
</blockquote>
{% if post.commit == 1 %}
<div class="alert alert-success" role="alert">
完了しています
</div>
{% endif %}
</div>
</div>
<br>
<div>
<a href="/edit/{{ post.id }}" class="btn btn-warning far fa-edit"> 編集 </a>
<a href="/destroy/{{ post.id }}" class="btn btn-danger fas fa-trash-alt" onclick="return confirm('この項目を削除します。よろしいですか?')"> 削除 </a>
<a href="/done/{{ post.id }}" class="btn btn-info fas fa-check-circle"> Done </a>
<a href="/undone/{{ post.id }}" class="btn btn-success fas fa-undo-alt"> Undone </a>
</div>
<br>
<p><a href="/" class="btn btn-default">一覧に戻る</a></p>
</div>
{% endblock %}
- タイトルと、さらに詳しく情報が表示されます
- 完了しているもの(commit == 1)にはブロックが表示されます
新規追加・編集ページの作成
新規追加・編集ページのレイアウトと、そこに記述された内容をPOSTメソッドで送るロジックを組みます。
new.html
先にテンプレートを作ります。
{% extends "layout.html" %}
{% block content %}
<div class="container">
<form action="/create" method="POST">
<div class="form-group">
<label for="title">ToDo</label>
<input type="text" name="title" value="" class="form-control" placeholder="What do you do next?">
</div>
<div class="form-group">
<label for="content">備考</label>
<textarea class="form-control" name="content" cols="40" rows="10" placeholder="If you have options, write down here."></textarea>
</div>
<button type="submit" class="btn btn-primary">保存</button>
<button type="button" class="btn btn-danger" onclick="history.back()">キャンセル</button>
</form>
</div>
{% endblock %}
-
/create
へPOSTリクエストを送ります - 送り先ではname属性で指定した値で取り出せます
app.py
@app.route('/new')
def new_post():
return render_template("new.html")
このメソッドに/new
にアクセスされた時の動きを記述します。
といってもnew.html
のテンプレートをレンダリングするだけです。
@app.route('/create', methods=["POST"])
def create_post():
new_post = Post()
new_post.title = request.form["title"]
new_post.content = request.form["content"]
new_post.date = str(datetime.today().year) + "-" + str(datetime.today().month) + "-" + str(datetime.today().day)
new_post.commit = 0
db.session.add(new_post)
db.session.commit()
return redirect(url_for('.index'))
こちらで、実際にnew.html
で入力された値をDBに格納します。
- 変数new_postをPostクラスでインスタンス化
- title, contentにPOSTリクエストで送られてきた値を格納
- dateは今日の日付
- 未完了なのでcommitは0に
- session.addでデータベースへ値を追加
- session.commitでデータベースの変更を実行します
- 編集後にわざわざレンダリングせず
index.html
を表示させます。こうすることでリロードした時に同じものが登録されてしまうのを防げます
edit.html
{% extends "layout.html" %}
{% block content %}
<div class="container">
<form action="/update/{{ post.id }}" method="POST">
<div class="form-group">
<label for="title">ToDo</label><br>
<input type="text" name="title" value="{{ post.title }}" class="form-control">
</div>
<div class="form-group">
<label for="content">詳細</label><br>
<textarea name="content" cols="40" rows="10" class="form-control">{{ post.content }}</textarea>
</div>
<button type="submit" class="btn btn-primary">保存</button>
<button type="button" class="btn btn-danger" onclick="history.back()">キャンセル</button>
</form>
</div>
{% endblock %}
こちらもnew.html
と見た目は一緒ですが、一緒にレンダリングした変数postを使って現在のデータを入力ボックス内に表示してあげないといけません。
-
/update/{{ post.id }}
へリクエストを送ります
app.py
@app.route('/edit/<int:id>')
def edit_post(id):
post = Post.query.get(id)
return render_template("edit.html", post = post)
こちらは編集ページなので指定したデータも一緒にレンダリングしてあげないといけません。
@app.route('/update/<int:id>', methods=["POST"])
def update_post(id):
post = Post.query.get(id)
post.title = request.form["title"]
post.content = request.form["content"]
post.date = str(datetime.today().year) + "-" + str(datetime.today().month) + "-" + str(datetime.today().day)
db.session.commit()
return redirect(url_for('.index'))
-
/create
の時と同じような要領で記述します - 今回も編集後にわざわざレンダリングせず
index.html
を表示します
項目の完了(make item done)・削除のロジック
リクエストがdestroy, done, undoneに送られた時の動きをapp.py
に記述します。これらに関するURLにリクエストが送られた時の「動き」を記述すれば良いだけなので、とくに新たなhtmlファイルを書く必要はありません。
Done, Undone
@app.route('/done/<int:id>')
def done_post(id):
post = Post.query.get(id)
post.commit = 1
post.date = str(datetime.today().year) + "-" + str(datetime.today().month) + "-" + str(datetime.today().day)
db.session.commit()
posts = Post.query.all()
return redirect(url_for('.index'))
@app.route('/undone/<int:id>')
def undone_post(id):
post = Post.query.get(id)
post.commit = 0
post.date = str(datetime.today().year) + "-" + str(datetime.today().month) + "-" + str(datetime.today().day)
db.session.commit()
posts = Post.query.all()
return redirect(url_for('.index'))
- 変数commitが0か1かで、完了・未完了を分けているのでそれをリクエストによって変更する記述をします
削除
一件のみの削除
@app.route('/destroy/<int:id>')
def destroy(id):
post = Post.query.get(id)
db.session.delete(post)
db.session.commit()
posts = Post.query.all()
return redirect(url_for('.index'))
- 新規追加や編集の時と記述の仕方はほとんど一緒です
- session.deleteで削除してコミットします
完了済みを全削除
@app.route('/destroy/alldone')
def destroy_alldone():
posts_done = Post.query.filter_by(commit=1).all()
for i in posts_done:
db.session.delete(i)
db.session.commit()
posts = Post.query.all()
return redirect(url_for('.index'))
- ルートページにこのボタンがあります
-
Post.query.filter_by
で変数commitが1のもの(完了しているもの)をとりだし - 削除してコミットします
- 最後にルートページに飛んで終了
最後に
GitHubではHerokuに公開したURLも記述していますがユーザー管理を導入していないので完全に一人用です。
Herokuの使い方はRailsのチュートリアルドキュメントを参考にしました。