こんにちは、はやたす(@hayatasuuu )です。
この記事では「Flaskを使ったWebアプリ(Todoアプリ)の作り方」を分かりやすく紹介していきます。
Flaskの日本語情報は少ないので、この記事が間違いなく役立つはずです!
この記事を読めば、Python初心者でもFlaskを使ったWebアプリを作成できるようになりますよ。
動画版:Flask入門コース
本記事で紹介するFlask入門は、YouTubeで動画をアップしています。
テキストだと動きが分かりづらいので、初心者であれば動画を使って学習するのがオススメです。
独学に役立つLINE配信
Python独学のコツや役に立つ情報を配信するLINEアカウントを作りました!
いまなら学習に役立つ限定音声コンテンツ73本や、YouTubeで使えるソースコードをプレゼントしています。
無料で追加できるので、よかったら友達になってください!
\ 無料で使えるソースコードをプレゼント /
Flaskアプリ開発① : Webアプリを作るための準備をする
まずは、Flaskアプリを作成するための準備をしていきましょう。
主にやることは以下のとおりです。
- Flaskアプリ用のフォルダ作成
- 仮想環境の作成
- 必要なライブラリのインストール
一つずつ解説していきます。
Flaskアプリ用のフォルダ作成
今回はデスクトップにflask-todo-app
という名前でフォルダを作成しましょう。
フォルダの作成が完了したら、VSCodeを起動してflask-todo-app
を開きます。
ここで、エディター内のターミナルを開いて現在地を確認しておきましょう。
# Macの場合
$ pwd
# Windowsの場合
$ cd
僕はMacを使っていますので、以下のようになります。
$ pwd
# /Users/hayato/Desktop/flask-todo-app
これでフォルダ作成の準備が完了しました。
仮想環境の作成
次に仮想環境を作成していきます。
先ほど起動したエディタ内のターミナルで、以下のコマンドを実行しましょう。
$ virtualenv env
もしvirtualenv
が入っていなければ、以下のコマンドでインストールします。
# Anaconda Version
$ conda install virtualenv
# Python Version
$ pip install virtualenv
上記のコマンドでインストールしたら、もう一度virtualenv env
を実行してみてください。
無事にvirtualenv
を実行できたら、フォルダが作成できたか確認します。
# Macの場合
$ ls
# Windowsの場合
$ dir
コマンドを実行した結果、env
というフォルダを確認できれば大丈夫です。
$ ls
# env
これで仮想環境を作成できました。
必要なライブラリのインストール
今回のWebアプリ開発では、以下のライブラリが必要です。
- Flask : もちろん必須です。
- Flask-SQLAlchemy : データベース操作に使います。
さっそく、上記のライブラリをインストールしていきましょう!
と言いたいところですが…、まずは仮想環境を有効化する必要があります。
以下のコマンドをターミナルに入力して、仮想環境を有効化しましょう。
# Macの場合
$ source env/bin/activate
# Windowsの場合
$ env\Scripts\activate
仮想環境を有効化できると、ターミナルの見た目が少し変わります。
それでは下記コマンドをターミナルに入力して、ライブラリをインストールしましょう。
$ pip install flask flask-sqlalchemy
インストールが完了すると、「Successfully installed ライブラリ」と表示されます。
もし「WARNING: You are using pip version…」という警告が出てきたら、指示されているとおりにpip
をアップグレードしてあげましょう。
$ pip install --upgrade pip
上記を実行したところ、僕はpip
のバージョンが20.1→20.2.3に変更されました。
STEP2 : Flaskを使って、Hello Worldを表示する
まずはFlaskに慣れるべく、Hello Worldを表示するアプリを作成してみましょう。
app.pyの編集
app.py
というファイルを作成して、以下のコードを書いてみます。
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Hello World'
if __name__ == "__main__":
app.run(debug=True)
ここまで書いたら、以下のコマンドでPythonファイルを実行します。
$ python app.py
また、Pythonを実行するときは、Pythonのインタプリタが、先ほど作成した仮想環境になっているか確認しましょう。
もしenv
になっていない場合には、赤枠をクリックすれば下のような画面になります。
そこからenv
のPythonを選んであげましょう。
実際にPythonファイルを実行すると、以下のような出力になるはずです。
※途中で出ているWARNINGは、「開発モードになっているから、本番では注意してね!」って意味です。今は気にしなくて大丈夫ですよ。
正常に起動できたら、ブラウザでlocalhost:5000
にアクセスしましょう。
そうすると、Hello Worldと書かれた画面が出てくるはずです。
これで、Flaskを使ったHello Worldアプリが完成しました。
たった10行のコードでアプリを作成できるので、Flask恐るべしですね、、、。
見出しを付けて、Hello Worldを表示
Hello Worldが表示されているページのHTMLを見てみると、以下のようになっています。
これは、<body>
タグの中にHello Worldが書かれている状態ですね。
Pythonでreturn 'Hello World'
と書いていたので、当たり前といえば当たり前です。
それでは、以下のようにコードを変更すると、どうなるでしょうか。
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return '<h1>Hello World</h1>' # 変更
if __name__ == "__main__":
app.run(debug=True)
Hello Worldを<h1>
タグで囲ってあげました。これを保存して、ブラウザをもう一度確認してみたいと思います。
さっきまでとは違い、Hello Worldが見出しになって表示されるようになりました。
HTMLを確認しても、<h1>
タグで囲まれていることが分かります。
つまり、「return
の後にHTMLを書くと、ページを作成できる」ということが分かりました。
Pythonはバックスラッシュを使えば文字列の改行ができますので、return
の後にひたすら文字列を書いてページ作成することが可能です。
STEP3 : render_templateを使って、HTMLを表示する
HTMLタグを含んだ文字列を書いてあげれば、物理的にはページ作成できることが分かりました。
でもPythonファイルに、HTMLの文字列を書き連ねるのは得策ではありません。
フロント部分はHTMLファイルに書き、PythonではHTMLファイルを表示するバックエンドの処理だけ書いてあげるのが望ましい形です。
というわけで、まずはHTMLファイルを作成してあげましょう。
新しくtemplates
というフォルダを作成して、その中にindex.html
を作成します。
まずはフォルダを作成していきましょう。VSCodeで以下の部分をクリックすれば、フォルダを作成できます。
さらに、作成したフォルダを右クリックして、新しいファイルを作成しましょう。
index.html
の中身には、以下を書いてあげます。※return後に書いていたことを、HTMLファイルで書き直しただけです。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
ここまで書けたら、後はPythonで表示するロジックを書くわけですが、そのためにはrender_template
を使ってあげます。
Pythonファイルの中身を、以下のように書き換えてあげましょう。
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)
render_template
をインポートして、少し変更を加えました。
これで、もう一度Pythonファイルを実行してみましょう。※すでに実行済みなら、保存してブラウザ更新で大丈夫です
そうすると以下のように、HTMLファイルを表示する形に変更できているかと思います。
分かりづらければ、HTMLファイルの中身を少し変更してみましょう。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 以下を変更 -->
<h1>Hello World2</h1>
</body>
</html>
変更を保存して、ブラウザを更新してみます。
上のように、変更がしっかりと反映されていることが分かりますね。
これで、HTMLを表示する部分が完成しました。だんだんアプリ作成らしくなってきましたね。
STEP4 : 共通テンプレート(base.html)の作成
たとえば、「See you」と書かれたページを追加で準備したいとき、どんなHTMLを書くでしょうか。
おそらく思いつくのは、以下のようなHTMLを記述することですよね。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>See you</h1>
</body>
</html>
上記のようなコードを記述すれば、当たり前ですが、新しく「See you」が表示されるページを作成できます。
ただ、これって、少し冗長になってしまいますよね。というのも、先ほど書いていたコードと見比べてみると明らかです。
Diff Checkerというツールを使って、先ほどのコードと「See you」を表示するコードを見比べてみました。
そうすると、1行違うだけで、ほぼ同じコードであることが分かります。言い方を換えると、<h1>
タグ以外は同じ構造を持つということです。
このような場合に、Flaskでは基盤になるHTMLを準備して、そのファイルを他のHTMLに継承させることができます。
Pythonで言えば、Classに似ていますよね。基底クラスを準備して、そのクラスを継承するイメージです。
というわけで、さっそく基盤になるテンプレートを準備していきましょう。
base.html
を作成して、いまindex.html
に記述していた内容を書き写したいと思います。
要するに、以下のような状況になっています。
この状態から、以下のようにコードを書き換えます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
{% block head %}{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
変更した点は、以下のとおりです。
<head>
部分に、{% block head %}{% endblock %}
が増えた<body>
部分に、{% block body %}{% endblock %}
が増えた<h1>Hello World</h1>
を削除した
こうすることで、基盤になるHTMLファイルを作成することができました。
あとは、何も書かれていないindex.html
を編集していくだけです。以下の5行を書いてみましょう。
{% extends 'base.html' %}
{% block body %}
<h1>extended!!!</h1>
{% endblock %}
上記のように編集できたら、サーバーを立ち上げ(=python app.py
を実行し)て表示を確認してみます。
そうすると、このように「extended!!!」が表示されているはずです。
ページのHTMLを見てみると、base.html
で書いていた内容を、しっかりと反映できていることが分かります。
今後、色々なHTMLファイルを追加していきます。
そのときも同様に、base.html
を拡張してあげれば、少ないコードでページを追加できるってわけですね。
STEP5 : データベースの準備
Todoアプリに書いたタスクを保存しておくために、データベースを準備していきましょう。
今回作成するTodoアプリでは、以下のようなデータを持っておきたいと思います。
- id : 基本的に、今回に限らず必要になります。
- title : タスクに名付けるタイトルです。
- detail : タスクの詳細部分を書いていきます。
- due : タスクには期限も設けておきましょう。
上記の項目ですね。もちろん他に追加できる、もしくは変更できる点があると思います。
その辺りはご自身で好きにアレンジを加えてみてください。
また、データベースには色々な種類がありますが、今回はSQLite
を使っていきます。
※SQLiteについては、今回のスコープ外とさせてください。ネット上に、参考になる記事がたくさんあります。
それでは、データベースの準備をしていきましょう。
まずは、app.py
に以下のコードを追加します。
from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///todo.db'
db = SQLAlchemy(app)
@app.route('/')
def index():
return render_template('index.html')
if __name__ == "__main__":
app.run(debug=True)
コードを増やした部分だけピックアップすると、以下のとおりです。
from flask_sqlalchemy import SQLAlchemy
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///todo.db'
db = SQLAlchemy(app)
このflask-sqlalchemy
は、Flaskと一緒にインストールしておいたライブラリです。
flask-sqlalchemy
を使うと、Pythonオブジェクトのようにデータベースを扱うことができます。
実際に、データベースの項目定義をおこなっていきましょう。
以下のコードを、app.py
に追加します。
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(30), nullable=False)
detail = db.Column(db.String(100))
due = db.Column(db.DateTime, nullable=False)
どこに追加すれば良いのか分からない方向けに!コードの全体像は、以下のようになります。
from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///todo.db'
db = SQLAlchemy(app)
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(30), nullable=False)
detail = db.Column(db.String(100))
due = db.Column(db.DateTime, nullable=False)
@app.route('/')
def index():
return render_template('index.html')
if __name__ == "__main__":
app.run(debug=True)
上記のとおりです。追加した部分を、少し深掘りします。
データベースのURI設定
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///todo.db'
db = SQLAlchemy(app)
上記を書くことで、todo.db
という名前のデータベースを設定しています。
データベースの作成をしたときには、todo.db
という名前になるってことですね。
そのあとで、SQLAlchemy()
内にapp
を入れて、db
を生成します。
データベースの項目定義
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(30), nullable=False)
detail = db.Column(db.String(100))
due = db.Column(db.DateTime, nullable=False)
データベースで使う項目は、Pythonのクラスオブジェクトで定義できます。
それぞれの項目は、以下のように設定しました。
id
: 整数値で設定しています。また、主キーに設定しました。title
: 30字以内の文字列で設定しています。空にするのはNGです。detail
: 100字以内の文字列で設定しています。こちらは空でもOKです。due
: 日付型で設定しています。期限空はNGです。
上記ですね。
detail
に関しては、空でもOKにしました。というのも、title
だけ見ればどんなタスクか分かると思うので。
あくまで補助的な機能ということにしておきます。
ここまで書けたら、いま定義したデータベースを作成していきます。
VSCodeでターミナルを開いて、Python
の対話型シェルを起動しましょう。
まずVSCodeでターミナルを開くには、上のタブからターミナルを選択してあげます。
ターミナルを開いたら、python
と入力して対話型シェルを起動してあげます。
対話型シェルになったら、以下のコマンドを入力しましょう。
>>> from app import db
>>> db.create_all()
※>>>
は対話型シェルを表しているだけで、実際に入力する必要はありません。
ここまでのところをキャプチャしたのが以下です。
問題なく実行できていれば、todo.db
が作成されているはずです(`・ω・´)!
そうしたら、もうターミナルは使わないので閉じてしまいましょう。
以下を入力します。
>>> exit()
これでデータベースの準備ができました。
あとは「追加したタスクの内容を、データベースに保存するコード」を書けばOKですね!
STEP6 : タスク作成ページとルーティング設定
前のステップでは、タスクを保存するためのデータベースを準備しました。
今回は、タスクを追加できるようにするために、Todoの作成ページを作っていきます。
新しくcreate.html
を作成して、以下のコードを書いていきましょう。
{% extends 'base.html' %}
{% block body %}
<div class="form">
<form action="/" method="POST">
<label for="title">Title</label>
<input type="text" name="title">
<label for="detail">Detail</label>
<input type="text" name="detail">
<label for="due">Due</label>
<input type="date" name="due" required>
<input type="submit" value="Create">
</form>
</div>
{% endblock %}
これは純粋にHTMLのコードになります。
Pythonとは少し離れた内容になるので、細かい部分は割愛しますが、これで必要な項目を入力するフォームが完成します。
各値を入力してsubmit
すると、そのデータをトップページにPOSTするようになっています。
と、フォームの見た目は作成できましたが、まだこのページにアクセスできません。タスクを作成するページにアクセスするには、Python側で「ルーティング」する必要があります。
app.py
に戻って、以下のコードを追加してあげましょう。
@app.route('/create')
def create():
return render_template('create.html')
これはトップページへのアクセスを可能にしたときと同じコードですね。
今回であれば、https://〇〇.com/create
にアクセスすれば作成ページを開けるようになります。手元のパソコンで開発している間は、localhost:5000/create
ですね。
というわけで、サーバーを起動してタスク作成ページにアクセスしてみましょう。
このように、タスク作成に必要な内容を入力できるページに遷移しました。
試しに、フォームにタスクを書いてみましょう。
この状態でCreate
をクリックしてみます。
そうすると、以下のようなエラー画面になるはず。
なぜこのような画面になるのかといえば、それはフォームのアクセス先の設定が原因です。
というのも、フォーム送信の宛先はaction="/"
でトップページになっていますが、そのトップページではPOSTを受けられる状態になっていません。
言い換えると、GETしか受け取れない状態になっています。
そこで、トップページにルーティングしている関数のデコレータ部分を、少し変更してあげます。
@app.route('/') # 元々はこれだったけど
@app.route('/', methods=['GET', 'POST']) # こちらに変更
上記のように書き換えることで、トップページでPOSTメソッドを受け入れる準備ができました。
なので、もう一度/create
にアクセスして、タスクを入力して送信してみましょう。
そうすると、元の画面に戻ってこれるはず。少し現状を整理すると、以下のとおりです。
/create
ページにアクセスして、タスクを作成できるようになった- タスク作成後は、トップページ(
localhost:5000
)に繊維できるようになった - でも、入力したタスクを保存、および表示はできていない
このような状況ですよね。
なので、あとはトップページのコードを変更してあげて、「①データベースにタスクを保存②保存されているタスクを表示する」を実装すれば良さそうです。
STEP7 : タスクの保存と表示
前のステップで、以下の内容を実装すれば良いことが分かりました。
- ①データベースにタスクを保存
- ②保存されているタスクを表示する
これをトップページで実装したいので、以下のコードを編集していけばOKですね。
@app.route('/', methods=['GET', 'POST'])
def index():
return render_template('index.html')
追加のインポート
まずは、追加で以下をimportしましょう。
from flask import Flask, render_template # これだけだったけど、
from flask import Flask, render_template, request, redirect # 追加で2つimportする
これらが必要になる理由ですが、これから追加したい内容は以下でした。
- ①データベースにタスクを保存
- ②保存されているタスクを表示する
そして、これらの実装は、リクエストメソッドによって対応が変わってきますよね。
具体的に言うと、以下のような挙動になるはずです。
- POSTメソッドのとき : データベースにタスクを保存
- GETメソッドのとき : 保存されているタスクを表示する
このように、リクエスト方法に応じて実装内容を変更していくために、request
が必要になります。
またredirect
は、POSTで受け取った内容をデータベースに反映したあとに、もう一度トップページへアクセスするために使います。
コードの編集
それでは、タスクの保存と表示をするコードを書いていきましょう。
結論、以下のように編集してあげます。
from datetime import datetime
# <-----中略----->
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'GET':
posts = Post.query.all()
return render_template('index.html', posts=posts)
else:
title = request.form.get('title')
detail = request.form.get('detail')
due = request.form.get('due')
due = datetime.strptime(due, '%Y-%m-%d')
new_post = Post(title=title, detail=detail, due=due)
db.session.add(new_post)
db.session.commit()
return redirect('/')
GETとPOSTに分解して、考えてみたいと思います。
まずはGETから。
if request.method == 'GET':
posts = Post.query.all()
return render_template('index.html', posts=posts)
GETの部分でやっていることは、「データベースからすべての投稿を取り出し(Post.query.all()
)、それをトップページに渡してあげる」です。
「トップページに投稿をわたすってどういうこと…?」と思われるかもですが、これからHTMLを編集していけば分かるはずです。
とりあえずPython側だけ理解しておきましょう(`・ω・´)!
次にPOSTです。
else:
title = request.form.get('title')
detail = request.form.get('detail')
due = request.form.get('due')
due = datetime.strptime(due, '%Y-%m-%d')
new_post = Post(title=title, detail=detail, due=due)
db.session.add(new_post)
db.session.commit()
return redirect('/')
もしリクエスト方法がPOSTなら、データベースに投稿を保存してあげます。
上記でやっていることは、主に3つです。
- ① : POSTされた内容を受けとる
- ② :
Post
クラスに受け取った内容を渡す - ③ : データベースに投稿を保存する
上記がデータベースへ投稿するまでのステップです。
データベースへ保存するときは、まずadd
で内容を追加して、実際に反映するためにcommit
します。
また、途中でdue
をキャスト(型変換)している部分があります。これは、データベースの定義で日付型を指定していたけど、フォームから受け取る値が文字列だからです。よって、Python上で文字列→日付型にキャストしています。
これでPython側でやることは完了したので、あとは投稿内容を反映するためにHTMLを変更していきます。
index.html
を以下のように変更しましょう。
{% extends 'base.html' %}
{% block body %}
<h1>トップページ</h1>
{% for post in posts %}
<h2>タイトル : {{ post.title }}</h2>
<p>期限 : {{ post.due.date() }}</p>
{% endfor %}
{% endblock %}
このように書くことで、投稿内容を表示できるようになります。
{% for post in posts %}{% endfor %}
で、先ほどPythonから渡しておいた投稿たちをfor loopしています。
さらに、ループしたpost
に入っているタイトルをpost.title
、日付をpost.due.date()
のように書けば中身を取り出せます。
なお、表示したい変数部分は、{{ 変数名 }}
のように書きます。波カッコが2つ付くことに注意しましょう。
これで、サーバーを立ち上げて、/create
にアクセスして、タスクを作成したいと思います。
Create
を押してみます。
このように、今回はタスクを作成すると、トップページに遷移しつつ投稿内容が表示されるようになりました。
さらにタスクを追加すれば、ちゃんと2つとも表示されるようになります。
これでタスクを作成と表示ができるようになりました!
これだけでも、だいぶアプリケーションらしくなってきましたね。
投稿の作成(Create)に加えて、あとは以下の実装を加えたいと思います。
- 投稿の詳細を確認(Read)する
- 投稿を編集(Update)する
- 投稿を削除(Delete)する
- 見た目を整える
現状だと、終わったタスクを削除したり、または変更を加えたりする機能がありません。
また、トップページではタイトルしか見えていないので、タスクの詳細を見れるページを作成していきましょう。
STEP8 : タスクの詳細を表示するページの作成
タスクの詳細を表示するには、PythonとHTMLの両方を編集する必要があります。
まずは、Python側からコードを追加していきましょう。
Pythonファイル(app.py)の編集
タスクの詳細を表示するためにPythonでやることは、以下のコード追加だけです。
@app.route('/detail/<int:id>')
def read(id):
post = Post.query.get(id)
return render_template('detail.html', post=post)
全体を確認しておくと、以下のようになっています。
from datetime import datetime
from flask import Flask, render_template, request, redirect
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///todo.db'
db = SQLAlchemy(app)
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(30), nullable=False)
detail = db.Column(db.String(100))
due = db.Column(db.DateTime, nullable=False)
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'GET':
posts = Post.query.all()
return render_template('index.html', posts=posts)
else:
title = request.form.get('title')
detail = request.form.get('detail')
due = request.form.get('due')
due = datetime.strptime(due, '%Y-%m-%d')
new_post = Post(title=title, detail=detail, due=due)
db.session.add(new_post)
db.session.commit()
return redirect('/')
@app.route('/create')
def create():
return render_template('create.html')
@app.route('/detail/<int:id>')
def read(id):
post = Post.query.get(id)
return render_template('detail.html', post=post)
if __name__ == "__main__":
app.run(debug=True)
詳細ページにアクセスするためには、「どの投稿の詳細ページを開くのか」を指定する必要があります。
そのため、URLは/detail
ではなく、/detail/<int:id>
と指定しましょう。
「え、idってなんですか…?」と思われるかもしれないですが、これはテーブルを作成するときに定義しました。
作成したフォームにはidを指定しませんでしたが、データベースにタスクを登録するとき、自動的に番号は割り振られています。
post = Post.query.get(id)
を使うことで、該当するidの投稿内容を取得し、そのタスクをdetail.html
に渡しているということですね。
Pythonで処理する部分は、たったこれだけです。あとはHTML側に変更を加えていきましょう。
HTMLファイルの編集(index.htmlとdetail.html)
まずはdetail.html
のファイル作成をしていきます。
ファイルの作成ができたら、以下のコードを書いてあげましょう。
{% extends 'base.html' %}
{% block body %}
<h2>詳細</h2>
<h2>{{ post.title }}</h2>
<p>{{ post.detail }}</p>
<p>{{ post.due.date() }}</p>
{% endblock %}
index.html
ではfor loopを使って投稿内容を表示していましたが、今回はpost
だけです。なぜなら、Pythonから渡される投稿が、常に1つだからです。
これで詳細を表示するページの作成もできました。
あとは、index.htmlも少し編集してあげましょう。
「なぜindex.htmlの編集をするんですか…?」と聞かれるかもです。それは、各投稿ごとに詳細ページへのリンクを設置したいからです。
以下のように、詳細ページへ飛べるURLを設置してあげましょう。
{% extends 'base.html' %}
{% block body %}
<h1>トップページ</h1>
{% for post in posts %}
<h2>タイトル : {{ post.title }}</h2>
<p>期限 : {{ post.due.date() }}</p>
<a href="/detail/{{ post.id }}" role="button">Detail</a>
{% endfor %}
{% endblock %}
追加したのは、<a>
タグの部分だけです。
postに含まれているidをURL部分に書いてあげることで、それぞれの投稿に対応する詳細ページへアクセスできるようになります。
※タグの中といえど、postの中身を使うには、{{ }}
のように波カッコを2つ付けます。
これで準備が整いました。コードを保存して、まずはトップページへアクセスしましょう。
そうすると、上のように詳細ページへのリンクが作成されています。実際にアクセスしてみましょう。
こちらのように、最初に作成したタスクはlocalhost:5000/detail/1
のURLになってアクセスできるようになりました。
もう一つ作成しておいたタスクがあるので、そちらにもアクセスしてみましょう。
こちらのタスクは、localhost:5000/detail/2
でアクセスできました。
タスクによってidが異なっていて、別々の詳細ページを表示できていることが分かりますね。
あとはTodoアプリが無法地帯にならないように、編集する機能と削除する機能を付け足していきましょう。
STEP9 : タスクを削除する
タスクを追加し放題の無法地帯を避けるべく、タスクを削除する機能を付けていきましょう。
タスクの削除については、新しいページを追加せずに、トップページから行えるようにすれば良いですね。
app.py
に、以下のコードを追加していきましょう。
@app.route('/delete/<int:id>')
def delete(id):
post = Post.query.get(id)
db.session.delete(post)
db.session.commit()
return redirect('/')
タスクを追加するときは、commit()
する前にadd
をおこなっていました。
今回はタスクを削除していきますので、「delete()
を使ってあげれば良い」ということは、直感的にも分かりますね。
データベースの処理が終わったら、トップページへリダイレクトしてあげます。
app.py
のコード全体で確認すると、以下のようになりますね。
from datetime import datetime
from flask import Flask, render_template, request, redirect
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///todo.db'
db = SQLAlchemy(app)
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(30), nullable=False)
detail = db.Column(db.String(100))
due = db.Column(db.DateTime, nullable=False)
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'GET':
posts = Post.query.all()
return render_template('index.html', posts=posts)
else:
title = request.form.get('title')
detail = request.form.get('detail')
due = request.form.get('due')
due = datetime.strptime(due, '%Y-%m-%d')
new_post = Post(title=title, detail=detail, due=due)
db.session.add(new_post)
db.session.commit()
return redirect('/')
@app.route('/create')
def create():
return render_template('create.html')
@app.route('/detail/<int:id>')
def read(id):
post = Post.query.get(id)
return render_template('detail.html', post=post)
@app.route('/delete/<int:id>')
def delete(id):
post = Post.query.get(id)
db.session.delete(post)
db.session.commit()
return redirect('/')
if __name__ == "__main__":
app.run(debug=True)
あとはトップの一覧ページで、投稿を削除するためのURLを追加すればOKです。
これはdetail
とほぼ同じで大丈夫ですね。
{% extends 'base.html' %}
{% block body %}
<h1>トップページ</h1>
{% for post in posts %}
<h2>タイトル : {{ post.title }}</h2>
<p>期限 : {{ post.due.date() }}</p>
<a href="/detail/{{ post.id }}" role="button">Detail</a>
<a href="/delete/{{ post.id }}" role="button">Delete</a>
{% endfor %}
{% endblock %}
このようにdelete
を追加してあげましょう。
コードを保存したら、トップページを確認してあげます。
そうすると、上の画像のように削除ボタンが追加されました。
試しに削除するリンクをクリックしてみましょう。
僕は「スタバに行く」タスクを削除してみました。
※どうでも良いですが、この記事を書いている日は、ちゃんとスタバに行ったのでタスク完了しています(`・ω・´)!笑
というわけで、削除する機能の実装も完了しました。あとは、投稿の変更(Update)だけですね。
STEP10 : タスクを編集(アップデート)する
間違えてタスクを作成してしまったり、あとからメモとして詳細に書き加えたいときがあるかと思います。
なので、作成したタスクの内容を変更できるようにしましょう。
まずは、app.py
に以下のコードを追加してあげます。
@app.route('/update/<int:id>', methods=['GET', 'POST'])
def update(id):
post = Post.query.get(id)
if request.method == 'GET':
return render_template('update.html', post=post)
else:
post.title = request.form.get('title')
post.detail = request.form.get('detail')
post.due = datetime.strptime(request.form.get('due'), '%Y-%m-%d')
db.session.commit()
return redirect('/')
編集するページは、/update/◯
にアクセスがあったとき、以下のような挙動になることが望ましいです。
- GETメソッドのとき : 今まで書かれていた内容を表示
- POSTメソッドのとき : 変更内容を更新する
なので、Pythonファイルでも、GETとPOSTで挙動を変えてあります。
POSTするとき、今まではadd
やdelete
を使っていましたが、今回は投稿内容を編集するだけです。
よって、編集した内容をフォームから受け取ったら、そのままcommit()
してあげます。
あとはupdate.html
を作成していきましょう。フォームを実装するので、コードはcreate.html
とほとんど変わりません。
{% extends 'base.html' %}
{% block body %}
<h2>編集</h2>
<div class="form">
<form action="/update/{{ post.id }}" method='POST'>
<label for="title">Title</label>
<input type="text" name="title" value={{ post.title }}>
<label for="detail">Detail</label>
<input type="text" name="detail" value={{ post.detail }}>
<label for="due">Due</label>
<input type="date" name="due" value={{ post.due.date() }} required>
<input type="submit" value="Change">
</form>
</div>
{% endblock %}
create.html
との違いは、action
で飛ばすURLですね。
このページから投稿を編集したとき、以下のような動きになります。
- ① : フォームでタスクを編集する
- ② : 編集した内容がPythonの
update
関数にいく - ③ :
update
関数のPOSTメソッド側の処理(=更新)が行われる
index.html
に、投稿を編集するためのリンクを設置して、実際にタスクをアップデートしてみましょう。
index.html
に追加する中身は、detail
とdelete
同様です。
{% extends 'base.html' %}
{% block body %}
<h1>トップページ</h1>
{% for post in posts %}
<h2>タイトル : {{ post.title }}</h2>
<p>期限 : {{ post.due.date() }}</p>
<a href="/detail/{{ post.id }}" role="button">Detail</a>
<a href="/update/{{ post.id }}" role="button">Update</a>
<a href="/delete/{{ post.id }}" role="button">Delete</a>
{% endfor %}
{% endblock %}
これでコードを保存して、ブラウザにアクセスします。
そうすると新しくupdate
ボタンが作成されました。クリックすると、以下のように、元の投稿内容がフォーム内に入っています。
あとはこの中身を編集してみましょう。僕は簡単のため「プログラミング学習をする2」に変更してみました。
トップページに戻るようredirect
しておいたので、編集した内容が反映された状態で、タスク一覧が見れるかと思います。
しっかりと変更内容が反映された形で、タスクを見れるようになりましたね。
これで、Todoアプリの基本的な部分が完成しました。
あとは細かい調整をしてあげて、フロント部分を整えていきましょう。
STEP11 : トップページの細かい調整
いまのアプリだと、 手打ちで/create
を検索しないとタスクを作成できません。
これだと不便極まりないので、トップページからタスクを作成する画面に遷移できるようにしましょう。
また、現状タスクの表示方法が”作成日順”になっています。ただ、タスクは”締切が近い順”にした方が使いやすいですよね。
なので、これら2点の編集を付け加えていきます。
タスクを作成するボタンの設置
タスクを作成するボタンの追加はカンタンで、「詳細・編集・削除」で使っていた<a>
タグを少し編集するだけでOKです。
タスクを表示しているfor loopの前に、作成ボタンを設置しましょう。
{% extends 'base.html' %}
{% block body %}
<h1>トップページ</h1>
<a href="/create" role="button">CREATE NEW TASK</a>
{% for post in posts %}
<h2>タイトル : {{ post.title }}</h2>
<p>期限 : {{ post.due.date() }}</p>
<a href="/detail/{{ post.id }}" role="button">Detail</a>
<a href="/update/{{ post.id }}" role="button">Update</a>
<a href="/delete/{{ post.id }}" role="button">Delete</a>
{% endfor %}
{% endblock %}
これでOKですね。
あとは、コードを保存してブラウザを確認してみます。
こんな感じで、タスクを作成するボタンが設置できています。ちゃんとアクセスできるか確認しましょう。作成ボタンをクリックします。
しっかりタスク作成する画面に遷移しました。これで、前よりも便利なTodoアプリになりましたね。
タスクを締切が近い順に並べる
少しタスクを追加してみました。タスクの期限を見ていただきたいのですが、現状だと並び順がバラバラで非常に使いにくいです。
これを締切が近い順に変更していきましょう。
今回編集するコードは、Python側になります。
これも編集はカンタンで、以下の1行だけコードを変更します。
posts = Post.query.all() # このコードを
posts = Post.query.order_by(Post.due).all() # こちらに変更
上記のように変更することで、締切が近い順に変更できました。
実際にコードを保存して、アプリのトップページを見てみましょう。
このように、タスクの締切が近い順に変更できました。これで、締切が近いタスクから消化できますね!
STEP12 : BootStrapを使って、見た目を整える
現状のTodoアプリだと、まったく持って見た目がイケていません。笑
ただ、自分で一からCSSを当てていくのも面倒ですので、今回はBootStrapを使ってサクッといい感じのアプリを作っていきましょう。
BootStrapのダウンロード
まずはBootStrapの公式ページから、必要なファイルをダウンロードしてきます。
»参考 : Bootstrap – 世界で最も人気のあるフロントエンドのコンポーネントライブラリ
トップページから、ダウンロードボタンを押して、次の画面に進みます。
今回使うのは、コンパイル済みのファイルですので、以下の赤枠をクリックしてファイルをダウンロードでOKです。
そうすると、以下のようなzipファイルをダウンロードできます。こちらを解凍しましょう。
解凍したフォルダの中身を見てみると、css
とjs
が入っています。今回はCSSだけ当てていくので、js
は不要です。
中身の確認ができたところで、自分たちが作っていたFlaskアプリのフォルダに戻りましょう。
現状は上の画像のようになっていますので、ここでstatic
フォルダを作成します。
作成できたら、先ほどダウンロードしてきたBootStrapのcss
だけFlaskアプリ側に移しておきましょう。
これでBootStrapのダウンロードが完了しました。
CSSを使う準備
FlaskでCSSを使うには、Python側とHTML側で少し準備が必要です。
先にPython側から追加部分を書いていきましょう。といっても、やることはシンプルで、importするものを増やすだけです。
from flask import Flask, render_template, request, redirect # ここの部分で
from flask import Flask, render_template, request, redirect, url_for # url_forを追加する
url_for
はHTMLファイルでCSSを読み込むときに使います。
というわけで、あとはHTMLの編集ですが、こちらは通常のCSS読み込みとあまり変わりません。
base.html
の<head>
部分に、以下を追記しましょう。
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
これでBootStrapのCSSを使えるようになります。変更したコードを保存して、トップページを確認してみましょう。
こちらは変更前です。
それに対してこちらが変更後。
このように、BootStrapを読み込めていると、見た目が少し変わった状態になります。
BootStrapは、すでに準備されているclass
を指定することで使用できます。
※ここでBootStrapについて詳しく書いていると、それだけで分量が大変なことになりますので、割愛させていただきますm(_ _)m
共通部分(base.html)の編集
まずは、どのページでも共通で使う部分をbase.html
に書いていきます。
今回はナビゲーションだけ全てのページで共通にしたいので、以下のようなコードに変更しましょう。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
<title>Todoアプリ</title>
{% block head %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-light bg-light p-3">
<a class="navbar-brand pl-3" href="/" style="font-size: 2rem;">Todoアプリ</a>
</nav>
{% block body %}{% endblock %}
</body>
</html>
»参考 : Navbar – Bootstrap 4.5 – 日本語リファレンス
公式ドキュメントに載っているコードを、ほぼそのままコピペで作成しています(`・ω・´)!笑
変更したコードを保存して、ブラウザを確認してみましょう。
画面上部分に、ナビゲーションが追加されていますね。
CREATE NEW TASK
をクリックして画面を遷移してみても、ナビゲーションが付いていることが分かります。
トップページ(index.html)の編集
次はトップページの編集をしていきます。
トップページでは、単純にCSSを当てるだけでなく、「タスクの期限が切れていたら、注意表示する」機能を付け加えたいと思います。
先にHTMLだけ書いておくと、以下のとおりです。
{% extends 'base.html' %}
{% block body %}
<div class="container">
<a class="btn btn-info btn-lg m-5" href="/create" role="button">CREATE NEW TASK</a>
{% for post in posts %}
<div class="card w-50 mb-3" style="margin: auto;">
<div class="card-body">
{% if post.due.date() < today %}
<div class="alert alert-warning" role="alert">
期限切れです!
</div>
{% endif %}
<h2 class="card-title">{{ post.title }}</h2>
<p>期限:{{ post.due.date() }}</p>
<a class="btn btn-secondary btn-sm" href="/detail/{{ post.id }}" role="button">Detail</a>
<a class="btn btn-success btn-sm" href="/update/{{ post.id }}" role="button">Update</a>
<a class="btn btn-danger btn-sm" href="/delete/{{ post.id }}" role="button">Delete</a>
</div>
</div>
{% endfor %}
</div>
{% endblock %}
上記のコードで使っているBootStrapは、以下の3つですね。
- ボタン : Buttons – Bootstrap 4.5 – 日本語リファレンス
- カード : Cards – Bootstrap 4.5 – 日本語リファレンス
- アラート : Alerts – Bootstrap 4.5 – 日本語リファレンス
前まではfor
文だけ使っていましたが、今回はif
文を使っています。
もしタスクの期限が切れていたら、「期限切れです!」というアラートを出力するようにしました。
「あれ、if文に入っているtoday
って何だろう…」と疑問に思ったかもしれません。
それは今からPythonのコードを編集して追加していきます。
トップページを表示するときにHTML側に渡していたのはposts
だけでしたが、新しくtoday
も渡してあげましょう。
return render_template('index.html', posts=posts) # この部分を
return render_template('index.html', posts=posts, today=date.today()) # こちらに変更する
また、date
を使えるようにするために、importも追加してあげます。
from datetime import datetime # この部分を
from datetime import datetime, date # こちらに変更する
これで変更が完了しました。
コードを保存して、昨日までの日付で、タスクを作成してみてください。
僕の画面では、このようになりました。黄色い注意書きで、期限が切れていることを表示できていますね。
タスク作成(create.html)ページの編集
次はタスクを作成する画面の編集をしましょう。
使うコードは、以下のとおりです。
{% extends 'base.html' %}
{% block body %}
<h2 class="m-5">新規追加</h2>
<form class="m-5" action="/" method='POST'>
<div class="form-group pb-3">
<label for="title">Title : </label>
<input type="text" class="form-control" name="title" aria-describedby="title-help">
<small id="title-help" class="form-text text-muted">To doを入力してください。</small>
</div>
<div class="form-group pb-3">
<label for="detail">Detail : </label>
<input type="text" class="form-control" name="detail">
</div>
<div class="form-group pb-3">
<label for="due">Due : </label>
<input type="date" name="due" required>
</div>
<a class="btn btn-outline-primary" href="/" role="button">Return</a>
<button type="submit" class="btn btn-primary">Create</button>
</form>
{% endblock %}
»参考 : Forms – Bootstrap 4.5 – 日本語リファレンス
BootStrapページに載っているコードを、ほとんどコピペして作っています。笑
タスクを編集するだけではなく、戻れるボタンも一緒に付け加えてみました。
コードを保存したら、タスクを作成する画面を見てみましょう。
そうすると、上のような画面になります。いい感じの見た目になりましたね!(BootStrapのおかげで!)
タスク編集(update.html)画面の編集
タスクを編集する画面は、作成ページとほとんど変わりません。
{% extends 'base.html' %}
{% block body %}
<h2 class="m-5">編集</h2>
<form class="m-5" action="/update/{{ post.id }}" method="POST">
<div class="form-group pb-3">
<label for="title">Title : </label>
<input type="text" class="form-control" name="title" aria-describedby="title-help" value={{ post.title }}>
</div>
<div class="form-group pb-3">
<label for="detail">Detail : </label>
<input type="text" class="form-control" name="detail" value={{ post.detail }}>
</div>
<div class="form-group pb-3">
<label for="due">Due : </label>
<input type="date" name="due" value={{ post.due.date() }} required>
</div>
<a class="btn btn-outline-primary" href="/" role="button">Return</a>
<button type="submit" class="btn btn-primary">Change</button>
</form>
{% endblock %}
こんな感じです。
コードを保存して、見た目を確認してみましょう。
このように、タスク編集画面もキレイになりましたね!(BootStrapのおかげで!)
詳細画面(detail.html)の編集
タスクの詳細を確認する画面は、以下のコードで作成しました。
{% extends 'base.html' %}
{% block body %}
<h2 class="m-5" style="text-align: center;">詳細</h2>
<div class="card w-50 mb-3" style="margin: auto;">
<div class="card-body">
<h2 class="card-title">{{ post.title }} <span class="ml-3"
style="font-size: 0.5em;">期限:{{ post.due.date() }}</span></h2>
<p>{{ post.detail }}</p>
<div class="pt-2">
<a class="btn btn-outline-success" href="/" role="button">Return</a>
<a class="btn btn-success" href="/update/{{ post.id }}" role="button">Update</a>
</div>
</div>
</div>
{% endblock %}
投稿の詳細を確認できるのはもちろん、そのまま編集ページに遷移できるようにボタンを追加してみました。
コードを保存して、ブラウザを確認してみましょう。
こんな感じです。詳細確認ページもキレイな見た目になりましたね!(BootStrapのおかげで!)
これで見た目の変更も完了しました。
STEP13 : アプリをデプロイする
あとは、作成したアプリを公開するだけです。アプリの公開は、Herokuを使っていきます。
アプリ公開に必要なものは、以下のとおりです。
- Heroku CLI
- Procfile
- requirements.txt
順を追って、準備を進めていきましょう。
Heroku CLIをダウンロード
まずはHeroku CLIをダウンロード&インストールします。
画面に従って、ポチポチするだけで大丈夫です。おそらく、ほとんどの人は64bitだと思われます(`・ω・´)!
requirements.txtの作成
アプリを公開するには、requirements.txt
を準備する必要があります。
まずは、アプリ公開に必要なgunicorn
をインストールしていきましょう。
$ pip install gunicorn
インストールが完了したら、いま入っているライブラリ一覧を確認します。pip freeze
を入力していきましょう。
いまインストールしたgunicorn
も一緒に入っていますね。
そうしたら、これをrequirements.txt
に書き起こします。
といっても、以下のコマンドを入力すればOKです!
$ pip freeze > requirements.txt
これで、アプリを公開するときに必要なライブラリ一覧を作成できました。
Procfileの作成
こちらはHerokuにデプロイするとき、必ず必要になります。
以下をコピペして、Procfileを作成しましょう。
web: gunicorn app:app --log-file=-
これだけでOKです!
gitリポジトリの準備
まずはapp.py
でデバッグモードにしていたので、これを外します。
# app.run(debug=True)
app.run() # こちらに変更
そうしたら必要なファイルが揃ったので、gitリポジトリを準備していきます。
VSCodeで以下のコードを実行していきましょう。
$ git init
$ git add .
$ git commit -m "first commit"
※Gitについては、今回のスコープ外とさせてください。
Herokuにログインして、サーバーの作成
VSCodeのターミナルで、以下のコマンドを入力してHerokuにログインします。
$ heroku login
入力すると、ブラウザが立ち上がって以下の画面に遷移します。
Log In
をクリックして、ログインを完了させましょう。以下の画面になれば成功です。
次にVSCodeのターミナルに戻って、以下のコマンドを実行します。
$ heroku create <アプリ名>
アプリ名の部分は、そのままURLに使われます。ゆえに他のアプリと重複していると、その名前は使えませんので、ユニークな名前にしてください!
僕はflask-todo-app-techdiary
にしました。成功すると、以下の画面になります。
アプリを公開する
あとは、アプリを公開するだけです。
いったん以下のコマンドを実行して、リモートリポジトリの設定が完了しているか確認してみましょう。
$ git remote -v
以下のようになっていれば、アプリを公開する準備が整っています。
あとはアプリの公開だけです。下記のコマンドを実行しましょう。
$ git push heroku master
※少し時間がかかるので、焦らずお待ちください(`・ω・´)!
pushが完了すると、以下のような画面になるかと思います。
そうしたら、heroku open
をターミナルで実行するか、以下のURLにアクセスしてアプリが公開できているか確認しましょう。
https://<自分のアプリ名>.herokuapp.com/
僕の場合は、なんのエラーもなくアプリを公開することができました!
おそらく、はじめてアプリを公開するときは、上手くいかないことがあると思います。
そんなときは根気強く、色々調べながら頑張っていきましょう(`・ω・´)!
いきなり上手くいくことなんて、滅多にありませんから!笑
まとめ
というわけで、Flaskを使ったTodoアプリの開発方法を紹介しました。
このチュートリアルで学んだことを活かして、自分オリジナルのアプリを開発してみてください。
また、本格的にWebアプリ開発を学ぶなら、FlaskよりDjangoを学びましょう。
Djangoは国内・海外問わず採用する企業が多いので、市場価値の高いスキルを習得できますよ!
↓↓↓画像をクリックして「期間限定30%オフ」で購入する↓↓↓
参考 : 【知識ゼロからデプロイまで】 Django基礎マスターコース〜PythonでWebアプリを開発できるようになろう〜