90
98

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Webアプリ開発未経験者がFlaskとSQLiteとHerokuを使って1週間でサービス公開までする

Last updated at Posted at 2019-06-29

初めに

この記事はWebアプリ開発未経験の筆者が1週間でWebアプリを開発してサービスを公開するまでにやったことのまとめです。MacOSでの開発を対象としています。実際にやりながら書いていったこちらの記事を校正したものになります。

この記事の想定読者層

筆者のスペックは

  • 応用情報を持っているので「HTTP通信」とか「サーバ」とか「データベース」とかの存在は知っているが、詳細も使い方も全く分からない
  • 競技プログラミングをちょっとだけやっているのでロジック部分の実装はちょっと得意
  • 仕事はエンジニアとは全然関係無いことをやっているし、当然個人で何かを開発したことも無い

という感じです。

恐らく「ProgateとかでPythonとかHTMLのチュートリアルはやったことがあって多少は書けるし、データベースとかサーバとかとうまいことちょちょっとやればWebアプリとか作れるんだろうなぁ、と思っているけど、いざ実際に何か作ってみようと思っても何をすればいいのか全く分からない」というような人にドンピシャな記事だと思います(まさに私がそれだったので)。

使用技術とゴール

今回主に使用する技術/サービスは

  • Flask(Pythonの軽量Webアプリ開発フレームワーク)
  • SQLite(軽量のリレーショナルデータベース)
  • Heroku(Webアプリ公開用プラットフォームサービス)

の3つです。これらは記事の中で順を追って解説するので、今の段階では何も分からなくて大丈夫です。

最終的にはこのような、神社にお願い事を投稿できるようなWebアプリを作ります。題材としてはしょーもないですが、その過程で、ユーザのログイン処理であったり、ログイン後のセッション管理であったり、パスワードや通信の暗号化であったり等、Webアプリを開発する上で欠かせない要素も盛り込んでいます。
スクリーンショット 2019-06-29 13.41.53.png
この記事を読んで「あれ、自分でも意外とすぐWebアプリ作れそうじゃん!」と思ってくれる人が少しでも増えると嬉しいです。

Day1

事前インストール作業

pythonのインストール

python公式ページから最新のpythonを落としてインストールしましょう。

Flaskモジュールのインストール

以下のコマンドをターミナルで叩いて、Flaskモジュールをインストールしましょう。

pip3 install Flask

エディタ(PyCharm)のインストール

PyCharm公式ページから最新のPyCharmを落としてきましょう。Professional版とCommunity版がありますが、無料のCommunity版で十分です。

HTMLだけのシンプルなWebページの作成

ディレクトリ基本構成

任意のディレクトリ以下にこのような構成のフォルダ/ファイルを作成します。ターミナルでmkdirとtouchを使いながらでも、PyCharm上で作成しても、どちらでも構いません。

(any directory)
 ├app/
 │ ├templates/
 │ ├static/
 │ └app.py
 └run.py

templates/

HTMLファイルを格納する場所です。
今回はここに以下のindex.htmlファイルを作成しましょう。

index.html
<!DOCTYPE html>
<html>
    <head>
    </head>
    <body>
    <h1>Webアプリ初心者のFlaskチュートリアル</h1>
    </body>
</html>

static/

HTMLファイル以外の、CSSファイル・JSファイル・画像ファイル等を格納する場所です。
今回はHTMLだけのシンプルなWebページなので、ここには何も配置しません。

app.py

アプリロジックを書くファイルです。リクエストされたURLに応じてどのHTMLファイルを返すかを指定します。
今回は中身を以下のように記載しましょう。

app.py
#Flaskとrender_template(HTMLを表示させるための関数)をインポート
from flask import Flask,render_template

#Flaskオブジェクトの生成
app = Flask(__name__)


#「/」へアクセスがあった場合に、"Hello World"の文字列を返す
@app.route("/")
def hello():
    return "Hello World"


#「/index」へアクセスがあった場合に、「index.html」を返す
@app.route("/index")
def index():
    return render_template("index.html")


#app.pyをターミナルから直接呼び出した時だけ、app.run()を実行する
if __name__ == "__main__":
    app.run(debug=True)

run.py

Webサーバを立ち上げる際に実行するファイルです。
中身を以下のように記載しましょう。

run.py
from app.app import app

if __name__ == "__main__":
    app.run()

Webサーバを立てる

ターミナルを開いて、カレントディレクトリをapp/とrun.pyのある階層(any directoryの場所)に移動させましょう。
以下のコマンドをターミナルで実行するとWebサーバが起動します。

python3 run.py

するとターミナルに以下のようなメッセージが出るので、

* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

ブラウザでhttp://127.0.0.1:5000/を開いてみましょう。
「Hello World」と表示されていればOKです。
スクリーンショット 2019-06-22 2.52.50.png
もう一つ、http://127.0.0.1:5000/indexを開いてみましょう。
「Webアプリ初心者のFlaskチュートリアル」と表示されていればOKです。
スクリーンショット 2019-06-22 2.53.40.png

Day2

Webページに画像を表示する

現在のディレクトリ構成

Day1終了時点ではこんな感じになっているはずです。

(any directory)
 ├app/
 │ ├templates/
 │ │ └index.html
 │ ├static/
 │ └app.py
 └run.py

画像の格納

staticフォルダ内にimagesフォルダを作成し、その中に任意の画像を格納します。
私は適当に鳥居の画像を取ってきて、torii.jpgという名称で格納しました。

(any directory)
 ├app/
 │ ├templates/
 │ │ └index.html
 │ ├static/
 │ │ └images/
 │ │  └torii.jpg
 │ └app.py
 └run.py

index.htmlの変更

imgタグを追加して、src属性に画像へのパスを指定します。
index.htmlを以下のように修正しましょう。(※ついでにh1タグのテキストを「神社」に変えました。)

index.html
<!DOCTYPE html>
<html>
    <head>
    </head>
    <body>
    <!-- 以下を修正 -->
    <h1>神社</h1>
    <img src="/static/images/torii.jpg" alt="鳥居">
    <!-- 修正終わり -->
    </body>
</html>

表示確認

あとはDay1同様、run.pyを走らせてhttp://127.0.0.1:5000/indexを開いて画像が表示されていればOKです。
スクリーンショット 2019-06-22 12.21.24.png

クエリストリングを受け取ってhtmlに送る

クエリストリングを受け取ってWebページのタイトルに表示したいと思います。
クエリストリングとは、URLの後ろによくくっついている?key=valueみたいなもののことです。
ここの情報を利用することで、例えばユーザによって表示させる名前を変えるなど、htmlを動的に生成することができるようになります。

app.pyにクエリストリングを受け取る機構を用意

requestモジュールをインポートして、request.args.get(paramater)で、クエリストリングを受け取ることができます。
また、受け取ったクエリストリングをrender_template()の引数に入れることで、html側に送ることができます。
以下のようにapp.pyを変えましょう。

app.py
#インポートするライブラリに「render_tmplate」と「request」を追加
from flask import Flask,render_template,request

app = Flask(__name__)


#ルーティングを統一
@app.route("/")
@app.route("/index")
def index():
    name = request.args.get("name")  #クエリストリングからname属性の値を受け取る
    return render_template("index.html",name=name)  #index.htmlにnameの情報を送ってWebページを表示させる


if __name__ == "__main__":
    app.run(debug=True)

index.htmlに変数を表示する機構を用意

html内で変数を表示するには{{ var }}を埋め込めばOKです。
今回はnameという変数をタイトルに表示させるので、index.htmlを以下のように変えましょう。

index.html
<!DOCTYPE html>
<html>
    <head>
        <!-- 以下を追加 -->
        <title>{{name}}</title>
        <!-- 追加終わり -->
    </head>
    <body>
    <h1>神社</h1>
    <img src="/static/images/torii.jpg" alt="鳥居">
    </body>
</html>

表示確認

あとはDay1同様、run.pyを走らせましょう。
http://127.0.0.1:5000/indexを開くと、タイトル部分がNoneになっているはずです。
スクリーンショット 2019-06-22 12.56.20.png
これにクエリストリングを追加してみましょう。
例えば、http://127.0.0.1:5000/index?name=kiyokiyoとして開くと、タイトル部分がkiyokiyoと表示されるはずです。
スクリーンショット 2019-06-22 12.59.05.png

html内でif文を書く

html側に渡した変数の値によってWebページに表示する要素を変えます。
クエリストリングのnameパラメータの値に応じて、
・kiyokiyoの場合は「スペシャルkiyokiyo神社」
・それ以外(○○)の場合は「○○神社」
・そもそもnameのクエリストリングが無い場合は「ただの神社」
と表示させたいと思います。

index.htmlにif文を書く

htmlファイル内では{% %}ブロック内にpythonコードを埋め込んであげればOKです。
index.htmlを以下のように変更しましょう。

index.html
<!DOCTYPE html>
<html>
    <head>
        <title>{{name}}</title>
    </head>
    <body>
    <!-- 以下を追加 -->
    {% if name == "kiyokiyo" %}
    <h1>スペシャル{{name}}神社</h1>
    {% elif name %}
    <h1>{{name}}神社</h1>
    {% else %}
    <h1>ただの神社</h1>
    {% endif %}
    <!-- 追加終わり -->
    <img src="/static/images/torii.jpg" alt="鳥居">
    </body>
</html>

表示確認

あとはいつも通り、run.pyを走らせましょう。
クエリストリングを変更することで文字の表示が変わると思います。
http://127.0.0.1:5000/indexの場合↓
スクリーンショット 2019-06-22 13.22.01.png
http://127.0.0.1:5000/index?name=kiyoの場合↓
スクリーンショット 2019-06-22 13.22.31.png
http://127.0.0.1:5000/index?name=kiyokiyoの場合↓
スクリーンショット 2019-06-22 13.23.02.png

html内でfor文を書く

html側に渡したリストの全要素を、for文を使ってWebページに表示させます。
般若心教の「色不異空、空不異色、色即是空、空即是色」を表示させたいと思います。

app.py内でリストを定義

app.py内で般若心教のリストokyoを定義します。
そしてそのリストをrender_template()の引数に加え、html側に送ります。
以下のように記載しましょう。

app.py
from flask import Flask,render_template,request

app = Flask(__name__)


@app.route("/")
@app.route("/index")
def index():
    name = request.args.get("name")
    okyo = ["色不異空","空不異色","色即是空","空即是色"]  #okyoに般若心教を定義
    return render_template("index.html",name=name,okyo=okyo)  #okyoもhtml側に送る


if __name__ == "__main__":
    app.run(debug=True)

html内でfor文を書く

for文もif文と同様、{% %}ブロック内にコードを書いていきます。
以下のように記載しましょう。

index.html
<!DOCTYPE html>
<html>
    <head>
        <title>{{name}}</title>
    </head>
    <body>
    {% if name == "kiyokiyo" %}
    <h1>スペシャル{{name}}神社</h1>
    {% elif name %}
    <h1>{{name}}神社</h1>
    {% else %}
    <h1>ただの神社</h1>
    {% endif %}
    <img src="/static/images/torii.jpg" alt="鳥居">
    <!-- 以下を追加 -->
    {% for word in okyo %}
    <p>{{word}}</p>
    {% endfor %}
    <!-- 追加終わり -->
    </body>
</html>

表示確認

あとはいつも通り、run.pyを走らせましょう。
http://127.0.0.1:5000/indexを開くと般若心教が表示されているはずです。
スクリーンショット 2019-06-22 13.35.46.png

Day3

POSTリクエストを受け取る

Webページのフォームから送信されたPOSTリクエストを受け取って処理を実行し、htmlを返します。

htmlにフォームを追加する

テキストと送信ボタンのみの簡単なフォームをindex.htmlに追加します。
formタグのaction属性で、フォームを送信する対象のURLを指定しています。
以下のようにindex.htmlを変更しましょう。

index.html
<!DOCTYPE html>
<html>
    <head>
        <title>{{name}}</title>
    </head>
    <body>
    {% if name == "kiyokiyo" %}
    <h1>スペシャル{{name}}神社</h1>
    {% elif name %}
    <h1>{{name}}神社</h1>
    {% else %}
    <h1>ただの神社</h1>
    {% endif %}
    <!-- 以下を追加 -->
    <form action="/index" method="POST">
        <input type="text" name="name" placeholder="Enter name">
        <input type="submit" value="Submit">
    </form>
    <!-- 追加終わり -->
    <img src="/static/images/torii.jpg" alt="鳥居">
    {% for word in okyo %}
    <p>{{word}}</p>
    {% endfor %}
    </body>
</html>

app.pyにPOSTリクエストを受け取る機構を用意

ルーティングに/indexにPOSTリクエストが来た場合の処理を追加します。
普通にURLを叩いた場合(GETリクエスト)はURLのみの指定で問題ないのですが、フォームからのPOSTの場合はmethods=["post"]という風にリクエストのメソッドを指定してあげる必要があります。
処理内では、フォームのテキスト要素を取得し、nameとしてhtml側に値を渡します。
以下のようにapp.pyを変更しましょう。

app.py
from flask import Flask,render_template,request

app = Flask(__name__)


@app.route("/")
@app.route("/index")
def index():
    name = request.args.get("name")
    okyo = ["色不異空","空不異色","色即是空","空即是色"]
    return render_template("index.html",name=name,okyo=okyo)


#以下を追加
@app.route("/index",methods=["post"])
def post():
    name = request.form["name"]
    okyo = ["色不異空", "空不異色", "色即是空", "空即是色"]
    return render_template("index.html", name=name, okyo=okyo)
#追加終わり


if __name__ == "__main__":
    app.run(debug=True)

表示確認

あとはいつも通り、run.pyを走らせてhttp://127.0.0.1:5000/indexにアクセスしましょう。
フォームに文字を入力してSubmitボタンを押すと、Webページの表示が変わるはずです。
スクリーンショット 2019-06-23 12.05.01.png

Day4

SQLiteをセットアップする

いよいよSQLiteを使ってデータベースにデータを格納したり、データを取り出したり、といったことをしてみたいと思います。
こちらのサイトをかなり参考にさせて頂きました。
現段階でのファイル構成はこんな感じです。

(any directory)
 ├app/
 │ ├templates/
 │ │ └index.html
 │ ├static/
 │ │ └images/
 │ │  └torii.jpg
 │ └app.py
 └run.py

ディレクトリの追加

(any directory)以下にmodelsフォルダを作成し、その中に__init__.pydatabase.pymodels.pyの3ファイルを作成します。__init__.py以外はフォルダ/ファイル名は任意で大丈夫ですが、変更した場合はそれに合わせて以降に記載するコードの対象箇所も変更しましょう。
ディレクトリ追加後のファイル構成はこんな感じです。

(any directory)
 ├app/
 │ ├templates/
 │ │ └index.html
 │ ├static/
 │ │ └images/
 │ │  └torii.jpg
 │ └app.py
 ├models/
 │ ├__init__.py
 │ ├models.py
 │ └database.py
 └run.py

__init__.py

中身は空のままで大丈夫です。
モジュールとして呼び出す際に__init__.pyという名前のファイルが必要なため作るそうです。

models.py

テーブルのカラム情報を定義するためのクラスを格納します。
テーブル操作を行う際のレコード生成もこのクラスを通して行います。
今回は神社に対しての「お願い」を格納するためのテーブルを作成したいと思います。
カラム構成は

  • ID(int:キー情報)
  • title(String(128):お願いのタイトル)
  • body(text:お願いの内容)
  • date(datetime:お願いの投稿日時)

にします。
models.pyを以下のように記載しましょう。

models.py
from sqlalchemy import Column, Integer, String, Text, DateTime
from models.database import Base
from datetime import datetime


class OnegaiContent(Base):
    __tablename__ = 'onegaicontents'
    id = Column(Integer, primary_key=True)
    title = Column(String(128), unique=True)
    body = Column(Text)
    date = Column(DateTime, default=datetime.now())

    def __init__(self, title=None, body=None, date=None):
        self.title = title
        self.body = body
        self.date = date

    def __repr__(self):
        return '<Title %r>' % (self.title)

はじめ3行で必要なライブラリをインポートしています。
今回SQLの定義をするためにsqlalchemyモジュールを利用しています。初出なので、ターミナルで

pip3 install sqlalchemy

と打って、sqlalchemyモジュールをインストールしましょう。

class OnegaiContent(Base)以降はカラム情報の定義を行なっています。
テーブル名と、カラム別にカラム名と型を指定しています。また、オプションで主キーとするか、ユニークとするか、デフォルト値をどうするか、といった情報も指定することができます。
なお、ここで引数に渡しているBaseは、次のdatabase.pyで作るインスタンスなので後で説明します。

database.py

DBとの直接的な接続の情報を格納します。
以下のように記載しましょう。

database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
import os

databese_file = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'onegai.db')
engine = create_engine('sqlite:///' + databese_file, convert_unicode=True)
db_session = scoped_session(sessionmaker(autocommit=False,autoflush=False,bind=engine))
Base = declarative_base()
Base.query = db_session.query_property()


def init_db():
    import models.models
    Base.metadata.create_all(bind=engine)

はじめ4行で必要なライブラリをインポートしています。

次の5行でDBへの接続情報を定義しています。
上から順に、

  1. database.pyと同じパスにonegai.dbというファイルを絶対パスで定義
  2. SQLiteを利用して1.で定義した絶対パスにDBを構築
  3. DB接続用インスタンスを生成
  4. Baseオブジェクトを生成して、
  5. そこにDBの情報を流し込む

というイメージです。
ここで生成されたBaseオブジェクトを前述のOnegaiContentクラスの引数に渡すことで、DB接続情報とテーブル定義を紐付けることができます。逆に言うと、DB接続情報(接続先のDB等)を変えたい場合はdatabase.pyを、テーブル定義を変えたい場合はmodels.pyをいじればOKということです。

また、最後3行でDB初期化のための関数を定義しています。
import文でDB初期化対象のテーブル定義を指定して、Base.metadata.create_all(bind=engine)でテーブルを作成しています。

DB初期化

コードの準備は整ったので、早速DB作成してみましょう。
PyCharmの「Python Console」に以下のコードを入力しましょう。
しばらく経つと、models/フォルダ以下にonegai.dbファイルが作成されるはずです。

>>> from models.database import init_db
>>> init_db()

サンプルデータのINSERT

このままだとDBの中身が空っぽなので、サンプルデータを投入してみましょう。
将来的にはapp.pyから入れたいですが、今回は一旦「Python Console」から直入れしましょう。
以下のコードを入力しましょう(OnegaiContentの中身は正直なんでもいいです)。

>>> from models.database import db_session
>>> from models.models import OnegaiContent
>>> c1 = OnegaiContent("お願いします","5000兆円ください")
>>> c2 = OnegaiContent("助けてください","ぽんぽんぺいん")
>>> c3 = OnegaiContent("許してください","なんでもしますから")
>>> db_session.add(c1)
>>> db_session.add(c2)
>>> db_session.add(c3)
>>> db_session.commit()

DBに正しくINSERTできたかはコンソールから確認できます。
カレントディレクトリをonegai.dbのあるディレクトリにして、

sqlite3 onegai.db

と叩くと、SQLiteに入れます。
そこで、

Select * from onegaicontents;

を叩くと、以下のように、onegaicontentsテーブルの中身が全て表示されるはずです。

1|お願いします|5000兆円ください|2019-06-24 07:46:24.821779
2|助けてください|ぽんぽんぺいん|2019-06-24 07:46:24.821779
3|許してください|なんでもしますから|2019-06-24 07:46:24.821779

DBからレコードを取得してWebページに表示させる

セットアップしたSQLiteのDBのonegaicontentsテーブルからレコードを全件引っ張ってきて、Webページに表示させます。
今まで般若心境をベタ書きしていた部分を、DBから受け取った値に置き換えたいと思います。

app.pyにテーブルからデータを受け取る機構を用意

テーブルからのデータの受け取りはmodels.pyで定義したクラス(今回はOnegaiContentクラス)を介して行います。
なので、まずOnegaiContentクラスをインポートして、そのクラスから参照クエリ用の関数を呼び出します。.query.all()でテーブル内のデータを全件取得できます(SELECT * FROM xx;に相当)。
以下のようにapp.pyを変更しましょう(中身がお経からお願いに変わったので、変数名とかも変えています)。

app.py
from flask import Flask,render_template,request
#下記のインポート文を追加
from models.models import OnegaiContent

app = Flask(__name__)


@app.route("/")
@app.route("/index")
def index():
    name = request.args.get("name")
    #以下を変更
    all_onegai = OnegaiContent.query.all()
    return render_template("index.html",name=name,all_onegai=all_onegai)
    #変更終わり


@app.route("/index",methods=["post"])
def post():
    name = request.form["name"]
    #ここも変更
    all_onegai = OnegaiContent.query.all()
    return render_template("index.html", name=name, all_onegai=all_onegai)
    #変更終わり


if __name__ == "__main__":
    app.run(debug=True)

html側の変更

引き渡す際の変数名とか、表示させたい情報とか色々変わったので、html側も変更します。
全お願いをタイトル:内容(作成日時)というフォーマットで表示させます。
以下のようにindex.htmlを変更しましょう。

index.html
<!DOCTYPE html>
<html>
    <head>
        <title>{{name}}</title>
    </head>
    <body>
    {% if name == "kiyokiyo" %}
    <h1>スペシャル{{name}}神社</h1>
    {% elif name %}
    <h1>{{name}}神社</h1>
    {% else %}
    <h1>ただの神社</h1>
    {% endif %}
    <form action="/index" method="POST">
        <input type="text" name="name" placeholder="Enter name">
        <input type="submit" value="Submit">
    </form>
    <img src="/static/images/torii.jpg" alt="鳥居">
    <!-- 以下を変更 -->
    {% for onegai in all_onegai %}
    <p>{{onegai.title}}:{{onegai.body}}({{onegai.date}})</p>
    {% endfor %}
    <!-- 変更終わり -->
    </body>
</html>

表示確認

あとはいつも通り、run.pyを走らせてhttp://127.0.0.1:5000/indexにアクセスしましょう。
般若心教の代わりに「サンプルデータのINSERT」で作成したお願い一覧が表示されていればOKです。
スクリーンショット 2019-06-24 10.38.02.png

Day5

レコードの追加

フォームに入力されたお願いのタイトルと中身を受け取ってテーブルにレコード追加をします。

お願い追加用のフォームを設置

htmlにtext2つとsubmit1つの簡単なフォームを追加します。
フォームのaction属性は「/add」に指定しておきます。
以下のようにindex.htmlを変更しましょう。

index.html
<!DOCTYPE html>
<html>
    <head>
        <title>{{name}}</title>
    </head>
    <body>
    {% if name == "kiyokiyo" %}
    <h1>スペシャル{{name}}神社</h1>
    {% elif name %}
    <h1>{{name}}神社</h1>
    {% else %}
    <h1>ただの神社</h1>
    {% endif %}
    <form action="/index" method="POST">
        <input type="text" name="name" placeholder="Enter name">
        <input type="submit" value="Submit">
    </form>
    <img src="/static/images/torii.jpg" alt="鳥居">
    <!-- 以下を追加 -->
    <form action="/add" method="post">
        <input type="text" name="title" placeholder="title">
        <input type="text" name="body" placeholder="body">
        <input type="submit" value="Add">
    </form>
    <!-- 追加終わり -->
    {% for onegai in all_onegai %}
    <p>{{onegai.title}}:{{onegai.body}}({{onegai.date}})</p>
    {% endfor %}
    </body>
</html>

フォームの値を受け取ってINSERT処理をする

フォームの値を受け取ってDBにレコード追加するための関数をapp.pyに作成したいと思います。
フォームのaction属性で「/add」と指定したので、@app.route("/add",methods=["post"])でルーティングを追加します。
INSERT処理はdb_session.add(content)関数にmodelsオブジェクト(今回はOnegaiContent)を引数として渡すことで実行できます。この関数を使用するために、models.databaseからdb_sessionをインポートします。
また、OnegaiContentオブジェクトを生成する際にタイムスタンプが必要になるので、from datetime import datetimeを宣言して、datetime.now()を使えるようにしておきます。
以下のようにインポート文とルーティング/関数をapp.pyに追加しましょう。

app.py
from flask import Flask,render_template,request
from models.models import OnegaiContent
#以下を追加
from models.database import db_session
from datetime import datetime
#追加終わり

app = Flask(__name__)


@app.route("/")
@app.route("/index")
def index():
    name = request.args.get("name")
    all_onegai = OnegaiContent.query.all()
    return render_template("index.html",name=name,all_onegai=all_onegai)


@app.route("/index",methods=["post"])
def post():
    name = request.form["name"]
    all_onegai = OnegaiContent.query.all()
    return render_template("index.html", name=name, all_onegai=all_onegai)


#以下を追加
@app.route("/add",methods=["post"])
def add():
    title = request.form["title"]
    body = request.form["body"]
    content = OnegaiContent(title,body,datetime.now())
    db_session.add(content)
    db_session.commit()
    return index()
#追加終わり


if __name__ == "__main__":
    app.run(debug=True)

表示確認

あとはいつも通り、run.pyを走らせてhttp://127.0.0.1:5000/indexにアクセスしましょう。
中央下部のフォームにタイトルと内容を入力して「Add」ボタンを押すと、お願いが追加されるはずです。
スクリーンショット 2019-06-24 22.04.44.png

レコードの更新

変更したいお願いを選択し、変更内容を入力/送信すると、DBが更新され、表示が変わるようにしたいと思います。

お願い変更用のフォームを設置

追加と同様、text2つ、submit1つのシンプルなフォームを用意します。
それに加え、今回は変更するレコードを指定するため、今までpタグで表示していたお願い一覧を、inputタグのラジオボタンに変更します。
以下のようにindex.htmlを変更しましょう。

index.html
<!DOCTYPE html>
<html>
    <head>
        <title>{{name}}</title>
    </head>
    <body>
    {% if name == "kiyokiyo" %}
    <h1>スペシャル{{name}}神社</h1>
    {% elif name %}
    <h1>{{name}}神社</h1>
    {% else %}
    <h1>ただの神社</h1>
    {% endif %}
    <form action="/index" method="POST">
        <input type="text" name="name" placeholder="Enter name">
        <input type="submit" value="Submit">
    </form>
    <img src="/static/images/torii.jpg" alt="鳥居">
    <form action="/add" method="post">
        <input type="text" name="title" placeholder="title">
        <input type="text" name="body" placeholder="body">
        <input type="submit" value="Add">
    </form>
    <!-- 以下を追加/変更 -->
    <form action="/update" method="post">
        <input type="text" name="title" placeholder="title">
        <input type="text" name="body" placeholder="body">
        <input type="submit" value="Update">
        {% for onegai in all_onegai %}
        <div><input type="radio" name="update" value={{onegai.id}}>{{onegai.title}}:{{onegai.body}}({{onegai.date}})</input></div>
        {% endfor %}
    </form>
    <!-- 追加/変更終わり -->
    </body>
</html>

フォームの値を受け取ってUPDATE処理をする

フォームの値を受け取ってDBのレコードを更新するための関数をapp.pyに作成したいと思います。
フォームのaction属性で「/update」と指定したので、@app.route("/update",methods=["post"])でルーティングを追加します。
UPDATE処理はmodelsオブジェクト(今回はOnegaiContent)を呼び出して、それを直接変更することで実行できます。今回は変更をかけるレコードを.query.filter_by(id=request.form["update"])で絞り込んでいます。
以下のようなルーティング/関数をapp.pyに追加しましょう。

app.py
from flask import Flask,render_template,request
from models.models import OnegaiContent
from models.database import db_session
from datetime import datetime

app = Flask(__name__)


@app.route("/")
@app.route("/index")
def index():
    name = request.args.get("name")
    all_onegai = OnegaiContent.query.all()
    return render_template("index.html",name=name,all_onegai=all_onegai)


@app.route("/index",methods=["post"])
def post():
    name = request.form["name"]
    all_onegai = OnegaiContent.query.all()
    return render_template("index.html", name=name, all_onegai=all_onegai)


@app.route("/add",methods=["post"])
def add():
    title = request.form["title"]
    body = request.form["body"]
    content = OnegaiContent(title,body,datetime.now())
    db_session.add(content)
    db_session.commit()
    return index()


#以下を追加
@app.route("/update",methods=["post"])
def update():
    content = OnegaiContent.query.filter_by(id=request.form["update"]).first()
    content.title = request.form["title"]
    content.body = request.form["body"]
    db_session.commit()
    return index()
#追加終わり


if __name__ == "__main__":
    app.run(debug=True)

表示確認

あとはいつも通り、run.pyを走らせてhttp://127.0.0.1:5000/indexにアクセスしましょう。
対象のお願いを選択してフォームにタイトルと内容を入力したのち「Update」ボタンを押すと、お願いが変更されるはずです(タイトルに一意制約をかけているため、重複するタイトルを入力するとエラーになってしまいますので、そこだけ注意)。
スクリーンショット 2019-06-24 23.19.36.png

レコードの削除

チェックボックスでレコードを指定し、削除ボタンで指定された全てのレコードが削除されるようにしたいと思います。

チェックボックスを設置

削除対象のレコードを指定するためのチェックボックスをラジオボタンの横に追加します。
ただ、そのままチェックボックスを追加しようとするとフォームが入れ子構造になってうまく表示されなくなってしまうため、formタグを外に出して、formタグのid属性と、inputタグのform属性で紐付けるスタイルを採用します。
以下のようにindex.htmlを変更しましょう。

index.html
<!DOCTYPE html>
<html>
    <head>
        <title>{{name}}</title>
    </head>
    <body>
    {% if name == "kiyokiyo" %}
    <h1>スペシャル{{name}}神社</h1>
    {% elif name %}
    <h1>{{name}}神社</h1>
    {% else %}
    <h1>ただの神社</h1>
    {% endif %}
    <form action="/index" method="POST">
        <input type="text" name="name" placeholder="Enter name">
        <input type="submit" value="Submit">
    </form>
    <img src="/static/images/torii.jpg" alt="鳥居">
    <form action="/add" method="post">
        <input type="text" name="title" placeholder="title">
        <input type="text" name="body" placeholder="body">
        <input type="submit" value="Add">
    </form>
    <!-- 以下を変更 -->
    <form action="/update" method="post" id="update">
        <input type="text" name="title" placeholder="title">
        <input type="text" name="body" placeholder="body">
        <input type="submit" value="Update">
    </form>
    <form action="/delete" method="post" id="delete">
        <input type="submit" value="Delete Selected All Onegai">
    </form>
        {% for onegai in all_onegai %}
        <div>
            <input type="radio" name="update" form="update" value={{onegai.id}}>
            <input type="checkbox" name="delete" form="delete" value={{onegai.id}}>
            {{onegai.title}}:{{onegai.body}}({{onegai.date}})
        </div>
        {% endfor %}
    <!-- 変更終わり -->
    </body>
</html>

フォームの値を受け取ってDELETE処理をする

フォームの値を受け取ってDBのレコードを削除するための関数をapp.pyに作成したいと思います。
フォームのaction属性で「/delete」と指定したので、@app.route("/delete",methods=["post"])でルーティングを追加します。
DELETE処理はmodelsオブジェクト(今回はOnegaiContent)をdb_session.delete()関数の引数に渡すことで実行できます。削除対象となるレコードをrequest.form.getlist("delete")でidのリストで受け取ってから、そのリストに対してfor文を回して1件ずつ削除します。
以下のようなルーティング/関数をapp.pyに追加しましょう。

app.py
from flask import Flask,render_template,request
from models.models import OnegaiContent
from models.database import db_session
from datetime import datetime

app = Flask(__name__)


@app.route("/")
@app.route("/index")
def index():
    name = request.args.get("name")
    all_onegai = OnegaiContent.query.all()
    return render_template("index.html",name=name,all_onegai=all_onegai)


@app.route("/index",methods=["post"])
def post():
    name = request.form["name"]
    all_onegai = OnegaiContent.query.all()
    return render_template("index.html", name=name, all_onegai=all_onegai)


@app.route("/add",methods=["post"])
def add():
    title = request.form["title"]
    body = request.form["body"]
    content = OnegaiContent(title,body,datetime.now())
    db_session.add(content)
    db_session.commit()
    return index()


@app.route("/update",methods=["post"])
def update():
    content = OnegaiContent.query.filter_by(id=request.form["update"]).first()
    content.title = request.form["title"]
    content.body = request.form["body"]
    db_session.commit()
    return index()


#以下を追加
@app.route("/delete",methods=["post"])
def delete():
    id_list = request.form.getlist("delete")
    for id in id_list:
        content = OnegaiContent.query.filter_by(id=id).first()
        db_session.delete(content)
    db_session.commit()
    return index()
#追加終わり


if __name__ == "__main__":
    app.run(debug=True)

表示確認

あとはいつも通り、run.pyを走らせてhttp://127.0.0.1:5000/indexにアクセスしましょう。
チェックボックスをいくつか選択して「Delete Selected All Onegai」をクリックすることで対象のお願い全てを削除することができるはずです。
スクリーンショット 2019-06-25 0.19.52.png

Day6

ログイン機能とセッション管理

以下の機能を追加して、さらにそれっぽいWebページにしたいと思います。

  • ログイン機能
  • ユーザ新規登録機能
  • ログイン中は左上に常に「{{ユーザ名}}神社」を表示
  • ログアウト機能

htmlの作成・変更

ログイン用、新規登録用のhtmlをtemplatesに作成します。
これらはログイン処理失敗時や新規登録失敗時にエラーメッセージを表示させたいので、statusによる条件分岐を埋め込んでいます。
また、index.htmlのタイトル変更用フォームを削除し、ログアウト用のリンクを追加します。
それぞれ以下のように作成・変更しましょう。

top.html
<!DOCTYPE html>
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>
    <h1>ログイン</h1>
    {% if status == "user_notfound" %}
    <p>ユーザが見つかりません。新規登録しましょう。</p>
    {% elif status == "wrong_password" %}
    <p>パスワードが間違っています。</p>
    {% elif status == "logout" %}
    <p>ログアウトが完了しました。</p>
    {% endif %}
    <form action="/login" method="post">
        <input type="text" name="user_name" placeholder="user name">
        <input type="password" name="password" placeholder="password">
        <input type="submit" value="Login">
    </form>
    <a href="/newcomer">新規登録はこちら</a>
</body>
</html>
newcomer.html
<!DOCTYPE html>
<head>
    <meta charset="UTF-8">
    <title>Registar</title>
</head>
<body>
    <h1>新規登録</h1>
    <a href="/top">ログイン画面に戻る</a>
    {% if status == "exist_user" %}
    <p>そのユーザは既に登録されています。</p>
    {% endif %}
    <form action="/registar" method="post">
        <input type="text" name="user_name" placeholder="user name">
        <input type="password" name="password" placeholder="password">
        <input type="submit" value="新規登録">
    </form>
</body>
</html>
index.html
<!DOCTYPE html>
<html>
    <head>
        <title>{{name}}</title>
    </head>
    <!-- 以下を追加 -->
    <a href="/logout">ログアウトする</a>
    <!-- 追加終わり -->
    <body>
    {% if name == "kiyokiyo" %}
    <h1>スペシャル{{name}}神社</h1>
    {% elif name %}
    <h1>{{name}}神社</h1>
    {% else %}
    <h1>ただの神社</h1>
    {% endif %}
    <!-- ここにあったフォームを削除 -->
    <img src="/static/images/torii.jpg" alt="鳥居">
    <form action="/add" method="post">
        <input type="text" name="title" placeholder="title">
        <input type="text" name="body" placeholder="body">
        <input type="submit" value="Add">
    </form>
    <form action="/update" method="post" id="update">
        <input type="text" name="title" placeholder="title">
        <input type="text" name="body" placeholder="body">
        <input type="submit" value="Update">
    </form>
    <form action="/delete" method="post" id="delete">
        <input type="submit" value="Delete Selected All Onegai">
    </form>
        {% for onegai in all_onegai %}
        <div>
            <input type="radio" name="update" form="update" value={{onegai.id}}>
            <input type="checkbox" name="delete" form="delete" value={{onegai.id}}>
            {{onegai.title}}:{{onegai.body}}({{onegai.date}})
        </div>
        {% endfor %}
    </body>
</html>

DBにユーザ情報を格納するためのテーブルを用意

DBにユーザの名前とパスワードを格納するためのusersテーブルを追加します。
まずmodels.pyにusersテーブルの定義を追加します。
以下のように変更しましょう。

models.py
from sqlalchemy import Column, Integer, String, Text, DateTime
from models.database import Base
from datetime import datetime


class OnegaiContent(Base):
    __tablename__ = 'onegaicontents'
    id = Column(Integer, primary_key=True)
    title = Column(String(128), unique=True)
    body = Column(Text)
    date = Column(DateTime, default=datetime.now())

    def __init__(self, title=None, body=None, date=None):
        self.title = title
        self.body = body
        self.date = date

    def __repr__(self):
        return '<Title %r>' % (self.title)

#以下を追加
class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    user_name = Column(String(128), unique=True)
    hashed_password = Column(String(128))

    def __init__(self, user_name=None, hashed_password=None):
        self.user_name = user_name
        self.hashed_password = hashed_password

    def __repr__(self):
        return '<Name %r>' % (self.user_name)
#追加終わり

次に、このusersテーブルをDBに実際に作成します。
PyCharmの「Python Console」に以下のコードを入力しましょう。
ちなみに、init_db()は既に作成されているテーブルに対しては適用されないので、以前作ったonegaicontentsテーブルが再度初期化されることはありません。

from models.database import init_db
init_db()

ちゃんと作成されたか確かめたい方はサンプルデータを投入してうまくいくか確かめてみましょう(Day4参照)。サンプルデータの削除を忘れずに。)

暗号化キー情報を扱うファイルkey.pyの作成

セッション管理に利用するSECRET_KEYと、パスワードの暗号化の際に利用するSALTを保持するためのkey.pyファイルを作成します。なぜ別ファイルに記述するのかと言うと、app.py等に直接記述してしまうとGitHub等のコード共有サービスに上げた際に暗号化キーが漏洩してしまうためです。別ファイルに記述してGitHubに上げるファイル対象から除外するのが一般的だそうです。
配置場所はとりあえずapp/以下にしましたが、どこでも問題ありません(一応、現在のディレクトリ構成こんな感じです)。

(any directory)
 ├app/
 │ ├templates/
 │ │ ├index.html
 │ │ ├newcomer.html
 │ │ └top.html
 │ ├static/
 │ │ └images/
 │ │  └torii.jpg
 │ ├app.py
 │ └key.py
 ├models/
 │ ├__init__.py
 │ ├models.py
 │ ├database.py
 │ └onegai.db
 └run.py

以下のように記載しましょう。

key.py
#どちらも任意の文字列で大丈夫です
SECRET_KEY = "g8wkf0pvje6m2unhgt3j"
SALT = "762ifuw9fj5wxjkeu0dk"

ログイン処理

いよいよロジック部分の実装に入っていきます。まずはログイン処理から。なお、ここからは処理別にコードを載せています。最終的にapp.pyがどうなったのかは一番最後にまとめて載せるので、とりあえず何をやっているのかだけ理解していただければ大丈夫です。

ログイン処理は以下のように書きます。

app.py
@app.route("/login",methods=["post"])
def login():
    user_name = request.form["user_name"]
    user = User.query.filter_by(user_name=user_name).first()
    if user:
        password = request.form["password"]
        hashed_password = sha256((user_name + password + key.SALT).encode("utf-8")).hexdigest()
        if user.hashed_password == hashed_password:
            session["user_name"] = user_name
            return redirect(url_for("index"))
        else:
            return redirect(url_for("top",status="wrong_password"))
    else:
        return redirect(url_for("top",status="user_notfound"))

1行目で例の如くルーティングをしています。
3行目でフォームに入力されたユーザ名を取得し、
4行目でそのユーザ名を持つDBレコードをusersテーブルから抽出しています。
もしDBレコードがあった場合、
6〜7行目でフォームに入力されたパスワードを取得してハッシュ化した後、
8行目でDBレコードのハッシュ化パスワードと一致するか判定しています。
一致した場合はログイン成功となるので、セッション情報にユーザ名を追加して/indexにリダイレクトしています。
不一致の場合はログイン失敗となるので、/top?status=wrong_passwordにリダイレクトしています。
そもそもDBレコードが無かった場合、ユーザ登録がされていないので、/top?status=user_notfoundにリダイレクトしています。

これらを使うために以下のインポート文が必要になります。app.pyの上部に追加しましょう。

app.py
from flask import session,redirect,url_for
from app import key
from hashlib import sha256

ユーザ新規登録処理

ユーザ新規登録処理は以下のように書きます。

app.py
@app.route("/registar",methods=["post"])
def registar():
    user_name = request.form["user_name"]
    user = User.query.filter_by(user_name=user_name).first()
    if user:
        return redirect(url_for("newcomer",status="exist_user"))
    else:
        password = request.form["password"]
        hashed_password = sha256((user_name + password + key.SALT).encode("utf-8")).hexdigest()
        user = User(user_name, hashed_password)
        db_session.add(user)
        db_session.commit()
        session["user_name"] = user_name
        return redirect(url_for("index"))

5行目まではログイン処理と同じです。
もしDBレコードがあった場合、既にユーザ登録されているため、特に登録処理は行わず/newcomer?status=exist_userにリダイレクトしています。
DBレコードが無かった場合、ユーザ登録されていないので、
8〜9行目でパスワードのハッシュ化を行い、
10〜12行目でusersテーブルへのDBレコード追加を行い、
13行目でセッション情報にユーザ名を埋め込んで、その後/indexにリダイレクトしています。

パスワードをDBにベタ書き保存するとセキュリティ上よろしくないため、ハッシュ化したパスワードを保存しています。今回はsha256という暗号化方式を使用しています。

セッションによる閲覧制御

/indexにアクセスしてきたユーザに対して、セッション情報がある場合はindex.htmlを表示し、セッション情報がない場合にはログインページ(/top)にリダイレクトしたいと思います。
以下のようにindex()関数を変更しましょう。

app.py
@app.route("/")
@app.route("/index")
def index():
    if "user_name" in session:
        name = session["user_name"]
        all_onegai = OnegaiContent.query.all()
        return render_template("index.html",name=name,all_onegai=all_onegai)
    else:
        return redirect(url_for("top"))

4行目でセッション情報にユーザ名が入っているか判定しています。
ログイン時にセッション情報にユーザ名をせっせと追加していたのはこのためです。
セッション情報にユーザ名が入っていた場合はログイン済みユーザーなので、index.htmlを表示させます。その際にセッション情報からユーザ名を受け取り、index.htmlに変数として渡しています。
セッション情報にユーザ名が入っていない場合は非ログインユーザなので、ログイン画面にリダイレクトしています。

なお、このセッション情報をちゃんと暗号化しないと容易にユーザのなりすましができてしまうので、app.secret_key変数に文字列を代入することで暗号化キーを定義してあげる必要があります。
そこにapp.pyで定義したSECRET_KEYを使用します。
以下の一文をapp = Flask(__name__)の下あたりに記述しましょう。

app.py
app = Flask(__name__)
#以下を追加
app.secret_key = key.SECRET_KEY

ログアウト処理

ログアウト処理はsession.pop()でセッション情報を削除してあげることでできます。
以下のように記載しましょう。ログアウト後にログインページにリダイレクトしてあげることも忘れないようにしましょう。

app.py
@app.route("/logout")
def logout():
    session.pop("user_name", None)
    return redirect(url_for("top",status="logout"))

その他の修正点

ログインページと新規登録ページへのルーティング処理を追加しましょう。

app.py
@app.route("/top")
def top():
    status = request.args.get("status")
    return render_template("top.html",status=status)


@app.route("/newcomer")
def newcomer():
    status = request.args.get("status")
    return render_template("newcomer.html",status=status)

それから、/add/update/deleteの戻り値をリダイレクトに変更しました。特に深い意味は無いですが、今までお願いを追加したり削除したりするとURLが変わってしまっていたのが/indexに統一されます。

app.py
@app.route("/add",methods=["post"])
def add():
    title = request.form["title"]
    body = request.form["body"]
    content = OnegaiContent(title,body,datetime.now())
    db_session.add(content)
    db_session.commit()
    return redirect(url_for("index"))


@app.route("/update",methods=["post"])
def update():
    content = OnegaiContent.query.filter_by(id=request.form["update"]).first()
    content.title = request.form["title"]
    content.body = request.form["body"]
    db_session.commit()
    return redirect(url_for("index"))


@app.route("/delete",methods=["post"])
def delete():
    id_list = request.form.getlist("delete")
    for id in id_list:
        content = OnegaiContent.query.filter_by(id=id).first()
        db_session.delete(content)
    db_session.commit()
    return redirect(url_for("index"))

あと、index.htmlの名前用フォームを削除したので、/indexのPOSTメソッドを受け取っていた以下の関数を削除しましょう。

app.py
#以下を削除
@app.route("/index",methods=["post"])
def post():
    name = request.form["name"]
    all_onegai = OnegaiContent.query.all()
    return render_template("index.html", name=name, all_onegai=all_onegai)

app.py完成形

以下のようになります。

app.py
from flask import Flask,render_template,request,session,redirect,url_for
from models.models import OnegaiContent,User
from models.database import db_session
from datetime import datetime
from app import key
from hashlib import sha256

app = Flask(__name__)
app.secret_key = key.SECRET_KEY


@app.route("/")
@app.route("/index")
def index():
    if "user_name" in session:
        name = session["user_name"]
        all_onegai = OnegaiContent.query.all()
        return render_template("index.html",name=name,all_onegai=all_onegai)
    else:
        return redirect(url_for("top"))


@app.route("/add",methods=["post"])
def add():
    title = request.form["title"]
    body = request.form["body"]
    content = OnegaiContent(title,body,datetime.now())
    db_session.add(content)
    db_session.commit()
    return redirect(url_for("index"))


@app.route("/update",methods=["post"])
def update():
    content = OnegaiContent.query.filter_by(id=request.form["update"]).first()
    content.title = request.form["title"]
    content.body = request.form["body"]
    db_session.commit()
    return redirect(url_for("index"))


@app.route("/delete",methods=["post"])
def delete():
    id_list = request.form.getlist("delete")
    for id in id_list:
        content = OnegaiContent.query.filter_by(id=id).first()
        db_session.delete(content)
    db_session.commit()
    return redirect(url_for("index"))


@app.route("/top")
def top():
    status = request.args.get("status")
    return render_template("top.html",status=status)


@app.route("/login",methods=["post"])
def login():
    user_name = request.form["user_name"]
    user = User.query.filter_by(user_name=user_name).first()
    if user:
        password = request.form["password"]
        hashed_password = sha256((user_name + password + key.SALT).encode("utf-8")).hexdigest()
        if user.hashed_password == hashed_password:
            session["user_name"] = user_name
            return redirect(url_for("index"))
        else:
            return redirect(url_for("top",status="wrong_password"))
    else:
        return redirect(url_for("top",status="user_notfound"))


@app.route("/newcomer")
def newcomer():
    status = request.args.get("status")
    return render_template("newcomer.html",status=status)


@app.route("/registar",methods=["post"])
def registar():
    user_name = request.form["user_name"]
    user = User.query.filter_by(user_name=user_name).first()
    if user:
        return redirect(url_for("newcomer",status="exist_user"))
    else:
        password = request.form["password"]
        hashed_password = sha256((user_name + password + key.SALT).encode("utf-8")).hexdigest()
        user = User(user_name, hashed_password)
        db_session.add(user)
        db_session.commit()
        session["user_name"] = user_name
        return redirect(url_for("index"))


@app.route("/logout")
def logout():
    session.pop("user_name", None)
    return redirect(url_for("top",status="logout"))


if __name__ == "__main__":
    app.run(debug=True)

表示確認

あとはいつも通り、run.pyを走らせてhttp://127.0.0.1:5000/topにアクセスしましょう。
ログイン画面が表示されるはずです。
スクリーンショット 2019-06-28 0.33.22.png
ユーザの登録を行なっていないので、「新規登録はこちら」をクリックして新規登録画面に遷移しましょう。
スクリーンショット 2019-06-28 0.35.08.png
ここで任意のユーザ名とパスワードを入力し、「新規登録」を押すと、入力したユーザ名でindex.htmlが見れるようになっているはずです。
スクリーンショット 2019-06-28 0.36.58.png
「ログアウトする」をクリックすると、再度ログイン画面に戻ります。
スクリーンショット 2019-06-28 0.38.57.png
ログアウトした後はセッション情報が削除されているので、http://127.0.0.1:5000/indexをベタ打ちしてもログイン画面にリダイレクトされて、先ほどの神社の画面には入れないはずです。
その他にも、

  • 登録されていないユーザでログインしようとする
  • パスワードを間違えてみる
  • 既に登録されているユーザ名で新規登録しようとする

等、色々試してみてください。

あと、DBのusersテーブルも確認してみましょう。
パスワードがハッシュ化されており、元のパスワードを特定できない状態になっていることが分かると思います。

sqlite> select * from users;
1|kiyokiyo|60c11be5fc1f8c1d20754c6676396206b0237a1cded874bf15c80d18446f8276
2|kiyo|7dc69137e3516e7c74e78696f3ba27e4cc37b302cfab6e50d7fc6f8c0eab118d

Day7

デプロイとアプリの公開

今までは自分のPCのローカル環境で実行してきたため、他の人からアクセスすることはできませんでしたが、
今回は作成したアプリをWebサーバ上に展開して、他の人からもアクセスできるように公開したいと思います。
デプロイにはHerokuというPaaS(Webアプリ公開用プラットフォームサービス)を利用したいと思います。

gunicornのインストール

Heroku上にWebサーバが用意されており、そこにFlaskで作ったアプリを乗せてあげることでWebアプリとして公開することができるのですが、WebサーバとFlaskアプリはそれぞれ独立しているため、ただ乗せるだけではうまく機能しません。そこで、WebサーバとFlaskアプリの間を取り持ってくれるWSGI(ウィズギー)というものを一緒に乗せる必要があります。
今回はそのWSGIの一つ「gunicorn」というものを使用します。
gunicornを使うための準備は簡単で、

pip install gunicorn

をターミナルで叩くだけです。実行しましょう。

requirements.txtの作成

今までpip installを使って色々なライブラリを利用してきましたが、これらはあくまで自分のローカルPCに保存されているだけで、HerokuのWebサーバ上には一切インストールされていません。そのため、Heroku側に「このライブラリのインストールが必要ですよ」と教えてあげる必要があります。そのために使用するファイルがrequirements.txtです。Herokuの制約上、このファイル名でないと動きませんのでスペルミスに気をつけましょう。また、Heroku側に認識させるために、アプリ内の一番上位のディレクトリ階層に配置してあげる必要があります。(今回は「any directory」直下、app/とかrun.pyと同じ階層です。)
中身にはインストールが必要なライブラリ(依存ライブラリ)の一覧を記載します。が、手書きする必要はなく、コマンド一行で実行できます。以下のコマンドをターミナルで叩きましょう。

pip freeze > requirements.txt

requirements.txtの中身を確認するとこんな感じで依存しているライブラリの情報が記載されています。先ほどinstallしたgunicornもちゃんと入ってることが確認できますね。

certifi==2019.6.16
Click==7.0
Flask==1.0.3
gunicorn==19.9.0
itsdangerous==1.1.0
Jinja2==2.10.1
MarkupSafe==1.1.1
SQLAlchemy==1.3.5
Werkzeug==0.15.4

Procfileの作成

herokuでアプリを起動する際に一番初めに呼び出す処理をProcfileというファイルに定義することができます。このファイルもHeroku側に認識させてあげるために、requirements.txtと同じ階層に配置します。
中身は以下のように記載しましょう。

web: gunicorn run:app --log-file=-

これで、gunicornを介して、Webサーバ上でrun.pyのapp.run()を実行させることができます。

最終的なディレクトリ構成はこんな感じになります。

(any directory)
 ├app/
 │ ├templates/
 │ │ ├index.html
 │ │ ├newcomer.html
 │ │ └top.html
 │ ├static/
 │ │ └images/
 │ │  └torii.jpg
 │ ├app.py
 │ └key.py
 ├models/
 │ ├__init__.py
 │ ├models.py
 │ ├database.py
 │ └onegai.db
 ├run.py
 ├requirements.txt
 └Procfile

アプリ全体をGitの管理下に置く

Gitはファイルの分散バージョン管理システムで、指定したディレクトリ以下のファイルの変更履歴を保存したり、バージョンを切り替えたり、といったことができるシステムです。Gitの詳細についてはサルでも分かるGitが分かりやすいのでオススメです。
アプリ全体をGitに登録するために、(any directory)にカレントディレクトリを合わせて、以下のコマンドをターミナルに入力しましょう。

git init
git add .
git commit -m "first commit"

1行目でGitの初期化
2行目でGit管理するファイルの選択(「.」で、カレントディレクトリ以下全てのファイルを指定しています)
3行目でGitへの保存を実行(「-m "~~~"」はコミットコメントと呼ばれているもので、どのような変更を加えてGitに保存したのかを記載していくことが推奨されています)
という流れです。

Herokuの利用登録

ここからHerokuの利用に入っていきます。まず、Heroku公式ページで会員登録をしましょう。登録に必要な個人情報はメールアドレスくらいで、無料でサービスを利用できます。
スクリーンショット 2019-06-29 2.49.32.png

Heroku CLIツールのインストール

こちらのページからHeroku CLIツールのダウンロード・インストールを実行しましょう。ターミナルからHerokuを操作することができるようになります。

Herokuへログイン

以下のコマンドをターミナルで叩くことでHerokuにログインすることができます。表示される内容に従ってログインしましょう。

heroku login

アプリケーションの作成

以下のコマンドをターミナルで叩くことでHeroku上にアプリケーションを作成することができます。「アプリケーション名」は、Herokuを利用している全ユーザ内で重複していなければどんな名前でもOKです。私は「kiyokiyo-shrine」にしました。

heroku create アプリケーション名

Herokuへデプロイ

いよいよHerokuにアプリをデプロイします。デプロイにはGitのpush機能を利用します。以下のコマンドをターミナルで叩きましょう。

git push heroku master

しばらくデプロイ作業のログが出続けると思いますが、1~2分もすれば終わると思います。

表示確認

ターミナルに以下のコマンドを入力するとHerokuでのWebページを開くことができます。(もしくはデプロイ作業のログの最後の方に出たURLでもWebページを開くことができます。)

heroku open

以下のようなWebページが表示されていたらOKです。ローカルと同じように動作するか、色々触って確認してみてください。
スクリーンショット 2019-06-29 4.15.25.png
ちなみに私の作ったアプリはこちらです。記事投稿から数日は公開していようと思うので、ご自由に触ってください。

アプリのコード等を変更した場合

ファイルを変更した後にはGitの操作を忘れずに行いましょう。基本的な流れは常にadd→commit→pushです。

git add 変更したファイル
git commit -m "コミットメッセージ"
git push heroku master

最後に

この1週間で、FlaskとSQLiteとHerokuを使ってWebアプリをイチから作ってサービス公開するまでの流れを掴めたと思います。ただ、サービスの中身としては正直物足りないと思います。なので自身の手で、CSSで見た目を良くしたり、JSでブラウザ側の挙動を充実させたり、app.pyでもっと色んな処理をできるようにしたり等、たくさん試してみてください。

余談

HerokuとSQLiteについて

SQLiteはHerokuのサポート対象外らしく、1日おきに中のテーブルデータがリセットされてしまうようです。今回は簡単に利用できるSQLiteを使用しましたが、今後はサポート対象になっているPostgreのDBを作成して、永続的にデータを保存できるようにすることをオススメします。ググってみればSQLiteからPostgreに移行するための手順がいっぱい出てくると思います。

MVCモデル

よくアプリの設計で言われているMVCモデルが、まさに今回開発したアプリ(というかFlask)で採用されているモデルです。

  • M(model):DBレコードをオブジェクトとして受け取る部分。今回のアプリで言う所のmodels.pyが該当します。
  • V(view):Webページを表示する部分。今回のアプリで言う所のtemplates/static/が該当します。
  • C(controller):httpリクエストを受け取って、modelを介してDB操作をしたり、viewを介してWebページを表示させたり等、中枢となる処理を実行する部分。今回のアプリで言う所のapp.pyが該当します。

応用情報の本とかで「MVCモデルは〜」とか書いてあって、当時は(なんだこれ分からん)としか思わなかったのですが、今回アプリを作ってみて初めて知識と経験が紐づいたので、1年越しくらいでやっと理解することができました。やっぱ実際に手を動かしてみるって大事ですね。

90
98
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
90
98

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?