▼動画版はこちらをご覧ください。
はじめに
こちらの記事では、PythonのWebアプリケーションフレームワークであるFlaskを使用したWebアプリケーションを開発(ブログアプリ)し、最後には一般に公開できるようアプリケーションのデプロイも行います。
▼成果物イメージ
前提条件
こんな人におすすめ
- PythonでWebアプリを開発してみたい!
- Flaskの知識は全くないので、1から学びたい!
- Flaskの基礎は分かるけど、サービスの公開方法が知りたい
- SQLite以外のDBでHerokuへデプロイしたい
最低限事前に必要な知識
- Pythonの基礎(環境構築/クラスや関数の知識/ライブラリなど)
- HTML/CSS/Bootstrapの基礎
- Gitの基礎(add/commit/pushなどのコマンドが使用できる)
無くても大丈夫な知識
- Linuxの深い知識
- インフラに関する知識
各種バージョンについて
こちらのチュートリアルでは、各種下記バージョン等を使用します。
基本的には仮想環境を使用すれば問題ありませんが、バージョン等が異なる場合、記事記載の内容と異なる動作をすることがありますのでご注意ください。
- Python 3.9.10
- ターミナル Zsh
- MacOS Venture
- CPU Apple M2 Pro
このあとインストールする手順も1つ1つご紹介しますが、必要なライブラリは以下となります。
python = "^3.9"
flask = "2.3.3"
flask-sqlalchemy = "3.0.5"
pytz = "^2024.1"
flask-login = "^0.6.3"
flask-bootstrap = "^3.3.7.1"
gunicorn = "^21.2.0"
psycopg = {extras = ["binary"], version = "^3.1.18"}
pyproject.toml環境構築について
こちらの記事では、Pythonのバージョン管理をPyenv、パッケージ管理をPoetryで行なっていきます。
Anacondaやvenvなどその他の環境をご使用の場合でも、Pythonとライブラリのバージョンが同じものを使用いただければ基本的には記事通りに問題なく進めることができます。
▼Pyenv×Poetryの環境構築方法については下記の記事で紹介しています
チュートリアルの構成
それでは、ブログアプリを制作するためのチュートリアルをはじめていきます。
チュートリアルは全部で5つの章で作成しています。
- 第一章 FlaskとHTML
- 第二章 Flaskとデータベース
- 第三章 Flaskとログイン
- 第四章 HTMLの装飾
- 第五章 Herokuへのデプロイ
それぞれ内容を簡単に説明します。
第一章 FlaskとHTML – Flaskの最もベースとなるコードを使用してHTMLを作成します
第二章 Flaskとデータベース – Flaskとデータベース(DB)を接続し、CRUD操作を行います
第三章 Flaskとログイン – Flaskにログイン機能を実装します
第四章 アプリの装飾 – CSSやBootstrapを追加し、画面を装飾します
第五章 Herokuへのデプロイ – 開発したアプリをHerokuを使用して外部公開します
第一章 FlaskとHTML
Flaskのインストール
それでは早速Flaskを使用するためにインストールを行なっていきます。
別の章でも紹介した通り、こちらの記事ではパッケージ管理にPoetryを使用します。
pipやcondaなどPoetry以外のインストールでも問題なく動作しますので、ライブラリとライブラリのバージョンだけ間違えないようにだけ注意しておきましょう。
▼poetryを使用したFlaskのインストール
poetry add flask=2.3.3
Zsh▼pipを使用する場合(以後pipでのインストール方法は割愛します)
pip3 install flask=2.3.3
Zsh基礎コードとHTML
Flaskのライブラリがインストールできましたので、いきなりですがFlaskを動かしてみます。
Flaskの基礎コードと実行
「myapp.py」という名前のファイルを作成し、下記のコードを貼り付けましょう。
▼myapp.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello_world():
return "<p>Hello, World!</p>"
Python参考: https://flask.palletsprojects.com/en/2.3.x/quickstart/
コードが保存できたら、ターミナルで下記のコマンドを実行します。
▼Flaskをデバッグモードで起動するコマンド
flask --app myapp run --debug
Pythonすると、ターミナルに下記のような文字が表示されます。
赤枠部分にある「http://127.0.0.1:5000」の部分をコピーして、ブラウザのURL欄に貼り付けてみましょう。
すると、下記のように「Hello World」と書かれた画面が表示されます。
たったこれだけですが、すでにFlaskを使用したWebアプリケーションの基本的な実行ができています。
基礎コードと実行方法の解説
それでは今実行した内容を解説していきます。
コードの内容について
まずは、Flaskをインポートして、Flaskのインスタンス化を行います。
インスタンス化されたFlaskが格納される、appを使用することでFlaskのあらゆる設定や機能を今後使用していくことになります。
また、Flaskはフレームワークなので、「中身がどうなっている?」よりも、まずはツールとして捉え、使い方を覚えることが重要です。
from flask import Flask
app = Flask(__name__)
Python次に、ルーティングと処理、そして画面に表示する内容の指定です。
@app.route("/")
def hello_world():
return "<p>Hello, World!</p>"
Python処理の流れは以下の通りです。
- @app.route(“”)の””の間に、ルーティング(URL)を設定
- ユーザがそのURLにアクセスしたときの処理を関数の内部に記載
- 処理を終えた後、どのような画面を表示するかをreturnの後に記載
という流れになっています。
実行方法について
Flaskではアプリケーションサーバーを起動する際に、以下のコマンドを実行します。
▼Flaskサーバー起動コマンド
flask --app app_name run
Zshapp_nameの部分をFlaskのインスタンス(今回でいうapp)が存在するファイルの名前(今回でいうmyapp)に変更することでサーバーを起動することができます。
この起動方法の場合、開発中にコードの変更をした際に毎回サーバーの起動と停止を繰り返す必要が出てきます。
そのため、コードの修正を自動的にサーバへ反映してくれる開発者モード(=debugモード)をコマンドの末尾に「–debug」と付けることで有効化することができます。
▼Flaskサーバー起動コマンド(debugモード)
flask --app app_name run --debug
ZshJinjaで動的なHTMLの作成
簡単なHTMLファイルが表示できるところまで一瞬でできてしまいました。
しかし、現状ではWebアプリケーションではなくWebサイトの状態で、最初に作られたHTMLファイルをただURLにアクセスしたユーザに返しているだけ(=静的)です。
そこで、次はHTMLの中にPythonで処理した変数を表示をできるようにしていきます。(動的)
Jinjaについて
ここからPythonで処理したデータを下記のHTML部分に入れていきたいのですが、
@app.route("/")
def hello_world():
return "<p>Hello, World!</p>"
Zshこのままウェブサイトに表示するHTMLをどんどん書いていき、ページ数も増えていったらPythonのファイルがぐっちゃぐちゃになるのがイメージできますか?
そこで、HTMLファイルを別のディレクトリに保存して整理し、そのHTMLファイルに対してPythonの変数を渡してあげるような構成を取れるようにしていきます。
そのために「Jinja」というライブラリを使用していきます。
また、JinjaというライブラリはFlaskライブラリに最初から備わっているため、追加でインストールを行う必要はありません。
HTMLをレンダリングする
「レンダリング」ってなんだろう…?ってなりますよね。
直訳だと分かりにくいので、「レンダリング=変換する」と覚えておけばOKです。
そして「HTMLをレンダリングする」ということは、「HTMLをAからBへ変換する」という意味になるのですが、ここからJinjaを使用して「静的HTMLから動的HTMLへ変換」していきます。
HTMLのレンダリングもライブラリで簡単にできます。
まずは、プロジェクト配下に「templates」ファイルを作成します。
ファイルが作成できたら、その下に「base.html」というタイトルでhtmlファイルを作成します。
このbase.htmlをこれからレンダリングしていきます。
それではHTMLの基本テンプレートに「Hello, レンダリング!」をbase.htmlに書いてみましょう。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
</head>
<body>
<p>Hello, レンダリング!</p>
</body>
</html>
HTMLHTMLが書けたら、myapp.pyに戻って、今作成したhtmlをレンダリングするコードを書きます。
▼myapp.py
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/")
def hello_world():
return render_template("base.html")
Pythonレンダリングには、flaskライブラリから「render_template」というメソッドをインポートし、その関数に対して「template」フォルダ内に作成した「base.html」のファイル名を入力します。
この状態だとまだレンダリング(=変換)を行ってはいませんが、一旦この状態で実行してみましょう。
▼先ほどと同じ実行コマンド
flask --app app_name run --debug
Zshすると、このようにtemplatesフォルダにあるbase.htmlを表示することができました。
動的HTMLの生成
先ほども言いましたが、「今の状態だとまだレンダリング(=変換)を行ってはいません」。
ここから、Pythonで処理した変数をHTMLに渡して初めてHTMLをレンダリング(=変換)したことになります。
それぞれの変数内の型、合計4種類に対応して、どのようにHTML内でJinjaを使って表現するか紹介していきます。
- 文字列や数値
- リスト
- 辞書型
- 辞書型の入ったリスト
①文字列や数値
それでは早速、Pythonから文字列が入った変数をHTMLに渡してみましょう。
↓のようにブラウザに「Hello World!」が表示できるようコードを書き換えていきます。
▼変数が表示されたブラウザ
Pythonファイル側で変数をHTMLに渡すには、render_templateメソッドの第二引数に変数名を決めて値を渡すだけです。
▼myapp.py
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/")
def hello_world():
return render_template("base.html", var="Hello World!")
PythonHTML側はJinjaライブラリを使用した少し特殊なことをします。
▼base.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>
<p>{{ var }}</p>
</body>
</html>
Python変数を表示している部分がわかりましたか?
このように、pythonからきた変数を表示させるためには、変数名を{{}}で囲ってあげます。
{{ 変数名 }}
HTMLただのHTMLではこのような書き方をすることはないので、違和感を覚える方もいると思います。
ただこのHTMLはただのHTMLではなく、pythonによって以下のように処理されます。
- pythonが指定されたHTMLファイルを読み込む
- HTML内の{{}}部分をPythonが認識する
- 認識した処理内容によってPythonが処理を実行する
- 処理内容をHTMLに反映しHTMLとして正しく書き換える
- 書き換えたHTMLをブラウザに渡す
このような処理となっているため、HTMLでは使用できない{{}}という記法をrender_templateによって扱えるようにしています。
こちらの例では文字列を表示させましたが、数値でも同じ処理で表示することができます。
②リスト
次にリストが変数の値に入っていた際の処理をみていきます。
↓のように、果物のデータが入ったリストを想定して作成していきます。
▼リストの中身が表示されたブラウザ
まずは、Pythonを編集していきます。先ほどの変数がリストになっただけですね。
▼myapp.py
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/")
def hello_world():
fruits=['りんご', 'バナナ', 'ぶどう', 'みかん', 'いちご']
return render_template("base.html", fruits=fruits)
Python次にHTMLファイルを見てみましょう。
▼base.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>
<ul>
{% for fruit in fruits %}
<li>{{ fruit }}</li>
{% endfor %}
</ul>
</body>
</html>
HTMLさっきの{{}}に加えて、関数らしきものがありますね。
▼base.htmlのbody
<ul>
{% for fruit in fruits %}
<li>{{ fruit }}</li>
{% endfor %}
</ul>
HTMLそうです、Jinjaを使うとHTML内にfor文やif文といったループや制御文を書くことができるのです。
for文に関しては、以下のように「{% %}の間にpythonのfor文」(:は不要)を記載し、for文の終わりには「{% endfor %}」を必ず書きます。
▼Jinjaのfor文の書き方
{% for value in python_list %}
処理を書く
{% endfor %}
HTML③辞書型
次に辞書型が変数の値に入っていた際の処理をみていきます。
↓のように、辞書内にブログ記事のタイトル、投稿日、記事の内容が入っていることを想定して作成していきます。
▼辞書の中身が表示されたブラウザ
それでは、編集したPythonを見ていきましょう。こちらも先ほどの変数が辞書になっただけですね。
▼myapp.py
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/")
def hello_world():
post = {
'title': '記事のタイトル',
'body': '記事の内容',
'created_at': '2020-01-01'
}
return render_template("base.html", post=post)
Python次にHTMLファイルを見てみましょう。
▼base.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>
<article>
<h2>{{ post.title }}</h2>
<p>{{ post.created_at }}</p>
<p>{{ post.body }}</p>
</article>
</body>
</html>
HTML{{}}で囲む点は文字列や数値と同じですが、辞書型のキーを指定して値を取り出す方法が通常のPythonの記法とは異なる点がポイントです。
▼辞書型の中の値の取り出し方
<h2>{{ post.title }}</h2>
<p>{{ post.created_at }}</p>
<p>{{ post.body }}</p>
HTML④辞書型の入ったリスト
それでは②リストと③辞書型で学んだことを組み合わせて④辞書型の入ったリストを↓の成果物を参考に表示するコードを書いてみましょう。
▼辞書型の入ったリストを表示するブラウザ
自分で考えてコードを書けましたか?以下答え合わせとなります。
▼myapp.py
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/")
def hello_world():
posts = [
{
'title': '記事のタイトル',
'body': '記事の内容',
'created_at': '2020-01-01'
},
{
'title': '記事のタイトル2',
'body': '記事の内容2',
'created_at': '2020-01-02'
},
{
'title': '記事のタイトル3',
'body': '記事の内容3',
'created_at': '2020-01-03'
}
]
return render_template("base.html", posts=posts)
Python▼base.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>
{% for post in posts %}
<article>
<h2>{{ post.title }}</h2>
<p>{{ post.created_at }}</p>
<p>{{ post.body }}</p>
</article>
{% endfor %}
</body>
</html>
HTML正解を見ずに、コードを書くことはできましたか?
最終ゴールとなるブログの作成には、①~④までの知識が必要となるので、理解ができるまでコードを作り替えたりして遊びながら学んでおきましょう。
URLの値の取得
レンダリングについて基本的な動きがわかったところで、次にURLの値をFlaskで取得して、その値を元に表示する内容を変更してみましょう。
これができることで、ブログの場合ユーザーからリクエストのあった記事番号に応じて記事を表示したりすることができるようになります。
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/<int:number>")
def hello_world(number):
posts = [
{
'title': '記事のタイトル',
'body': '記事の内容',
'created_at': '2020-01-01'
},
{
'title': '記事のタイトル2',
'body': '記事の内容2',
'created_at': '2020-01-02'
},
{
'title': '記事のタイトル3',
'body': '記事の内容3',
'created_at': '2020-01-03'
}
]
posts = posts[number]
return render_template("base.html", posts=posts)
PythonHTMLでDRY
Jinjaの基礎が学べたところで、本章最後に、「HTMLにDRYの原則を適用」を行なっていきます。
DRYとは、「Don’t Repeat Yourself」の略で、「同じことを繰り返すな」という意味になります。
HTMLにはヘッダー、フッター、HTMLのボイラーテンプレート、ナビゲーションバーなど、複数のページにまたがって同じコードを使用することが非常に多くあります。
そして、HTMLはプログラミング言語ではないため本来はDRYを実践することができません。
しかし、render_templateというPythonの処理を一度通しているため、DRYの原則を使用することができます。
それではその方法を以下の順でみていきます。
- 繰り返す部分と繰り返さない部分を分ける
- 繰り返す部分と繰り返さない部分を合わせる
繰り返す部分と繰り返さない部分を分ける
それでは下記のHTMLファイルを元に、繰り返す部分と繰り返さない部分に分けていきましょう。
▼編集前のbase.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>
{% for post in posts %}
<article>
<h2>{{ post.title }}</h2>
<p>{{ post.created_at }}</p>
<p>{{ post.body }}</p>
</article>
{% endfor %}
</body>
</html>
HTML新しくhtmlファイルを作成し、このbase.htmlの繰り返さない部分(<body>と</body>の間の部分)を新しいファイルに移動させます。
新しいファイルは「admin.html」と名付けて作成していきます。
▼新規作成したHTML(admin.html)
{% for post in posts %}
<article>
<h2>{{ post.title }}</h2>
<p>{{ post.created_at }}</p>
<p>{{ post.body }}</p>
</article>
{% endfor %}
HTML▼編集後のbase.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>
</body>
</html>
HTMLこれでとりあえず、繰り返す部分(=base.html)と繰り返さない部分(=admin.html)に分けることができました。
▼現在のファイル構成
繰り返す部分と繰り返さない部分を合わせる
現状のままだと、admin.htmlを開いてもbase.htmlの内容が引き継がれていません。
jinjaの機能を使用して、base.htmlの内容をadmin.htmlに引き継ぐ(継承する)設定を書いていきましょう。
まず、継承元の方には、継承しない部分に{% block content%}と{% endblock %}を記述します。
▼base.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>
{% block content %}
{% endblock %}
</body>
</html>
HTML次に継承先の方には{% extends ‘継承元のhtml名’ %}とファイルの先頭に記述し、
繰り返さない部分を{% block content %}と{% endblock %}でハンバーガーのように挟み込んであげます。
▼admin.html(継承先)
{% extends 'base.html' %}
{% block content %}
{% for post in posts %}
<article>
<h2>{{ post.title }}</h2>
<p>{{ post.created_at }}</p>
<p>{{ post.body }}</p>
</article>
{% endfor %}
{% endblock %}
HTMLこれで、繰り返す書くHTMLと繰り返さないHTML部分の結合(継承)をすることができました。
最後に、第二章からのブログ作成のことを考慮し、myapp.pyを編集して、「/admin」にアクセスすると、継承先のHTML(admin.html)が表示されるように設定しておきましょう。
▼myapp.py
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/admin")
def hello_world():
posts = [
{
'title': '記事のタイトル',
'body': '記事の内容',
'created_at': '2020-01-01'
},
{
'title': '記事のタイトル2',
'body': '記事の内容2',
'created_at': '2020-01-02'
},
{
'title': '記事のタイトル3',
'body': '記事の内容3',
'created_at': '2020-01-03'
}
]
return render_template("admin.html", posts=posts)
Pythonそれでは、サーバーを起動し「/admin」にアクセスしてみましょう。
▼「/admin」にアクセス時のブラウザ
このように無事、継承ができてエラーなしで表示されました。
今後は、これらの継承を多用していくので流れについて必ず理解しておくようにしましょう。
第二章 Flaskとデータベース
使用するデータベースについて
Pythonの変数をHTMLへ渡し「動的なHTMLの生成」ができるようになったところで、ここからはバックエンドの最も重要な部分「データベース操作」を行なっていきます。
今回使用するDB(DataBaseの略。以降DBと呼びます。)には、PostgreSQLを使用します。
PostgreSQLを採用する理由は以下のとおりです。
- オープンソースであり無償で使用できる
- 世界的にも人気が高い
- 今回のWEBアプリのデプロイ先であるHerokuで使用できる
Herokuでデプロイする際に「SQLite」を使用する無料教材がネット上には多くありますが、HerokuではSQLiteを非推奨としているため、使用は避けるようにしましょう。※
※参考 https://devcenter.heroku.com/ja/articles/sqlite3
PostgreSQLのインストール
それではPostgreSQLをインストールしていきましょう。各OSでのインストール方法は以下のとおりです。
▼Macをお使いの場合
brew install postgresql@16
Zsh▼Windowsをお使いの場合
こちらのリンクにアクセスして、赤枠をクリックしインストールしてください。
https://www.enterprisedb.com/downloads/postgres-postgresql-downloads
PostgreSQLの起動
Mac編
PostgreSQLがインストールができたら、次にデータベースサーバを起動します。
まずは現状の起動状態を確認してみましょう。
▼システム稼働状況確認コマンド
brew services list
Zsh起動ができていない状況では以下のように表示されます。
インストールした「postgresql@16」のstatusがnoneになっているのがわかります。
noneはまだ起動していない状態を表すので、起動していきましょう。
▼PostgreSQLの起動コマンド
brew services start postgresql@14
Zshコマンドが無事通っていれば起動ができているはずです。先ほど実行したシステム稼働状況コマンドで再度確認してみましょう。
▼システム稼働状況
statusがstartedになっており、無事に起動できたことがわかります。
補足ですが、postgresを使用しないときには下記のコマンドで停止しておきます。
▼データベースサーバー停止コマンド
brew services stop postgresql@14
ZshWindows編
現在執筆中です。
申し訳ありませんが、「windows postgresql 起動」などでご検索ください。
pgAdminについて
データベースのインストールと起動ができましたので、次にPostgreSQLのGUI操作ツール「pgAdmin」をインストールしていきます。
このツールを使用することで、アプリ上で簡単にデータを確認することができます。
PostgreSQLのCUI操作に慣れている方はインストールしなくても大丈夫です。
pgAdminのインストール
OSに応じて下記のリンクからインストールします。
Mac
リンク: https://www.postgresql.org/ftp/pgadmin/pgadmin4/v8.5/macos
Macに搭載のチップ種類に応じてダウンロードファイルが異なります。
Intel製チップ: x86_64.dmg
apple silicon製: arm64.dmg
Windows
リンク: https://www.postgresql.org/ftp/pgadmin/pgadmin4/v8.5/windows
pgAdminの設定
pgAdminの日本語化
pgAdminを開くと下記のような画面が表示されます。
初期の状態では英語表記になっているので、日本語表記に変更していきましょう。
「Configure pgAdmin」を選択します。
ソフトウェアの設定画面が表示されるので、左のサイドバーを少しだけスクロールし「Miscellaneous」(その他)の下の「User language」を選択。
「User language」から「Japanese」を選択後、右下の「Save」を押下します。
設定の変更を反映するためにページの更新を求められるので、「Refresh」を選択。
日本語化することができました!
pgAdminとPostgreSQLの接続
接続方法
それではPostgreSQLをGUI上で扱えるよう、pgAdminとPostgreSQLを接続していきます。
まずは「新しいサーバを追加」を選択。
接続したいサーバーの情報を入力する画面が表示されます。
通常DBサーバーへ接続するには以下の情報が必要となりますので、それぞれ入力方法を確認していきましょう。
- 接続先サーバーの名前
- 接続先アドレス
- ポート番号
- ユーザー名
- パスワード
①接続先サーバーの名前
「新しいサーバーを追加」を押下してすぐに「名前」という欄がありますが、こちらの名前は接続先サーバーの名前ではなく、pgAdmin上で扱う用のニックネーム(Alias)になります。
ここではFlaskで使用するdbということで分かり易く「flask-db」と名付けておきます。
任意の名前が入力できたら、接続先サーバーの名前を「接続」タブ内の、管理用データベースに入力します。
Postgresをインストールした際に、「postgres」という名前のサーバーが自動で設定されているので、初めから入力されている「postgres」のままで編集しなくてOKです。
②アドレス
接続先が外部のサーバーであれば、サーバのIPアドレスを入力するのですが、今回はローカルでの開発用に自分のPCに接続するので、「localhost」と入力しましょう。
③ポート番号
ポート番号は通常PostgreSQLを前述の方法で起動した際に「5432」が使用されています。
そのため、デフォルトの値を変更しなくて大丈夫です。
④ユーザ名
ユーザ名もサーバー名と同様にインストール時に「postgres」という自動で登録されているユーザ名を使用します。※
※Macの場合は、自動で現在ログインしているアカウントの名前も自動で登録されています。(私の場合「hoshi」というユーザ名も自動で登録されています)
⑤パスワード
MacOSを使用の場合、基本的には空白で大丈夫です。※
※ローカル接続時のデフォルト認証方式が「Trust=パスワードなしでもOK」のため。参考: https://www.postgresql.org/docs/current/auth-pg-hba-conf.html
pgAdminでテーブルの確認
データベースへの接続ができたら、データベース内にあるテーブルを確認してみます。
「Servers>flask-db>データベース>スキーマ>テーブル」の順で確認することができます。
テーブルをクリックしても、まだ何も表示されないことがわかります。
この後の章でFlask内でSQL-Alchemyというライブラリを使用してテーブルを作成します。
作成後に再度pgAdminで確認してみましょう。
Flaskとデータベースの接続
- OSへのデータベースのインストール
- データベースのGUI操作ツール
の2つができたところで、データベースを扱うために必要な設定の最後に
Python(Flask)でデータベースを操作するために2つのライブラリをインストールしていきます。
- psycopg
- SQL Alchemy
psycopgとは
pycopgは先ほどインストールしたPostgreSQLとPythonを繋ぐためのライブラリです。
psycopgのインストール
それではpycopgをインストールしていきましょう。他にもpsycopgをインストールする方法がありますが、最も簡単に使用していける方法であるbinaryバージョンをインストールしていきます。※
▼psycopgのインストール(poetry)
poetry add "psycopg[binary]"=3.1.18
Zsh▼psycopgのインストール(pip)
pip install psycopg[binary]==3.1.18
Zsh※参考: https://www.psycopg.org/psycopg3/docs/basic/install.html#binary-installation
SQLALchemyについて
次にPostgreSQLやMySQLなどデータベースの種類に関係なく同じコードの書き方で楽にDB操作をするためのライブラリであるSQL Alchemyをインストールしていきます。
また、SQL Alchemyのようなデータベース操作をデータベースの種類に依存せず簡単に行うためのツールを「Object Relational Mapper」略してORMと読んだりします。
それぞれ、下記のような意味を持ちます。
O: オブジェクト指向言語で扱うオブジェクト(Object)
R: リレーショナルデータベースのリレーショナル(Relational)
M: マッパー(Mapper)はデータ変換/移動を表すマッピング(Mapping)をするもののこと
PythonをO、RをPostgreSQLとすると、
「Python(Object)⇔変換(Mapper)⇔PostgreSQL(Relational Data Base)」
という関係で表せます。
SQLAlchemyのインストール
それではSQL Alchemyをインストールしていきましょう。今回はFlask専用にカスタマイズされたFlask-SQLAlchemyというライブラリをインストールしていきます。
▼SQL Alchemyのインストール(Poetry)
poetry add Flask-SQLAlchemy=3.1.1
Zsh▼SQL Alchemyのインストール(pip)
pip3 install Flask-SQLAlchemy==3.1.1
ZshFlaskとデータベースの接続
PythonでDBを操作するために必要なもののインストールが完了しました。
- OSへのデータベースのインストール
- データベースのGUI操作ツール
- PythonのDB操作ライブラリ
それでは、Python(Flask)からデータベースへ接続するコードを書いていきましょう。
▼元コード
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/admin")
def hello_world():
posts = [
{
'title': '記事のタイトル',
'body': '記事の内容',
'created_at': '2020-01-01'
},
{
'title': '記事のタイトル2',
'body': '記事の内容2',
'created_at': '2020-01-02'
},
{
'title': '記事のタイトル3',
'body': '記事の内容3',
'created_at': '2020-01-03'
}
]
return render_template("admin.html", posts=posts)
Python▼データベースへの接続コード追加版
from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
db = SQLAlchemy()
DB_INFO = {
'user': 'hoshi', #登録ユーザ名: ユーザ名(hoshi) or postgres
'password': '', #パスワード: Macのローカルの場合空白でもOK
'host': 'localhost',
'name': 'postgres' #DBの名前: ユーザ名(hoshi) or postgres
}
SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg://{user}:{password}@{host}/{name}'.format(**DB_INFO)
app.config["SQLALCHEMY_DATABASE_URI"] = SQLALCHEMY_DATABASE_URI
db.init_app(app)
@app.route("/admin")
def hello_world():
posts = [
{
'title': '記事のタイトル',
'body': '記事の内容',
'created_at': '2020-01-01'
},
{
'title': '記事のタイトル2',
'body': '記事の内容2',
'created_at': '2020-01-02'
},
{
'title': '記事のタイトル3',
'body': '記事の内容3',
'created_at': '2020-01-03'
}
]
return render_template("admin.html", posts=posts)
Python追加した部分だけ見てみましょう。
▼データベースへの接続コード追加版(追加部分のみ)
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
DB_INFO = {
'user': 'hoshi', #登録ユーザ名: ユーザ名(hoshi) or postgres
'password': '', #パスワード: Macのローカルの場合空白でもOK
'host': 'localhost',
'name': 'postgres' #DBの名前: ユーザ名(hoshi) or postgres
}
SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg://{user}:{password}@{host}/{name}'.format(**DB_INFO)
app.config["SQLALCHEMY_DATABASE_URI"] = SQLALCHEMY_DATABASE_URI
db.init_app(app)
Python処理の流れとしては、
- SQL Alchemyのインスタンス化(db)
- データベースの接続情報の文字列を作成(SQLALCHEMY_DATABASE_URI)
- SQLALCHEMY_DATABASE_URIをapp.configに格納
- SQL Alchemyのインスタンスのメソッドinit_appにflaskインスタンスを渡して実行
①、③、④はどのユーザーも同じコードになりますが、②に関しては設定しているDB情報によって変わるの注意しましょう。
DBの情報はpgAdminとPostgreSQLを接続した時の情報と同じです。
- 接続先サーバーの名前
- 接続先アドレス
- ポート番号
- ユーザー名
- パスワード
コード中にポート番号を入力することもできますが、入力しない場合ローカル接続時のデフォルトの番号(5432)が処理中に自動で補完されるので基本的には入力不要です。
テーブルの作成
テーブルの定義
DB⇔Flaskの接続ができたところで、テーブルデータをPythonを使用して作成していきます。
今回はブログアプリを作成するため、投稿されるブログの中身の情報のテーブルを作成していきます。
ブログの中身に必要な情報は以下の通りです。
- 投稿ID
- ブログタイトル
- ブログの本文
- 投稿日時
それでは、それぞれの情報をSQL Alchemyを使用して定義していきます。
▼投稿されるブログの中身の定義コード
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(850),nullable=False)
body = db.Column(db.String(300),nullable=False)
tokyo_timzone = pytz.timezone('Asia/Tokyo')
created_at = db.Column(db.DateTime,nullable=False,default=datetime.now(tokyo_timzone))
Python※nullableのデフォルトはTrueになるため、Falseと指定しています。
Columnの定義に関する情報はここから確認できます: https://docs.sqlalchemy.org/en/20/core/metadata.html#column-table-metadata-api
また、投稿日時のtimzezone取得用にpytzライブリをインストールしておきます。
▼pytzライブラリのインストール
poetry add pytz
Zshテーブル定義を入れたapp.pyのコードは以下のようになります。
▼app.pyコード全体
from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy
import pytz
from datetime import datetime
app = Flask(__name__)
db = SQLAlchemy()
db_info = {
'user': 'hoshi', #登録ユーザ名: ユーザ名(hoshi) or postgres
'password': '', #パスワード: Macのローカルの場合空白でもOK
'host': 'localhost',
'name': 'postgres' #DBの名前: ユーザ名(hoshi) or postgres
}
SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg://{user}:{password}@{host}/{name}'.format(**db_info)
app.config["SQLALCHEMY_DATABASE_URI"] = SQLALCHEMY_DATABASE_URI
db.init_app(app)
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(850), nullable=False)
body = db.Column(db.String(300), nullable=False)
tokyo_timzone = pytz.timezone('Asia/Tokyo')
created_at = db.Column(db.DateTime, nullable=False, default=datetime.now(tokyo_timzone))
@app.route("/admin")
def hello_world():
posts = [
{
'title': '記事のタイトル',
'body': '記事の内容',
'created_at': '2020-01-01'
},
{
'title': '記事のタイトル2',
'body': '記事の内容2',
'created_at': '2020-01-02'
},
{
'title': '記事のタイトル3',
'body': '記事の内容3',
'created_at': '2020-01-03'
}
]
return render_template("admin.html", posts=posts)
Pythonテーブルの作成
Python上でテーブルの定義が作成できたところで、定義の情報をもとにPostgreSQLにテーブルを作成していきます。
作成ではapp.pyファイルのあるディレクトリ下でpythonシェルを開き、下記のコードを実行します。
▼DBのテーブル作成コマンド(Python Shell)
>from app import app, db
>with app.app_context():
> db.create_all()
Zsh▼実際に実行した際のスクリーンショット
テーブルの確認
それでは、実際にテーブルが作成できているか、pgAdminを開いて確認してみましょう。
▼pgAdmin
無事テーブルの作成を確認することができました!
FlaskのCRUD
CRUDとは
Pythonを使用してデータベース接続、テーブル作成ができましたので、「Flask×DB」章の最後にCRUDの方法について解説していきます。
CRUDとはそれぞれ以下の頭文字をとって作られたバックエンドの重要機能を表します。
Create-作成 Read-読み込み Upate-更新 Delete-削除
また、それぞれの機能をブログに当てはめると、以下のようになります。
Create-記事の作成 Read-記事の表示 Upate-記事の更新 Delete-記事の削除
記事の作成(Create)
まずは記事の作成をおこなうためのコードを書いていきましょう。
下記の元コードをコピー&編集して、以下のような処理を作成していきます。
- 「/create」にアクセスすると、投稿画面が表示される
- 投稿画面からタイトルと本文を記入し、「投稿ボタン」を押すと内容がDBに保存される
- 「投稿ボタン」が押されると、「/admin」にリダイレクトされる
▼元コード
@app.route("/admin")
def hello_world():
posts = [
{
'title': '記事のタイトル',
'body': '記事の内容',
'created_at': '2020-01-01'
},
{
'title': '記事のタイトル2',
'body': '記事の内容2',
'created_at': '2020-01-02'
},
{
'title': '記事のタイトル3',
'body': '記事の内容3',
'created_at': '2020-01-03'
}
]
return render_template("admin.html", posts=posts)
Python①「/create」にアクセスすると、投稿画面が表示される
この処理を作成するためには、まず「投稿画面」が必要になるので、HTMLを書いていきます。
▼投稿画面(templates/create.html)
{% extends 'base.html' %}
{% block title %}新規登録{% endblock %}
{% block content %}
<h1>新規登録</h1>
<form method="POST">
<label for="title">タイトル</label>
<input type="text" name="title" id="title">
<label for="body">内容</label>
<input type="text" name="body" id="body">
<input type="submit" name="新規登録" value="投稿">
</form>
{% endblock %}
HTML次に「/create」にアクセスしたら、今作成した「create.html」を返す処理をapp.pyに作成します。
@app.route("/create")
def create():
return render_template("create.html")
Python①に関してはこれで終わりです。念の為、「/create」にアクセスして、フォームが表示されるか確認してみましょう。
▼「/create」アクセス時のブラウザ
②投稿画面からタイトルと本文を記入し、「投稿ボタン」を押すと内容がDBに保存される
次に「投稿ボタン」がクリックされた際に、以下の処理を行います。
- Flask側でPOSTを受け入れる
- POSTが来た際にDBへ情報を保存する
①Flask側でPOSTを受け入れる
Flask側で入力情報を受け入れるためには、下記のようにmethodsを指定しPOSTメソッドを受け入れる必要があります。
▼POSTを受け入れるコード
@app.route("/create", methods=['GET', 'POST'])
def create():
return render_template("create.html")
Python②POSTが来た際にDBへ情報を保存する
まずは編集後のコードを確認します。
▼DB保存とメソッド分岐追加コード
from flask import render_template, request
@app.route("/create", methods=['GET', 'POST'])
def create():
if request.method == 'POST': #①メソッドによる分岐
title = request.form.get('title') #②ポストされたデータの取得1
body = request.form.get('body')#②ポストされたデータの取得2
post = Post(title=title, body=body) #③取得データの保存1
db.session.add(post) #③取得データの保存2
db.session.commit() #③取得データの保存3
return render_template('create.html')
Pythonそれぞれコメントに記載していますが、 「POSTが来た時」に「POSTデータを取得」し「情報を保存する」の以下3つの処理を行う必要があります。
①まずは、flaskライブラリから「request」をインポートし、ユーザーからのリクエストのメソッドに応じて処理を分岐できるようにします。
②分岐ができたら、requestの機能により、ポストされたデータを取得します
③最後にそれぞれのデータをSQL Alchemyの機能を使用してPostgreSQLへ保存していきます。
③「投稿ボタン」が押されると、「/admin」にリダイレクトされる
現状のコードでは、「GET」メソッドが来た際に何も返却しておらず、投稿完了後も「create.html」を返却しているため、画面からの操作では何もできません。
ということで、画面上でスムーズに進めれるよう以下の処理に修正していきます。
- GETメソッドで「create.html」を返却
- POSTメソッドの場合、「/admin」へリダイレクトする
▼修正後のコード
from flask import Flask, render_template, request, redirect
@app.route("/create", methods=['GET', 'POST'])
def create():
if request.method == 'GET':
return render_template('create.html')
elif request.method == 'POST':
title = request.form.get('title')
body = request.form.get('body')
post = Post(title=title, body=body)
db.session.add(post)
db.session.commit()
return redirect('/')
Python大きな修正点は以下のとおりです。
①flaskライブラリから「redirect」をインポート
②メソッドに応じて、render_templateとredirectを実装
以上でCRUDのCー「記事の投稿画面と投稿情報のデータベースへの保存の処理」を行うことができました。
作成したコードの動作確認
念の為、実際に画面から情報を投稿し、pgAdminから情報が登録されているか確認していきましょう。
まずはpgAdminに情報が何も入っていないか確認していきます。
SQLコードを直接書く、もしくはpostを右クリックして「データを閲覧/編集>すべての行」をクリックすることで中身をすべて確認することができます。
▼データを閲覧/編集>すべての行
▼データの中身
現状、何もデータを保存していないので中身が空っぽであることがわかります。
それでは、投稿画面から記事の投稿→pgAdminの情報を確認してみましょう。
▼記事の投稿画面(127.0.0.1:5000/create)
上記の内容で投稿してみます。すると、、、
無事に、「/admin」へリダイレクトされ、
▼リダイレクト画面(127.0.0.1:5000/admin)
pgAdminでデータベースの中身を確認すると、、、
▼pgAdmin
無事、画面で入力された情報がFlaskを経由してDBへ保存されることが確認できました!
以降も似たような流れで、Read、Update、Deleteの処理を行なっていきます。
記事の読み込み(Read)
次は下記の元コードをコピーせずに編集する形で、以下のような処理を作成していきます。
- 「/admin」にアクセスすると、DBのpost情報を表示させる
▼元コード
@app.route("/admin")
def hello_world():
posts = [
{
'title': '記事のタイトル',
'body': '記事の内容',
'created_at': '2020-01-01'
},
{
'title': '記事のタイトル2',
'body': '記事の内容2',
'created_at': '2020-01-02'
},
{
'title': '記事のタイトル3',
'body': '記事の内容3',
'created_at': '2020-01-03'
}
]
return render_template("admin.html", posts=posts)
Pythonすでにpost情報を表示させるためのHTML(admin.html)は作成しているので、myapp.pyのみを以下のように編集していきます。
▼編集後の/admin処理部分コード
@app.route("/admin")
def admin(): #①helloo_workd→adminに変更
posts = Post.query.all() #②全ての投稿を取得
return render_template("admin.html", posts=posts)
Python修正点は以下のとおりです。
①関数名を/adminに合わせて、hello_worldからadminに変更
②DBからpostデータを取得するコードを追加
それでは早速「/admin」にアクセスして、画面を確認してみましょう。なお、現状2つのデータが入っています↓
▼pgAdmin
▼「/admin」ブラウザ画面
データベースの2つのデータが表示されていることが確認できました。
記事の更新(Update)
次に、記事の更新画面と更新処理を作成するため、以下の作業を行います。
- app.pyに記事更新用のupdate関数を作成
- create.htmlをコピーしupdate.htmlを作成
app.pyに記事更新用のupdate関数を作成
update関数は以下のようになります。
▼update関数部分
@app.route("/<int:post_id>/update", methods=['GET', 'POST'])#①postidをURLに追加
def update(post_id):#①postidを引数に追加
post = Post.query.get(post_id)#②postidで投稿を取得
if request.method == 'POST':
post.title = request.form.get('title')#②タイトルを更新
post.body = request.form.get('body')#②本文を更新
db.session.commit()#②DBを更新
return redirect('/admin')
else:
return render_template('update.html', post=post)
PythonHTTPメソッドによる分岐やredirectなどはcreate関数と同じですが、以下の処理が大きく異なります。
- 特定の投稿を更新するためにURL経由でpostidを受け取る
- SQL Alchemyのdb更新コードを挿入
②create.htmlをコピーしupdate.htmlを作成
記事更新のUIは記事投稿のUIを使い回していきますので、create.htmlをコピー後update.htmlに名称を変更しましょう。
▼update.htmlとディレクトリ構成
次にコピーした内容の中で、記事更新用に変更すべき点を修正していきます。
▼update.html
{% extends 'base.html' %}
{% block title %}編集{% endblock %}
{% block content %}
<h1>記事の更新</h1>
<form method="POST">
<label for="title">タイトル</label>
<input type="text" name="title" value="{{ post.title }}" id="title">
<label for="body">内容</label>
<input type="text" name="body" value="{{ post.body }}" id="body">
<input type="submit" name="更新">
</form>
{% endblock %}
HTML記事の更新用に、以下の修正を加えています。
- h1タグの中身を「新規登録」から「記事の更新に変更」
- タイトルと本文のvalueに既存の投稿内容を挿入
- 投稿ボタンの「投稿」文字を「更新」に変更
こちらも、修正ができたら他機能作成時と同様に「実際に更新できるか?」の動作確認をしておきましょう。
記事の削除(Delete)
最後に、記事の削除ができるよう、以下の作業を行います。
- app.pyに記事更新用のdelete関数を作成
- admin.htmlに削除ボタンを追加
①app.pyに記事更新用のdelete関数を作成
ポストを削除するための関数は以下の通りです。更新と同様にURLからpost_idを取得し、対象のpost_idのデータを消すようSQL Alchemyのメソッドを使用して削除します。
@app.route("/<int:post_id>/delete")
def delete(post_id):
post = Post.query.get(post_id) #①特定のpost_idデータの削除
db.session.delete(post) #①特定のpost_idデータの削除
db.session.commit() #①特定のpost_idデータの削除
return redirect('/admin')
Python②admin.htmlに削除ボタンを追加
Webアプリケーションとして機能させるために、「削除ボタン」をadmin.htmlに追加し、URLで直接「/post_id/delete」を叩かなくても投稿を削除できるようにしていきます。
削除ボタンは以下のように追記します。
▼admin.html
{% extends 'base.html' %}
{% block content %}
{% for post in posts %}
<article>
<a href="/{{ post.id }}/delete" role="button">削除</a>
<h2>{{ post.title }}</h2>
<p>{{ post.created_at }}</p>
<p>{{ post.body }}</p>
</article>
{% endfor %}
{% endblock %}
HTML削除ボタンを参考に、下記の2つのボタンもそれぞれ作成しておきましょう。
- 新規登録ボタン
- 記事更新ボタン
▼すべてのボタンを追加したadmin.html
{% extends 'base.html' %}
{% block content %}
<a href="/create" role="button">新規作成画面</a>
{% for post in posts %}
<article>
<a href="/{{ post.id }}/update" role="button">編集</a>
<a href="/{{ post.id }}/delete" role="button">削除</a>
<h2>{{ post.title }}</h2>
<p>{{ post.created_at }}</p>
<p>{{ post.body }}</p>
</article>
{% endfor %}
{% endblock %}
HTML画像の投稿機能の追加
ブログアプリの制作ということで、下記の画面のように記事の投稿時画像をアップロードと管理画面での表示をできるようにしていきます。
成果物のアップロード部分
現在のデータの削除
これから、新しく情報を追加していくため、UIから既存のデータを削除ボタンを押して全て削除しておきます。(DBから直接削除しても大丈夫です)
▼データの削除
カラムの追加
Webアプリにおいて画像をアップロード&表示できるようにするには、画像が保存されているパスをDBに保存し、そのパス先に画像データを保存する必要があります。
そのため、postテーブルに画像のパスを保存できるカラムを追加しましょう。
▼postデータベースの定義コード
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(850), nullable=False)
body = db.Column(db.String(300), nullable=False)
tokyo_timzone = pytz.timezone('Asia/Tokyo')
created_at = db.Column(db.DateTime, nullable=False, default=datetime.now(tokyo_timzone))
img_name = db.Column(db.String(300), nullable=True) #画像のパス保存用カラムを追加
Pythonimg_nameというカラムを追加します。また、パスを保存できるよう文字列型とします。
マイグレーションとは?
それでは、「前回同様にpythonシェルからdb.createを実行しテーブル作成していきましょう」…と言いたいところですが、
今回はテーブルの新規作成ではないため少し話が変わります。
これまでに存在しなかったカラムを既存のテーブルに追加する場合、データベースのマイグレーションという操作を行う必要があります。
Migration(マイグレーション)とは、本来「環境や季節の変化に合わせ生存確率を高めるため動物が集団でAからBへ移住すること」を表す英単語です。
データベースも生き物と同様に、ユーザーの行動やソフトウェア環境の変化に生存していくために古い構造から新しい構造へと移動(変化)させていく必要があります。
IT業界では、フレームワークや言語に関わらず、データベースのカラムを追加や削除、形式などを変更する際には「マイグレーション」という単語を頻繁に使用するので頭に入れておきましょう。
Flask-migrateのインストール
マイグレーションの単語の意味が理解できましたか?
それでは早速Flaskにおいてのマイグレーションを行うため、最新の「Flask-Migrate」というライブラリをインストールしていきます。
▼Flask-Migrateのインストール(poetry)
poetry add Flask-Migrate
Zshマイグレーションの実行
マイグレーションを実行するにはmyapp.pyにマイグレーションのためのコードを書く必要があります。
▼追加するコード
from flask_migrate import Migrate #flask_MigrateからMigrateライブライをインポート
migrate = Migrate(app, db) #appとdbを引数にMigrateをインスタンス化
Python▼myapp.pyマイグレーションコード追加版上部
from flask import Flask, render_template, request, redirect
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate #Migrateライブライをインポート
from flask_login import UserMixin, LoginManager, login_user, login_required, logout_user
import pytz
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
import os
app = Flask(__name__)
db = SQLAlchemy()
db_info = {
'user': 'hoshi',
'password': '',
'host': 'localhost',
'name': 'postgres'
}
SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg://{user}:{password}@{host}/{name}'.format(**db_info)
app.config["SQLALCHEMY_DATABASE_URI"] = SQLALCHEMY_DATABASE_URI
app.config["SECRET_KEY"] = os.urandom(24)
db.init_app(app)
migrate = Migrate(app, db) #appとdbを引数にMigrateをインスタンス化
login_manager = LoginManager()
login_manager.init_app(app)
Pythonコードが追加できたら、マイグレーションに必要な実行や設定用のファイル群を準備するために下記のコマンドをターミナルで実行します。
▼マイグレーション初期設定コマンド
flask --app myapp db init
Zsh上記のコマンドが成功すると、下記のように「migrations」というフォルダとその他マイグレーションに必要なファイル群が自動で生成されています。
▼db init後のディレクトリ構造
初期設定ができたので、次にテーブル情報の変更部分を検知し、実際にテーブルの変更を加えるために必要なスクリプトを生成するためのコマンドを実行します。
ダブルクォーテーション(“)の中身はgit commitのように、変更する内容をコメントとして記載しておきます。
▼テーブル設定変更スクリプト生成用コマンド
flask --app myapp db migrate -m "Add img_name column to Post model"
Zsh実行すると、「migrations/versions/」の中に新しいファイルが生成されているのがわかります。
▼migrateコマンド実行後のディレクトリ構成
ファイルの中身を確認すると、今回変更を加えた点を検出して「upgrade関数」では、カラムの追加、「downgrade関数」では、カラムの削除を実行しようとしている様子が伺えます。
▼migrateコマンドで生成されたスクリプトの詳細
今回は、img_nameというカラムをPostというテーブルに追加したいので、下記のコマンドを実行して、データベースへ変更を反映します。
▼データベースへマイグレーションを反映するコマンド
flask --app myapp db upgrade
Zsh実行が確認できたら、pgAdminでデータベースの変更を確認してみましょう。
pgAdminを改めて開く際には「右クリック>再読み込み」を選択しましょう。
▼pgAdmin>再読み込み
新しくimg_nameというカラムが追加されたことが確認できました!
▼pgAdmin>Post>列
static/imgフォルダの作成
画像のパスを保存するためのカラムをDBへ追加できたので、次に画像本体を保存するディレクトリを作成します。
Flaskでは、画像やCSSなどの静的ファイルを作成する際に、「static」というディレクトリを作成することがルールになっています。
下記のようにVSCodeやターミナルから、「static」というフォルダをプロジェクト直下に作成し、その中に「css」と「img」というフォルダも作成しておきましょう。
▼各フォルダの作成
create.htmlの更新
画像および画像のパスを保存する体制ができたので、アップロードするための機能をcreate.htmlの画面に追加しましょう。
{% extends 'base.html' %}
{% block title %}新規登録{% endblock %}
{% block content %}
<h1>新規登録</h1>
<!-- enctype="multipart/form-data"を追加 -->
<form method="POST" enctype="multipart/form-data">
<label for="title">タイトル</label>
<input type="text" name="title" id="title">
<label for="body">内容</label>
<input type="text" name="body" id="body">
<!-- 新規追加 -->
<label for="fileUpload">画像</label>
<input type="file" name="img" id="fileUpload">
<!-- 新規追加 -->
<input type="submit" name="新規登録" value="投稿">
</form>
{% endblock %}
HTMLadmin.htmlの更新
次にアップロードされた画像をadmin.htmlで表示できるよう以下のコードを追記します。
▼画像表示用追記コード
<img src="{{ url_for('static', filename='img/' + post.img_name) }}" width="150" height="100">
Python画像の参照先をjinjaの機能を使用した書き方で指定していきます。
具体的には、url_forという関数を{{}}で括り、第一引数には「‘static’」、そしてfilenameには作成した「画像アップロード先のディレクトリ(「’img’」)とアップロードされたファイル名を文字列として結合することで表現します。
widthとheightはとりあえず150,100と小さく表示しておきます。見た目に関しては、この後の章でBootstrapを使用した装飾を行います。
コードの追加と一部表示順を変更したadmin.htmlは以下の通りです。
▼admin.html
{% extends 'base.html' %}
{% block content %}
<a href="/create" role="button">新規作成画面</a>
<a href="/logout" role="button">ログアウト</a>
{% for post in posts %}
<article>
<img src="{{ url_for('static', filename='img/' + post.img_name) }}" width="150" height="100">
<h2>{{ post.title }}</h2>
<p>{{ post.created_at }}</p>
<p>{{ post.body }}</p>
<a href="/{{ post.id }}/update" role="button">編集</a>
<a href="/{{ post.id }}/delete" role="button">削除</a>
</article>
{% endfor %}
{% endblock %}
Pythoncreate関数の修正
フロント側は機能追加できたので、最後にバックエンド側(craete関数)の機能を画像のアップロードに合わせて変更していきます。
記事のタイトルや本文と同様に、requestモジュールを使用しアップロードされたファイルの情報を取得していきます。
情報が取得できたら、画像は指定フォルダへ保存し、パスはDBへ保存していきます。
@app.route("/create", methods=['GET', 'POST'])
@login_required
def create():
if request.method == 'GET':
return render_template('create.html')
elif request.method == 'POST':
file = request.files['img'] # ファイルを取得
filename = file.filename # ファイル名を取得
img_name = os.path.join(app.static_folder, 'img', filename) # 保存するパスを作成
file.save(img_name) # ファイルを保存
title = request.form.get('title')
body = request.form.get('body')
post = Post(title=title, body=body, img_name=filename)
db.session.add(post)
db.session.commit()
return redirect('/admin')
Python動作確認
それでは、実際にサンプルの画像をアップロードしてブログ管理画面で表示できるか確認していきましょう。
課題-1
現状のcreate関数の場合、画像ファイル以外もアップロードできる状態になっています。
画像ファイル以外をアップロードさせたくない処理はライブラリやFlaskのフレームワーク機能を使用せずに、Pythonの基礎知識さえあれば作成することができます。
ヒント: 拡張子を制限する
▼現状のcreate関数
@app.route("/create", methods=['GET', 'POST'])
@login_required
def create():
if request.method == 'GET':
return render_template('create.html')
elif request.method == 'POST':
file = request.files['img']
filename = file.filename
img_name = os.path.join(app.static_folder, 'img', filename)
file.save(img_name)
title = request.form.get('title')
body = request.form.get('body')
post = Post(title=title, body=body, img_name=filename)
db.session.add(post)
db.session.commit()
return redirect('/admin')
Python画像ファイル以外はアップロードできないように処理を加えた最終コードに関しては、以下のLineより「Flask基礎完全攻略」の電子書籍と共にお配りしています。
他にもアップロードを試すためのサンプル画像も配布しております。
下記よりLine登録するだけでお受け取り頂けますので、ぜひ登録してみてください。
SQLAlchemyの操作まとめ
ここで、CRUD操作を行う上で機能の要となるSQL Alchemyの基本操作をおさらいしておきましょう。
データの登録(Create)
必要な情報をデータベースのモデルの引数に渡し、sessionのaddとcommitを行う。
post = Post(title=title, body=body)
db.session.add(post)
db.session.commit()
Pythonデータの読み込み(Read)
必要な情報をデータベースのモデルのqueryメソッドのall()ですべてを抽出。
posts = Post.query.all()
Pythonデータの更新(Update)
まずはprimary keyを元に情報を取得、取得した情報から更新したい情報をドットアノテーションで指定し情報を代入。代入後sessionをcommit。
post = Post.query.get(post_id)
post.title = '更新されたタイトル'
post.body = '更新された本文'
db.session.commit()
Pythonデータの削除(Delete)
まずはprimary keyを元に情報を取得し、sessionのdeleteメソッドに代入しcommit。
post = Post.query.get(post_id)
db.session.delete(post)
db.session.commit()
Python課題-2
ここまでで、以下のページを作成してきました。
- 投稿記事管理画面(admin.html)
- 記事投稿画面(create.html)
- 記事更新画面(update.html)
現状のままでは、ブログとしてこのアプリケーションを考えた時、以下の二つの画面が存在しません。
- ユーザがアクセスし情報を確認する画面(index.html)
- ブログの詳細を確認する画面(readmore.html)
成果物の参考動画にある内容を基に、二つの画面を課題として作成してみましょう。
▼成果物参考動画
これまでの知識があれば、作成することは可能です。
正解コードを確認したい方は、以下のLineより「Flask基礎完全攻略」の電子書籍と共に、完成版のコードをお配りしています。
そちらよりLine登録するだけでお受け取り頂けますので、ぜひ登録してみてください。
第三章 Flaskとログイン
Flaskとログイン
ここまでで、管理者画面(admin.html)のために必要な記事の編集機能を作成してきました。
ここから機能面開発の最終工程として、以下の機能を開発していきます。
- サインアップ
- ログイン
- ログイン判定機能
- ログアウト
ログインの仕組み
この後にログイン機能の実装について解説していくのですが、実装に記述するコードだと、なぜそのコードが必要なのか?をイメージするためにはログイン機能を理解する必要があります。
例えば、以下の質問には回答できますか?
「FacebookやXでログインした後に、なぜ毎回パスワードとIDを入力しなくても他の人では見れない情報や操作ができるのか?」
「そういえば、なんでできるのか分からない。」と思った方が多いかと思います。
そのような今後の開発に役立つような基礎的な情報も「Flask基礎完全攻略」の電子書籍には解説がありますので、下記のURLよりLine登録して無料で受け取り勉強してみてください。
ログイン用ライブラリのセットアップ
1/4 flask_loginのインストール
ログイン機能の作成には最新版の「flask_login」ライブラリを使用していきます。
▼flask_loginのインストール(poetry)
poetry add flask_login
Zsh2/4 ユーザ管理データベースの定義作成
次にアプリケーションに登録されているユーザを管理するためのユーザDBを作成します。
作成の要領の基本は、postDB作成時と同じですが、今回はログイン機能に関わるDBのため少し作成方法が異なります。
▼ユーザー管理DB定義コード
from flask_login import UserMixin
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(30), unique=True, nullable=False)
password = db.Column(db.String(120), unique=False, nullable=False)
Pythonそれぞれログインをするために必要な情報になるのですが、flask_loginライブラリからUserMixinをインポートし、Userデータベースで継承を行なっています。
UserMixinには、ログイン管理に必要なセッション管理や認証状態の確認に必要なメソッドが実装されているため、継承するだけでflask_loginの機能を十分に活用していくことができます。
3/4 ユーザ管理データベースのテーブル作成
画像投稿機能作成の際にFlask-Migrateをインストールしたことを覚えていますか?
ユーザデータベースはその機能を再度使用して、作成していきます。
まずは、新しく定義したデータベース作成のためのスクリプトを下記のコマンドで生成します。
▼マイグレーションスクリプト作成コマンド
flask --app myapp db migrate -m "Create User model"
Zsh生成したスクリプトを下記のコマンドで実行し、テーブルを作成します。
▼マイグレーションスクリプト実行コマンド
flask --app myapp db upgrade
Zsh実行後は念の為、pgAdminからDBが作成できたか確認しておきましょう。
▼pgAdmin
4/4 flask_loginの初期設定
データベースが作成できたら、flask_loginでログイン機能を実装していくために必要なコードを書いていきます。
▼追加コード部分(myapp.py)
from flask_login import UserMixin, LoginManager
import os
#①Flaskのセッション情報の暗号化等に使用
app.config["SECRET_KEY"] = os.urandom(24)
#②login管理用
login_manager = LoginManager()
login_manager.init_app(app)
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(30), unique=True, nullable=False)
password = db.Column(db.String(120), unique=False, nullable=False)
#③現在のユーザーを識別するために必要
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id)) #引数には文字列がくるため整数型に変換
Pythonそれぞれのコードを簡単に解説します。
①Flaskではセッション情報をクライアント側、つまりユーザーのブラウザのクッキーに保存するのですが、その内容を暗号化したり、サーバー自身が発行者が自分であることを認めるために使用されます。
②login管理のクラスであるLoginManagerの初期化のため、LoginManagerクラスをインスタンス化し、init_appメソッドにappを引数に代入した該当の2行を記述します。
③ログイン後、ユーザからリクエストがある度にセッション情報を基にユーザIDを取得し、現在もユーザーは存在しているか?などの最新情報を確認したり、current_userというflask_loginライブラリに含まれる他のメソッドを活用する時などに役立つため、実装は必須です。
現在の最終的なmyapp.pyの中身は以下のようになっています。
▼myapp.py
from flask import Flask, render_template, request, redirect
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import UserMixin, LoginManager
import pytz
from datetime import datetime
import os
app = Flask(__name__)
db = SQLAlchemy()
db_info = {
'user': 'hoshi',
'password': '',
'host': 'localhost',
'name': 'postgres'
}
SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg://{user}:{password}@{host}/{name}'.format(**db_info)
app.config["SQLALCHEMY_DATABASE_URI"] = SQLALCHEMY_DATABASE_URI
app.config["SECRET_KEY"] = os.urandom(24)
db.init_app(app)
migrate = Migrate(app, db)
login_manager = LoginManager()
login_manager.init_app(app)
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(850), nullable=False)
body = db.Column(db.String(300), nullable=False)
tokyo_timzone = pytz.timezone('Asia/Tokyo')
created_at = db.Column(db.DateTime, nullable=False, default=datetime.now(tokyo_timzone))
img_name = db.Column(db.String(300), nullable=True)
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(30), unique=True, nullable=False)
password = db.Column(db.String(120), unique=False, nullable=False)
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
@app.route("/admin")
def admin():
posts = Post.query.all()
return render_template("admin.html", posts=posts)
@app.route("/create", methods=['GET', 'POST'])
def create():
if request.method == 'GET':
return render_template('create.html')
elif request.method == 'POST':
file = request.files['img']
filename = file.filename
img_name = os.path.join(app.static_folder, 'img', filename)
file.save(img_name)
title = request.form.get('title')
body = request.form.get('body')
post = Post(title=title, body=body, img_name=filename)
db.session.add(post)
db.session.commit()
return redirect('/admin')
@app.route("/<int:post_id>/update", methods=['GET', 'POST'])
def update(post_id):
post = Post.query.get(post_id)
if request.method == 'GET':
return render_template('update.html', post=post)
elif request.method == 'POST':
post.title = request.form.get('title')
post.body = request.form.get('body')
db.session.commit()
return redirect('/admin')
@app.route("/<int:post_id>/delete")
def delete(post_id):
post = Post.query.get(post_id)
db.session.delete(post)
db.session.commit()
return redirect('/admin')
Pythonサインアップの実装
サインアップ画面の作成(signup.html)
サインアップ画面には、ログインに必要な情報である①ユーザ名②パスワードの二つを登録するように画面を設計します。
▼templates/singnup.html
{% extends 'base.html' %}
{% block content %}
<h1>ユーザー登録</h1>
<form method="POST">
<label for="">ユーザー名</label>
<input type="text" name="username">
<label for="">パスワード</label>
<input type="password" name="password">
<input type="submit" name="ユーザー登録">
</form>
{% endblock %}
HTMLサインアップ機能の実装
サインアップ機能は①ユーザ名②パスワードの二つを登録するだけなので、
create関数をコピーして編集していきましょう。
▼signup関数コード
from werkzeug.security import generate_password_hash #パスワードのハッシュ化
@app.route("/signup", methods=['GET', 'POST'])
def signup():
if request.method == 'GET':
return render_template('signup.html')
elif request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
hashed_pass = generate_password_hash(password) #パスワードのハッシュ化
post = User(username=username, password=hashed_pass)
db.session.add(post)
db.session.commit()
return redirect('/login')
Python真面目にcreate関数からコピーして作成した方は
「あれ、なんか違う….!」となったのではないでしょうか?
そのままsignupに合わせて編集するだけ、と言いたいところですが、
passwordの保存時にパスワードのハッシュ化※が必要となります。
また、ハッシュ化にはwerkzeugというflaskの基盤になっているライブラリを使用します。werkzeugはFlaskインストール時に同時に自動でインストールされています。
※ハッシュ化とは、パスワードの文字列をランダムな文字列にして安全に保管する技術です。暗号化とは異なり、鍵のような情報を基にハッシュ化された文字列を元の文字列に戻すことは非常に困難なため、パスワードのような作成したユーザ以外に知る必要がないような機密情報を保管する際に役立ちます。
動作確認
login.htmlは未完成のため、サインアップ成功後「Not Found」が出てしまいますが、実際にユーザ名とパスワードを入力後、pgAdminを確認してデータの保存を確認してみましょう。
▼signup.html
▼サインアップ後のpgAdmin
無事にユーザー名とハッシュ化されたパスワードの保存が確認できました。
ログインの実装
ログイン画面の作成(login.html)
ログイン画面は、サインアップ画面をコピーして一部名称を変更し、ログイン失敗時用にメッセージ(msg)を表示できるようにしておきます。
▼ログイン画面(login.html)
{% extends 'base.html' %}
{% block content %}
<h1>ログイン</h1>
{{ msg }}
<form method="POST">
<label for="">ユーザー名</label>
<input type="text" name="username">
<label for="">パスワード</label>
<input type="password" name="password">
<input type="submit" name="ログイン">
</form>
{% endblock %}
HTMLログイン機能の実装
ログイン機能では、入力された情報が①DBの内容と一致しているか②照合し、③正しければログインさせ、④誤っていればメッセージと共にログイン画面に戻す形にしていきます。
▼login関数部分
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin, LoginManager, login_user
@app.route("/login", methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
user = User.query.filter_by(username=username).first() #①DBからusernameでユーザー情報検索
if check_password_hash(user.password, password=password): #②パスワード照合
login_user(user) #③ログイン
return redirect('/admin')
else:
return redirect('/login', msg='ユーザ名/パスワードが違います') #④ログイン画面に戻す
else:
return render_template('login.html', msg='')
Python①query.filter_byというメソッドを使用し、入力されたusernameに当てはまる情報を検索します。usernameはDB定義時にuniqueとしているため、複数行が取得されることはありません。そのため、.first()で最初の一行を取るだけで大丈夫です。
②パスワード照合に関しては、generate_password_hashと同じように、werkzeug.securityからcheck_password_hashというメソッドをインポートし活用します。
③ログインに必要な処理はflask_loginのlogin_userを使用するだけで簡単にできてしまいます。
④ログイン画面に戻す際には、なぜ戻ったのかをユーザに示すために理由をメッセージとして返します。
動作確認
login.htmlが作成できたので、再度signup.htmlから、ユーザ登録→ログイン→admin.htmlの流れを見ていきましょう。
▼ユーザー登録画面(signup.html)
▼pgAdmin
▼ログイン画面(login.html)
▼ログイン後の画面(admin.html)
無事にサインアップ〜ログインを行うことができました!
ログアウトの実装
ログアウト機能の実装
ログアウトもログイン同様にflask_loginライブラリに含まれるlogout_userを使用することで簡単に実装することができます。
▼ログアウト関数部分
from flask_login import UserMixin, LoginManager, login_user, login_required, logout_user
@app.route('/logout')
@login_required
def logout():
logout_user()
return redirect('/login')
Pythonログアウトボタンをadmin.htmlに実装しておきます。
▼templates/admin.html
{% extends 'base.html' %}
{% block content %}
<a href="/create" role="button">新規作成画面</a>
<a href="/logout" role="button">ログアウト</a>
{% for post in posts %}
<article>
<a href="/{{ post.id }}/update" role="button">編集</a>
<a href="/{{ post.id }}/delete" role="button">削除</a>
<h2>{{ post.title }}</h2>
<p>{{ post.created_at }}</p>
<p>{{ post.body }}</p>
</article>
{% endfor %}
{% endblock %}
HTML▼実装後の記事管理画面(admin.html)
次の「ログイン判定機能の実装」後に動作確認をしてみましょう。
ログイン判定機能の実装
現状のままだと、アクセス制御機能を実装していないため、ログインをしなくても誰でもadmin.htmlにアクセスできてしまいます。
ログイン判定機能の実装
「ログイン判定機能」というとものすごく難しそうに聞こえるのですが、flask_loginを使用すると非常に簡単にアクセス制御をすることができます。
まずは、flask_loginからlogin_requiredを追加でインポートします。
▼login_requiredのインポート
from flask_login import UserMixin, LoginManager, login_user, login_required
Pythonあとはこのlogin_requiredをデコレータとしてログインが必要なページの関数に実装していくだけです。ログインが必要なのは以下のページ(関数)です。
・ブログ管理画面(admin)
・記事投稿画面(create)
・記事削除(delete)
・記事更新(update)
▼login_requiredの実装
@app.route("/admin")
@login_required
def admin():
posts = Post.query.all()
return render_template("admin.html", posts=posts)
@app.route("/create", methods=['GET', 'POST'])
@login_required
def create():
if request.method == 'GET':
return render_template('create.html')
elif request.method == 'POST':
file = request.files['img']
filename = file.filename
img_name = os.path.join(app.static_folder, 'img', filename)
file.save(img_name)
title = request.form.get('title')
body = request.form.get('body')
post = Post(title=title, body=body, img_name=filename)
db.session.add(post)
db.session.commit()
return redirect('/admin')
@app.route("/<int:post_id>/update", methods=['GET', 'POST'])
@login_required
def update(post_id):
post = Post.query.get(post_id)
if request.method == 'GET':
return render_template('update.html', post=post)
elif request.method == 'POST':
post.title = request.form.get('title')
post.body = request.form.get('body')
db.session.commit()
return redirect('/admin')
@app.route("/<int:post_id>/delete")
@login_required
def delete(post_id):
post = Post.query.get(post_id)
db.session.delete(post)
db.session.commit()
return redirect('/admin')
Python動作確認
それでは、login_requiredの動作を確認するために以下の2点を編集したすべての画面で確認してみましょう。
- ログインをしないとアクセスできないようになっているか?
- 逆にログイン後はアクセスできるようになっているか?※
ログインをせずにlogin_required実装箇所にアクセスすると以下のようになります。
▼ログイン前の/admin
「Unauthorized」ー許可はないですよ。と表示されればOKです。
▼ログイン後の/admin
ログインをしている場合は、問題なく表示されます。
その他、記事投稿画面や編集画面に関しても、ログアウト状態やログイン状態でうまく動作するか確認しておきましょう。
第四章 アプリの装飾
Bootstrapの適用
基本機能とページがすべてできましたので、最後にもう少しWebアプリケーションらしくデザインを入れていきます。デザインにはBootstrapを使用していきます。
Bootstrapを使用するための設定方法はいくつかあるのですが、今回は公式サイトにあるCDNへのリンクをHTMLに貼り付けて使えるようにしていきます。
Bootstrap公式: https://getbootstrap.jp/
公式ページの真ん中にあるCSS用のCDNリンクをコピー。
▼公式ページ
base.htmlへリンクを貼り付けます。ついでにブラウザのタブに表示されるタイトルも「{% block title%}」を使用してページ毎に変更できるようにしておきます。
▼base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
<title>{% block title %}【要変更】デフォルトタイトル{% endblock %}</title>
</head>
<body>
{% block content %}
{% endblock %}
</body>
</html>
HTMLCSSを使用する方法
次にFlask上でCSSを使用する方法3ステップを解説します。
1/3 cssフォルダの作成
次に、static配下に「css」というフォルダを作成します。
▼cssフォルダの作成
2/3 cssファイルの作成
cssフォルダ配下に使用したいcssファイルを作成します。
▼cssファイルの作成
3/3 CSSフォルダの読み込み
作成したいCSSファイルをbase.htmlで読み込みます。読み込み方法は以下の通りです。
<link rel="stylesheet" href="{{ url_for('static',filename='css/sample.css') }}">
HTMLCSSでも画像の読み込み時と同様にパスの指定を行います。
以下、上のコードをbase.htmlに挿入したコードになります。ついでに、サイトの言語指定部分「lang=en」になっていたのを「lang=ja」に変更しています。
▼修正済み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/sample.css') }}">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
<title>{% block title %}【要変更】デフォルトタイトル{% endblock %}</title>
</head>
<body>
{% block content %}
{% endblock %}
</body>
</html>
HTML以上でデザインを変更するための準備は終わりになります。
各ページの体裁を整える
admin.html
それではBootstrapとCSSを使用して、各ページの体裁を整えていきましょう。
それぞれのページを修正していく様子は動画にありますので、コーディング経過を見たい方は下記のYoutubeからご覧ください。▼
Youtubeリンク
まずは下記のadmin.htmlを修正していきます。
▼修正前のadmin.html
{% extends 'base.html' %}
{% block content %}
<a href="/create" role="button">新規作成画面</a>
<a href="/logout" role="button">ログアウト</a>
{% for post in posts %}
<article>
<img src="{{ url_for('static', filename='img/' + post.img_name) }}" width="150" height="100">
<h2>{{ post.title }}</h2>
<p>{{ post.created_at }}</p>
<p>{{ post.body }}</p>
<a href="/{{ post.id }}/update" role="button">編集</a>
<a href="/{{ post.id }}/delete" role="button">削除</a>
</article>
{% endfor %}
{% endblock %}
HTML▼修正前の画面
▼修正後のadmin.html
{% extends 'base.html' %}
{% block title %}管理ページ{% endblock %}
{% block content %}
<section class="container my-5">
<div class="text-center mb-4">
<h2>ほしのブログ</h2>
<p>最新のニュースとトレンドをお伝えします</p>
<a href="/create" class="btn btn-info">新規作成画面</a>
</div>
<div class="row row-cols-3 g-4">
{% for post in posts %}
<div class="col">
<div class="card">
<img src="{{ url_for('static', filename='img/' + post.img_name) }}" class="card-img-top">
<div class="card-body">
<h5 class="card-title">{{ post.title }}</h5>
<p class="card-text">{{ post.body[:10] }}</p>
<p>投稿日:{{ post.created_at }}</p>
<a href="/{{ post.id }}/update" class="btn btn-success">編集</a>
<a href="/{{ post.id }}/delete" class="btn btn-danger">削除</a>
</div>
</div>
</div>
{% endfor %}
</div>
</section>
{% endblock %}
HTML▼修正後の画面
create.html
次にcreate.htmlを修正していきます。
▼修正前のcreate.html
{% extends 'base.html' %}
{% block title %}新規登録{% endblock %}
{% block content %}
<h1>新規登録</h1>
<form method="POST" enctype="multipart/form-data">
<label for="title">タイトル</label>
<input type="text" name="title" id="title">
<label for="body">内容</label>
<input type="text" name="body" id="body">
<label for="fileUpload">画像</label>
<input type="file" name="img" id="fileUpload">
<input type="submit" name="新規登録" value="投稿">
</form>
{% endblock %}
HTML▼修正前の作成画面
修正後のコードと画面は以下の通りです。
▼修正後のcreate.html
{% extends 'base.html' %}
{% block title %}新規登録{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="row">
<div class="col-lg-8 mx-auto">
<h2 class="mb-4">ブログ記事の作成</h2>
<form method="POST" enctype="multipart/form-data" class="form-group">
<div class="mb-3">
<label for="title" class="form-label">タイトル</label>
<input type="text" class="form-control" id="title" name="title" placeholder="タイトルを入力">
</div>
<div class="mb-3">
<label for="body" class="form-label">記事</label>
<textarea class="form-control" id="body" rows="8" name="body" placeholder="記事を入力"></textarea>
</div>
<div class="mb-3">
<label for="fileUpload" class="form-label">ファイルを選択</label>
<input class="form-control" type="file" name="img" id="fileUpload">
</div>
<button type="submit" class="btn btn-primary">作成</button>
</form>
</div>
</div>
</div>
{% endblock %}
HTML▼修正後の作成画面
update.html
create.htmlのレイアウトをほぼ真似る形でupdate.htmlも修正していきます。
▼修正前のupdate.html
{% extends 'base.html' %}
{% block title %}編集{% endblock %}
{% block content %}
<h1>記事の更新</h1>
<form method="POST">
<label for="title">タイトル</label>
<input type="text" name="title" value="{{ post.title }}" id="title">
<label for="body">内容</label>
<input type="text" name="body" value="{{ post.body }}" id="body">
<input type="submit" name="更新">
</form>
{% endblock %}
HTML▼修正前の更新画面
▼修正後のupdate.html
{% extends 'base.html' %}
{% block title %}編集{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="row">
<div class="col-lg-8 mx-auto">
<h2 class="mb-4">ブログ記事の編集</h2>
<form method="POST">
<div class="mb-3">
<label for="title" class="form-label">タイトル</label>
<input type="text" class="form-control" id="title" name="title" value="{{ post.title }}">
</div>
<div class="mb-3">
<label for="body" class="form-label">記事</label>
<textarea class="form-control" id="body" rows="8" name="body">{{ post.body }}</textarea>
</div>
<button type="submit" class="btn btn-primary">作成</button>
</form>
</div>
</div>
</div>
{% endblock %}
HTML▼修正後の更新画面
signup.html
▼修正前のsignup.html
{% extends 'base.html' %}
{% block content %}
<h1>ユーザー登録</h1>
<form method="POST">
<label for="">ユーザー名</label>
<input type="text" name="username">
<label for="">パスワード</label>
<input type="password" name="password">
<input type="submit" name="ユーザー登録">
</form>
{% endblock %}
HTML▼修正前のサインアップ画面
▼修正後のsignup.html
{% extends 'base.html' %}
{% block content %}
<div class="container mt-5">
<div class="row">
<div class="col-6 mx-auto">
<div class="card">
<div class="card-body">
<h3 class="card-title text-center">アカウント登録</h3>
<form method="POST">
<div class="mb-3">
<label for="username" class="form-label">ユーザ名</label>
<input type="text" class="form-control" id="username" name="username" placeholder="ユーザ名" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">パスワード</label>
<input type="password" class="form-control" id="password" name="password" placeholder="パスワード" required>
</div>
<div class="mb-3">
<input type="submit" class="btn btn-primary w-100" value="アカウント登録">
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
HTML▼修正後のサインアップ画面
login.html
▼修正前のlogin.html
{% extends 'base.html' %}
{% block content %}
<h1>ログイン</h1>
{{ msg }}
<form method="POST">
<label for="">ユーザー名</label>
<input type="text" name="username">
<label for="">パスワード</label>
<input type="password" name="password">
<input type="submit" name="ログイン">
</form>
HTML▼修正前のログイン画面
▼修正後のlogin.html
{% extends 'base.html' %}
{% block content %}
<div class="container mt-5">
<div class="row">
<div class="col-6 mx-auto">
<div class="card">
<div class="card-body">
<h3 class="card-title text-center">ログイン</h3>
<form method="POST">
<div class="mb-3">
<label for="username" class="form-label">ユーザ名</label>
<input type="text" class="form-control" id="username" name="username" placeholder="ユーザ名" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">パスワード</label>
<input type="password" class="form-control" id="password" name="password" placeholder="パスワード" required>
</div>
<div class="mb-3">
<input type="submit" class="btn btn-primary w-100" value="ログイン">
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
HTML▼修正後のログイン画面
課題-3
ある程度まで各ページを見やすく編集してきましたが、冒頭の成果物と比較するとまだ装飾足りません。
今回はHTMLやCSSの講義ではなく、Flaskの講義のため、残りはできるところまで自分でやってみましょう。
・Bootstrapの自信がない方
・とりあえず成果物と同じデザインのコードが欲しい方
下記のLine登録URLより最終コードをお受け取り頂けます。
第五章 Herokuへのデプロイ
ここまででFlaskを使用したローカルでの開発を行ってきました。
「もう、あとはデプロイだけだ!」と気を抜きたくなるところですが、
ここからもそこそこ時間がかかるので、もうちょっとだけ気合いをいれていきましょう。
Heokuへのデプロイに必要なステップ
簡単には下記の4ステップを行なっていきます。
- Herokuの登録と各種設定
- HerokuCLIの設定
- デプロイ前の準備
- Heokuデプロイ
1. Herokuの登録と各種設定
まずはWebアプリのデプロイ先であるHeorku上で各種設定を行なっていきます。
以下の各種ステップを行っていきましょう。
- Herokuアカウントの作成
- 支払い方法の追加
- アプリの作成
- HerokuPostgresの追加
1. Herokuアカウントの作成
まずは下記のリンクにアクセスしましょう。
Heroku: https://jp.heroku.com
リンクの「新規登録」からアカウントを作成→メール認証→パスワード設定の順で行います。
2. 支払い方法の追加
次に支払い方法の追加を行なっていきます。この設定を行わないとアプリを作らせてくれないので、無料枠を使用する場合でも必要な作業となります。
Herokuへのログイン
アカウント作成後すぐにログインすると下記の画面が表示されます。
2段階認証が必須となっているので、「Continue」を選択し、2段階認証の方法を設定しましょう。
2段階認証の設定が終わったら、「Logged In」と下記の画面が表示されるので、中央のHerokuロゴマークをクリックしてダッシュボードへ遷移します。
ダッシュボードへ遷移後、利用規約が表示されるので問題無ければ「Accept」を押してダッシュボードへ進みます。
ダッシュボードが表示されました。
支払い方法を追加する
今回は使用するサービスではお金を払う必要はありません。しかし、支払い方法を追加しなとサービスが使用できないので、まずは支払い方法を追加しましょう。
ダッシュボード画面左中央にある「Add payment method」をクリック後、下記の画面へ遷移します。
右端の「Add credit card」からクレジットカード情報を入力していきます。
無事正しく入力すれば、クレジットカード情報が表示されます。(赤枠部分)
アプリの作成
左上のHerokuロゴをクリックするなどしてダッシュボード画面を開き、画面中央にある「create new app」をクリックします。
ここで①アプリの名前と②サーバーを置く地域を選択します。
①に関しては、この後にも使用するものになるので、「flask-blog-hoshi」などわかりやすい名前にしておきましょう。※名前はSNSのIDのようにユニークでなければいけません
②はアメリカかヨーロッパが選択できます。もし日本が選択できれば日本にしますが、ここではどちらでもいいので「アメリカ」を選択します。
①と②を入力後、「Create app」を押下すると、アプリの作成が完了し下記の画面に遷移します。
HerokuPostgresの追加
赤枠のResourcesを選択
「Add-ons」に「postgres」と入力すると、「Heroku Postgres」(赤枠)が検索結果に表示されるので、「Heroku Postgres」をクリック。
クリックすると、どのような課金プランを使用するか表示されます。
学習に使用する分であれば、無料枠で十分ですので「Essential2 (Beta) – Free」を選択。※
※Plans&Pricing参照 https://elements.heroku.com/addons/heroku-postgresql
プランを選択後、beta版であることを理解した旨了承するチェックボックスにチェックを入れ「Submit Order Form」をクリックし、Heroku Postgresをサーバーへ追加します。
HerokuPostgresがサーバーへ追加できました。
2. Heroku CLIの設定
Webアプリのデプロイ先であるHeorku上の設定がほぼ終わったので、次にローカルでのHerokuの設定を行います。
まずはHerokuをローカルで操作するためのツール「Heroku CLI」をHomeBrewを使用してパソコンにインストールします。
▼「Heroku CLI」インストールコマンド
brew tap heroku/brew && brew install heroku
Zsh※参照 https://devcenter.heroku.com/ja/articles/heroku-cli#install-with-an-installer
インストールが完了したら、下記のコマンドでインストールができたか確認しましょう。
▼Heroku CLIバージョン確認コマンド
heroku --version
Zshインストールができていることが確認できたら、ブラウザとは別にHeroku CLIでHerokuにログインしていきます。
▼Herokuログインコマンド
heroku login
Zshコマンドを実行すると下記の表示が出ます。「任意のキーを入力してログインのためにブラウザを開く、もしくは’q’を入力して閉じる」という意味になりますので、enterなど適当なキーを押下しブラウザと連携してログインを進めます。
最近流行り(?)のブラウザ連携型ログインですので、ブラウザと連携してCLI上でのログインを進めていきます。
▼enter押下後ブラウザに表示されるログイン画面
ログインが完了すると、「Logged In」と表示されます。
ブラウザで「Logged In」と表示されれば、ターミナル上でも「Logged In as メールアドレス」と表示されており、Heroku CLIのログインが完了しています。
3. デプロイ前の準備
- Gunicornのインストール
- Herokuに必要なファイルの作成
- Flaskの修正
1. Gunicornのインストール
まずはじめに、FlaskのWebサーバーに使用するライブラリをインストールします。
▼poetryを使用したgunicornのインストール
poetry add gunicorn
Zsh2. Herokuに必要なファイルの作成
Herokuというサーバーでは、デプロイをする際に下記の3ファイルがプロジェクト配下に追加で必要となります。
- requirements.txt
- Procfile
runtime.txt
それぞれのファイルの解説と作成方法をみてきましょう。
1. requirements.txt
・requirements.txtの解説
外部のサーバーを借りてデプロイする際には、作成したWebアプリで使用するライブラリをそのサーバに教えてあげる必要があります。
そしてこのファイルは、これまでインストールしたPythonライブラリを全て記載するテキストデータとなります。
・requirements.txtの作成方法
requirements.txtはターミナル上でコマンドをプロジェクト配下で実行するだけで作成することができます。
▼Pythonを直接インストールしている方は、下記のコマンドを実行します。
pip freeze > requirements.txtpip freeze > requirements.txt
Zsh▼Poetryを使用している場合は、下記のコマンドを使用します。
poetry export -f requirements.txt > requirements.txt
Zshコマンドを実行すると以下のようにファイルが作成されますので、確認しましょう。
2. Procfile
・Procfileの解説
ProcfileはHerokuのようなPaaSで使用される、アプリケーションの起動を管理する設定ファイルです。
これがないとHerokuとしては、「Flaskファイルあるけど、だから何?」となってしまいます。
・Procfileの作成方法
Procfileはrequirements.txtのようにコマンドで生成するわけでなはなく、エディターやターミナルからpythonファイルを作成するように、「ファイル作成→”Procfile”という名前で保存」で作成します。
▼Procfileには以下のように記述します。
web: gunicorn app_name:app
Zsh※右端にZshと記載がありますが、実際はProcfileです。
それぞれコマンドの意味を解説します。
「web: 」
webサーバーの起動を表します。他にも「worker:」という、バックグラウンドでpythonファイルを実行する時のコマンドなどもあります。
「gunicorn」
WebサーバーにGunicornを使用することを表します。使用したいWebサーバーに合わせて変更します。
「app_name:app」
app_nameの部分に実行したいpythonファイルの名前を記載します。今回のチュートリアルではmyappというファイルでFlaskを起動するので以下のように記載します。
app部分には、myappで定義しているFlaskをインスタンス化したオブジェクトの変数を記載します。
web: gunicorn myapp:app
Zsh3. runtime.txt
・「runtime.txt」の解説
Herokuに使用して欲しいPythonのバージョンを知らせるためのファイルです。
参考: https://devcenter.heroku.com/ja/articles/python-runtimes
・「runtime.txt」の作成方法
Procfile同様、エディターやターミナルから「ファイル作成→”runtime.txt”という名前で保存」で作成します。中身は「python-バージョン」を記載するだけでOK。
▼「runtime.txt」
python-3.9.19
Zsh以上で3.デプロイ前の準備の1.Herokuに必要なファイルの作成は終了です。
- Herokuに必要なファイルの作成
- Flaskの修正
3. Flaskの修正
デプロイ前準備の最後にFlaskのコードを本番環境(Heroku環境)に対応させる必要があります。
変更内容は以下の2点です。
- DB情報
- Secret key情報
①DB情報の変更
PostgreSQLが参照するDBの情報をHeroku用に変更する必要があります。
まずは、変更点をみていきましょう。
▼変更前(app.py)
SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg://{user}:{password}@{host}/{name}'.format(**{
'user': 'postgres',
'password': 'postgrespass',
'host': 'localhost',
'name': 'blog_db'
})
app.config["SQLALCHEMY_DATABASE_URI"] = SQLALCHEMY_DATABASE_URI
Python▼変更後(app.py)
if app.debug:
SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg://{user}:{password}@{host}/{name}'.format(**{
'user': 'postgres',
'password': 'postgrespass',
'host': 'localhost',
'name': 'db2'
})
else:
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL').replace("postgres://", "postgresql+psycopg://")
app.config["SQLALCHEMY_DATABASE_URI"] = SQLALCHEMY_DATABASE_URI
Pythonぱっと見大きく変わったように見えるのますが、大きく変わったのは2点だけです。
- debugモードとdebugモードでないときの起動方法でDB情報の参照内容を変更する
- HerokuのPostgreSQL情報を参照
大きく分けるとこの2点となります。
それぞれ解説していきます。
①debugモードとdebugモードでないときの起動方法でDB情報の参照内容を変更する
これまでローカルで開発していた時は以下のコマンドでローカルサーバを立ち上げてきました。
flask --app app_name run --debug
Zshこのコマンドの末尾に「–debug」というオプションがありますが、このオプションをつけることで、flaskインスタンス変数appのdebugというメソッドの値がTrueになります。
それを利用することで、app.debugがTrueの時はローカルのDB情報を参照し、そうでないときはHerokuのDB情報を読み込む、という条件分岐を行うことができます。
②HerokuのPostgreSQL情報を参照
▼HerokuのPostgreSQL参照コード部分
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL').replace("postgres://", "postgresql+psycopg://")
PythonHerokuのPostgreSQLと接続するために必要なURI情報は全てHerokuの環境変数「DATABASE_URL」に登録されています。
▼Herokuの環境変数の確認方法は以下の通りです。
①ダッシュボードから「Settings」をクリック
②「Reveal Config Vars」をクリック
③環境変数名と環境変数の内容が表示されました。
DATABASE_URLで取得できるURLは直接SQLAlchemyのURLでは使用できません。※1
そのため、.replaceを使用し、「postgres://」の部分を「postgresql+psycopg://」へ変換します。
②Serekey情報の変更
最後にFlaskがセッションやクッキーのデータを暗号化するために使用する秘密鍵を本番環境に合わせて変更します。
まずは、変更点をみていきましょう。
▼変更前(app.py)
app.config["SECRET_KEY"] = os.urandom(24)
Python▼変更後(app.py)
if app.debug:
app.config["SECRET_KEY"] = os.urandom(24)
else:
app.config["SECRET_KEY"] = os.environ.get('SECRET_KEY')
Python基本的にはDBの情報変更と同様に、以下の2点が違いとなります。
①デバッグモードと本番モードで処理を分ける
②本番環境での秘密鍵(SECRET_KEY)はサーバーの環境変数を引用する
②に関しては、DBの情報変更とは異なりユーザーが自ら環境変数を設定します。
DBの情報確認時と同様に「Dashboard>Settings>Config Vars」より「Reveal Config Vars」をクリックします。
赤枠のKEY部分に「SECRET_KEY」、VALUEに自分で決めた秘密鍵を入力します。
入力後に「Add」をクリックすれば、環境変数の登録完了です。
4. Heokuデプロイ
お待たせしました。ここで最後のステップHerokuへのデプロイを行なっていきます。
あと少しですので、残り頑張っていきましょう。
Webアプリの完成には残り下記の2ステップを行うだけになります。
- コードをHerokuへデプロイ
- Heroku内PostgreSQL上にテーブルを作成
それでは残り少しやっていきましょう。
1. コードをHerokuへデプロイ
これまでに準備をしてきたFlaskファイルやHerokuに必要なファイルをHerokuへアップロードしていきます。
Herokuというサービスは少し変わったやり方でアップロードしていきますので、その手順3ステップを解説していきます。
- ローカルレポジトリの作成
- アプリをリモートレポジトリに登録
- herokuへpush
①ローカルレポジトリの作成とコミット
Herokuではgitを使用し(×github)、heroku内のgitレポジトリにpushする形でデプロイを行います。
そのため、githubやgitlabなどにcommitする手順同様に下記のコマンドをプロジェクト配下で実行していきます。
▼ローカルレポジトリの作成と初期化
git init
Zsh▼ファイルを追加
git add .
Zsh▼ファイルをコミット
git commit -m "first commit"
Zsh②アプリをリモートレポジトリに登録
ここで、githubなどのサービスではなくheroku内のレポジトリをリモートレポジトリとして指定し登録します。コマンドはgitではなくherokuコマンドを使用します。
heroku git:remote -a example-app
Zsh「example-app」の部分は先ほどHeroku上で作成したアプリの名前に変えましょう。
③herokuへpush
それではいよいよHerokuへアップロード(デプロイ)していきます。
git push heroku main
Zshこれで一旦デプロイは終了となります。
2. Heroku内PostgreSQL上にテーブルを作成
「やっとデプロイ終わった!」と一服したいところですが、WEBアプリをブラウザ上で動かせるようにするには最後この手順が必要となります。
ローカルで開発している際にも行なった「DBテーブル作成」の作業です。
同じ作業をデプロイするサーバーでも行っていきます。
テーブル作成方法
やり方はほぼローカルでのテーブル作成方法と同じです。
①Herokuサーバー内でPythonシェルを開く
heroku run python
Zsh②ローカル同様テーブル作成コマンドを実行
>from myapp import app, db
>with app.app_context():
> db.create_all()
>
Zsh実行ができれば、「exit()」コマンドでshellを閉じれば終了です。
最後にアプリケーションがうまく動作するか確認してみましょう!
最後に
いかがだったでしょうか?
無事に、アプリをHerokuへデプロイしブラウザ上で動作させることはできましたか?
・記事のはじめにあった成果物のようなアプリを作りたい
・Flaskの使い方に関するさらなる解説が欲しい
・エンジニア転職や勉強方法を詳しく知りたい!
という方は、下記のリンクより、私のLineへ登録して特典をお受け取りください。
・Flaskアプリの完成コード
・動画中に使用したFlask解説スライド
・チュートリアル中によくあるエラーの解決方法
・Pythonでの転職や学習方法についての電子書籍
などなどLine登録者限定で配信しております。
Line登録のリンク▶︎https://utage-system.com/line/open/QAoW5PZjZAHi