ブログ投稿サービスを作る

# 今回のテーマ

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">次のテーマは<br>・仮想環境使う<br>・GitHub使う<br>・Postgre使う<br>・Flask-session/Flask-SQLAlchemy使う<br>・フロント側に少し力を入れる<br>でいきます<br>題材は無難にブログとかでいいかな？</p>&mdash; きよきよ (@kiyokiyo_kzsby) <a href="https://twitter.com/kiyokiyo_kzsby/status/1144883143188475904?ref_src=twsrc%5Etfw">2019年6月29日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

ということで、これらを踏まえて１週間でブログ投稿サービスを作っていきたいと思います。

#Day1
## 仮想環境（pipenv）を使う
pythonの仮想環境系のツールはたくさんあってどれを使えばいいのか分からなかったのですが、[この記事](https://www.m3tech.blog/entry/python-packaging)曰く「pipenvがpipとかvenvとかをラップしているのでとりあえずpipenvを使えばOK」とのことだったので、今回はpipenvを使います。

まず、pipenvを初めて使うのでインストールからします。

```
$ pip install pipenv
```
次に、ブログ投稿サービスの開発用フォルダ`TutorialBlog`を作成します。

```
$ mkdir TutotialBlog
$ cd TutotialBlog/
```
そして、そのフォルダに対し仮想環境を構築します。

```
$ pipenv install
```
こんな感じで、`TurorialBlog/`以下に`.venv`、`Pipfile`、`Pipfile.lock`が作成されていればOKです。

```
$ ls -a
.       ..      .venv       Pipfile     Pipfile.lock
```

## Flask基本構成を作る
[この記事](https://qiita.com/kiyokiyo_kzsby/items/f8aa0bf68007e18d6882)を参考にFlaskの基本構成を作っていきます。
まずFlaskのインストールから。`pipenv install [パッケージ名]`でパッケージをインストールできます。

```
$ pipenv install Flask
```
続いて以下のフォルダ・ファイルを作っていきます。

```
TutorialBlog/
 ├app/
 │　├templates/
 │　├static/
 │　└app.py
 └run.py
```
app.pyとrun.pyの中身は一旦こんな感じで作っておきます。

```python:app.py
from flask import Flask

app = Flask(__name__)

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

```python:run.py
from app.app import app

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

## GitHubを使う
### リモートリポジトリの作成
まず、GitHubにログインし、「New Repository」でリモートリポジトリを作ります。
今回は「TutorialBlog」という名前でパブリックリポジトリを作成しました。
なお、この時に「Initialize this repository with a README」というチェックボックスがありますが、これにチェックを付けないようにしましょう。先にリモート側を作成してしまうと、ローカルから初pushする際にエラーとなってしまいます。

### リモートリポジトリへ保存
まず、ローカルのTutorialBlogフォルダ以下をGitの管理下にするため、カレントディレクトリが`TutorialBlog`の状態で`git init`を叩きます。

```
$ git init
```
続いて、リモートリポジトリの情報を登録します。コマンドは`git remote add origin [リモートリポジトリのURL]`です。リモートリポジトリのURLはGitHubの画面から取得しましょう。

```
$ git remote add origin https://github.com/kiyokiyo-kzsby/TutorialBlog.git
```
最後に以下のコマンドを叩いて、ローカルリポジトリへの保存、リモートリポジトリへの保存を行います。

```
$ git add .
$ git commit -m "first commit"
$ git push -u origin master
```
GitHubで先ほど作成したリモートリポジトリを見ると、ローカルで作成したファイルが登録されていることが分かると思います。

# Day2
## Postgreを利用する
### Postgreのインストール
[このページ](http://vdeep.net/mac-postgresql)を参考にして行いました。
主に使用したコマンドは以下。

Homebrewを使ってPostgreSQLを導入

```
$ brew install postgresql
```
導入確認

```
$ psql --version
psql (PostgreSQL) 11.3
```
データベース初期化

```
$ initdb /usr/local/var/postgres -E utf8
```
起動

```
$ brew services start postgresql
```
ユーザの追加

```
$ createuser -s -P [ユーザ名]
```
データベースの作成

```
$ createdb tutorial_blog
```
データベース一覧の確認

```
$ psql -l
```
停止

```
$ brew services stop postgresql
```

### psycopg2のインストール
普通に`pipenv install psycopg2`でイケると思ったらエラー吐いて失敗しました。
[このサイト](https://hamaguchictrlz.hatenablog.com/entry/2019/04/23/013941)を参考に再度インストールを試してみたらうまくいきました。
なんでエラー吐いたのかは分からずじまいです。

関係モジュールを削除

```
$ pipenv uninstall psycopg2 psycopg2-binary
```
psycopg2-binaryをインストール

```
$ pipenv install psycopg2-binary
```

## メインページ作成
仕様としては

- 投稿されたブログのタイトルと執筆者といいね数が、最新順に表示される
- 各ブログをクリックすると、そのブログの詳細画面に飛べる
- 未ログインユーザにはログイン画面へ遷移するためのボタンを表示
- ログインユーザにはユーザ名と、マイページへ遷移するためのボタン、ログアウトボタン等を表示

という感じで作っていきたいと思います。

### models.pyの作成
まずflask_sqlalchemyのインストールをします。

```
$ pipenv install falsk_sqlalchemy
```
次に、app.pyと同じ階層にmodels.pyを作成します。

```
TutorialBlog/
 ├app/
 │　├templates/
 │　├static/
 │　├app.py
 │　└models.py
 └run.py
```

```models.py
from flask_sqlalchemy import SQLAlchemy
from app.app import app
from datetime import datetime

app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://postgres:@localhost/tutorial_blog'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

class Content(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(80), nullable=False)
    body = db.Column(db.Text, nullable=False)
    pub_date = db.Column(db.DateTime, nullable=False,default=datetime.utcnow)
    good_count = db.Column(db.Integer, default=0)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'),nullable=False)
    user = db.relationship('User',backref=db.backref('content', lazy=True))

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

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=False)
    hashed_password = db.Column(db.String(100), nullable=False)

    def __repr__(self):
        return '<User %r>' % self.name
```

### テーブルの作成
Python Consoleで以下のコードを入力してテーブルを作成します。

```python
>>> from app.models import db
>>> db.create_all()
```
ちゃんと作成されているか試し打ちしてみます。

```python
>>> from app.models import User
>>> u1 = User(id="1",name="kiyokiyo",hashed_password="aaaa")
>>> u2 = User(id="2",name="kiyokiyo2",hashed_password="bbbb")
>>> from app.models import db
>>> db.session.add(u1)
>>> db.session.add(u2)
>>> db.session.commit()
>>> User.query.all()
[<User 'kiyokiyo'>, <User 'kiyokiyo2'>]
```

ついでにターミナルからPostgreに接続して直接中身を確認してみます。
Postgreへの接続コマンドは`psql -U [ユーザ名] [DB名]`です。

```
$ psql -U postgres tutorial_blog
```
あとは普通にSQL文を叩きます。（「from user」だとユーザ情報のテーブルとバッティングするようで、「from public.user」にする必要があるそうです。）

```
# select * from public.user;
 id |   name    | hashed_password 
----+-----------+-----------------
  1 | kiyokiyo  | aaaa
  2 | kiyokiyo2 | bbbb
```
ついでにcontentテーブルの方にもサンプルデータを格納しておきます。

```python
>>> from app.models import db
>>> from app.models import Content
>>> c1 = Content(title="タイトル１",body="ボディ１",user_id="1")
>>> c2 = Content(title="タイトル２",body="ボディ２",user_id="2")
>>> c3 = Content(title="タイトル３",body="ボディ３",user_id="1")
>>> db.session.add(c1)
>>> db.session.add(c2)
>>> db.session.add(c3)
>>> db.session.commit()
>>> Content.query.all()
[<Content 'タイトル１'>, <Content 'タイトル２'>, <Content 'タイトル３'>]
```

# Day3
## メインページ続き
前回はDB設定とModel構築までしか終わらなかったので、今回はViewとControllerを実装します。

### index.html
ランディングページです。
for文で回して全コンテンツの作者名、コンテンツへのURL、コンテンツタイトル、いいね数を表示させます。

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>よくあるブログサービス</title>
</head>
<body>
    <h1>よくあるブログサービス</h1>

    {% for content in contents %}
    <div>
        <p>{{content.user.name}}：<a href="/content/{{content.id}}">{{content.title}}</a>　//　{{content.good_num}} good</p>
    </div>
    {% endfor %}

</body>
</html>
```

### content.html
個別のコンテンツページです。
index.htmlのaタグのURLから飛んで、特定コンテンツの詳細を表示させます。

```content.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>よくあるブログサービス</title>
</head>
<body>
    <a href="/">メインページへ戻る</a>
    <h1>{{content.title}}</h1>
    <p>{{content.user.name}}</p>
    <p>{{content.good_count}} good</p>
    <p>{{content.body}}</p>
</body>
</html>
```

### app.py
コントローラです。ここが一番詰まりました（もしかしたら自分だけかもですが）。

- 詰まりポイント１：python interpreter
app.pyを書いている途中でPycharmに`invalid python interpreter selected for the project`というメッセージが出ました。Python語を機械語に変換するためのインタープリタの設定が間違っているようです。[Pycharmの公式リファレンス](https://pleiades.io/help/pycharm/configuring-python-interpreter.html)を参考に、Pythonインタープリタの設定を行いました。

- 詰まりポイント２：import文の記載位置

Java原人の私はimport文を全て上に寄せて書いていたのですが、Pythonはimport文を（使用する行より上であれば）どこに書いても良いそうです。今回私は`from app.models import Content,User`を一番上に書いていたのですが、その瞬間models.pyで`from app.app import app`が呼ばれる＆この段階ではapp.pyでまだ`app = Flask(__name__)`が実行されてないため「appが無いよ！」というエラーを吐いていました。なので、`from app.models import Content,User`を`app = Flask(__name__)`より下に書いて、appが作成されてからmodels.pyを呼び出すようにしました。

- 詰まりポイント３：レコードのJOIN

結論から言うと、JOIN操作は`Table1.query.join(Table2).all()`でできます。
普段のSQLからするとJOINするための結合条件をどこかに書かないといけないと思っていたのですが、ググってもその記法が検索できなくて詰まりました。が、実はその結合条件は既にContentクラスの`db.relationship`で定義できていたようです。脳死で書いていたのでここの意味を全く理解していませんでした。`.join(User)`が呼ばれると`user = db.relationship('User',backref=db.backref('content', lazy=True))`の情報をもとにJOIN処理が走って`user`変数にUserの情報が格納されるようです。Userの情報を取り出す際も`content.user.name`みたいな感じで、一旦userを介せばアクセスできます。

最終的にこんな感じになりました。

```app.py
from flask import Flask,render_template

app = Flask(__name__)

from app.models import Content,User

@app.route("/")
def index():
    contents = Content.query.join(User).all()
    return render_template("index.html",contents=contents)


@app.route("/content/<content_id>")
def content(content_id):
    content = Content.query.filter_by(id=content_id).join(User).all()[0]
    return render_template("content.html",content=content)
```
ここまで実装したら`python run.py`で実行してみて、ローカルでサーバ立てて動作確認してみます。手元では想定通り機能したので一安心です。一段落したのでgit add/commit/pushを忘れずに。

# Day4
## ログインとセッション管理
Flask-loginを利用したログインとセッション管理の機能を実装します。

### Flask-loginのインストール
いつも通りpipenvでインストールします。

```
$ pipenv install flask_login
```

### key.pyの作成
続いてセッション情報を暗号化するためのsecret_keyと、パスワードを暗号化するためのsaltを格納するためのkey.pyを作ります。
これはGitHubに上げたくないので、GitIgnore設定しておきます。

```
$ touch .gitignore
$ vim .gitignore
---
app/key.py
```
20文字程度のキーを生成します。
Python Consoleで以下のコードを叩くと、ランダムに文字列が生成されます。

```python
>>> import random,string
>>> "".join(random.choices(string.ascii_letters + string.digits, k=20))
'lae0GBuXY19le4QqZmIG'
>>> "".join(random.choices(string.ascii_letters + string.digits, k=20))
'14qO12UpjEiaeeewz5pl'
```
これらをそれぞれsecret_keyとsaltとして格納しましょう。

```key.py
SECRET_KEY = "lae0GBuXY19le4QqZmIG"
SALT = "14qO12UpjEiaeeewz5pl"
```

### modelの修正
ユーザー情報を管理するクラスにUserMixinクラスを継承させます。
これにより、login_user/logout_userの処理に、そのクラスを利用することができるようになります。
UserMixinをインポートして、Userクラスの引数にUserMixinを与えてあげます。

```models.py
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
from flask_login import UserMixin
from app.app import app

app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://postgres:@localhost/tutorial_blog'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

class Content(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(80), nullable=False)
    body = db.Column(db.Text, nullable=False)
    pub_date = db.Column(db.DateTime, nullable=False,default=datetime.utcnow)
    good_count = db.Column(db.Integer, default=0)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'),nullable=False)
    user = db.relationship('User',backref=db.backref('content', lazy=True))

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

class User(db.Model,UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=False)
    hashed_password = db.Column(db.String(100), nullable=False)

    def __repr__(self):
        return '<User %r>' % self.name
```

### viewの追加・修正
ログインユーザに関する情報は`current_user`で取得できます。ここで取得できるオブジェクトが、UserMixinを継承しているオブジェクトになるので、今回の場合はmodels.pyで定義したUserクラスになります。つまり、例えば`{{current_user.name}}`で現在ログインしているユーザのユーザ名を取得できます。

追加修正したページは以下です。

```index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>よくあるブログサービス</title>
</head>
<body>
    {% if current_user.is_authenticated %}
    <div>
        <a href="/mypage/{{current_user.name}}">{{current_user.name}}</a>
        <a href="/logout">ログアウト</a>
    </div>
    {% else %}
    <div>
        <a href="/login">ログイン</a>
    </div>
    {% endif %}

    <h1>よくあるブログサービス</h1>

    {% for content in contents %}
    <div>
        <p>{{content.user.name}}：<a href="/content/{{content.id}}">{{content.title}}</a>　//　{{content.good_num}} good</p>
    </div>
    {% endfor %}

</body>
</html>
```

```login.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>よくあるブログサービス</title>
</head>
<body>
    <a href="/">メインページへ戻る</a>
    <h1>login</h1>
    <form action="/login_submit" 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="/sign_up">sign up</a>
</body>
</html>
```

```sign_up.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>よくあるブログサービス</title>
</head>
<body>
    <a href="/">メインページへ戻る</a>
    <h1>sign up</h1>
    <form action="/sign_up_submit" method="post">
        <input type="text" name="user_name" placeholder="user name">
        <input type="password" name="password" placeholder="password">
        <input type="password" name="confirm_password" placeholder="confirm password">
        <input type="submit" value="login">
    </form>
    <a href="/login">return to login page</a>
</body>
</html>
```

```mypage.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>よくあるブログサービス</title>
</head>
<body>
    <h1>{{current_user.name}}</h1>

    {% for content in contents %}
    <div>
        <p>{{content.user.name}}：<a href="/content/{{content.id}}">{{content.title}}</a>　//　{{content.good_num}} good</p>
    </div>
    {% endfor %}
</body>
</html>
```

### controllerの修正
Flask-loginを使うにあたっての追加事項はこちらです。

```python
login_manager = LoginManager()
login_manager.init_app(app)
app.config["SECRET_KEY"] = key.SECRET_KEY


@login_manager.user_loader
def load_user(id):
    return User.query.filter_by(id=id).first()
```
上3行はログインマネージャーを立ち上げて、secret_keyをセットしています。
下3行はユーザidから、UserMixinを継承しているオブジェクトを返す処理を書いています。これが無いと、セッション情報をうまく管理できないようです。

あとは、ルーティングに合わせて書きたい処理を書いています。
主に、ログイン、ユーザの登録、ログアウト、マイページへの遷移等の記述を追加しました。
 
```app.py
from flask import Flask,render_template,request,redirect,url_for
from flask_login import LoginManager,login_user,logout_user,login_required
from app import key
from hashlib import sha256

app = Flask(__name__)
from app.models import db,Content,User
login_manager = LoginManager()
login_manager.init_app(app)
app.config["SECRET_KEY"] = key.SECRET_KEY


@login_manager.user_loader
def load_user(id):
    return User.query.filter_by(id=id).first()


@app.route("/")
def index():
    contents = Content.query.join(User).all()
    return render_template("index.html",contents=contents)


@app.route("/content/<content_id>")
def content(content_id):
    content = Content.query.filter_by(id=content_id).join(User).all()[0]
    return render_template("content.html",content=content)


@app.route("/login")
def login():
    return render_template("login.html")


@app.route("/login_submit",methods=["POST"])
def login_submit():
    user_name = request.form["user_name"]
    user = User.query.filter_by(name=user_name).first()
    if user is None:
        return redirect(url_for("login"))
    else:
        hashed_password = sha256((user_name + request.form["password"] + key.SALT).encode("utf-8")).hexdigest()
        if hashed_password != user.hashed_password:
            return redirect(url_for("login"))
        else:
            login_user(user)
            return redirect(url_for("index"))


@app.route("/sign_up")
def sign_up():
    return render_template("sign_up.html")


@app.route("/sign_up_submit",methods=["POST"])
def sign_up_submit():
    user_name = request.form["user_name"]
    user = User.query.filter_by(name=user_name).first()
    if user is None:
        password = request.form["password"]
        confirm_password = request.form["confirm_password"]
        if password != confirm_password:
            return redirect(url_for("sign_up"))
        else:
            hashed_password = sha256((user_name + password + key.SALT).encode("utf-8")).hexdigest()
            new_user = User(name=user_name,hashed_password=hashed_password)
            db.session.add(new_user)
            db.session.commit()
            login_user(new_user)
            return redirect(url_for("index"))
    else:
        return redirect(url_for("sign_up"))


@app.route("/mypage/<user_name>")
@login_required
def mypage(user_name):
    user_id = User.query.filter_by(name=user_name).all()[0].id
    contents = Content.query.filter_by(user_id=user_id).all()
    return render_template("mypage.html",contents=contents)



@app.route("/logout")
@login_required
def logout():
    logout_user()
    return redirect(url_for("index"))


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

# Day5
今日はお休みです。
無性に俺ガイル2期を一気見したくなったので。
休息も仕事のうちだよね。

# Day6
## 共通部分のテンプレート化
htmlヘッダーとかナビゲーションバーの部分は全画面同じなので共通化します。`block`を使って共通部分を`base.html`に切り出します。個別のhtml側で`extends`で共通部分を呼び出して、各ページ固有の部分を`block`文で記載します。一例としてindex.htmlも載せます。

```base.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>よくあるブログサービス</title>
    <link href="https://use.fontawesome.com/releases/v5.9.0/css/all.css" rel="stylesheet">
    <link rel="stylesheet" type="text/css" href="/static/style.css">
    <script type="text/javascript" src="/static/jquery-3.4.1.min.js"></script>
    <script type="text/javascript" src="/static/good.js"></script>
</head>
<body>
    <div id="navigation_bar">
        <a href="/"><i class="fas fa-home fa-2x nav-icon" title="メインページに戻る"></i></a>
        {% if current_user.is_authenticated %}
        <a href="/edit/new"><i class="far fa-file fa-2x nav-icon" title="新規作成"></i></a>
        <a href="/mypage/{{current_user.name}}">
            <img src="/static/uploads/{{current_user.icon_file_name}}" width="32" height="32" title="{{current_user.name}}">
            {{current_user.name}}
        </a>
        <a href="/config/{{current_user.name}}"><i class="fas fa-user-cog fa-2x nav-icon" title="ユーザ設定"></i></a>
        <a href="/logout"><i class="fas fa-sign-out-alt fa-2x nav-icon" title="ログアウト"></i></a>
        {% else %}
        <a href="/login"><i class="fas fa-sign-in-alt fa-2x nav-icon" title="ログイン"></i></a>
        {% endif %}
    </div>

    <div id="body">
        {% block body %}
        {% endblock %}
    </div>

</body>
</html>
```

```index.html
{% extends "base.html" %}

{% block body %}

<h1>よくあるブログサービス</h1>

{% for content in contents %}
<div>
    <p>{{content.user.name}}：<a href="/content/{{content.id}}">{{content.title}}</a>　//　{{content.good_num}} good</p>
</div>
{% endfor %}

{% endblock %}
```

ついでにナビゲーションバーをfontawesomeのアイコンに置き換えたり、jQueryを読み込むスクリプトを追加したり、地味に変更加えています。

## プロフィール画像のアップロード
### 画像アップロード用フォーム

```config.html
{% extends "base.html" %}

{% block body %}

<form action="/config_submit" method="post" enctype="multipart/form-data">
    <span>ユーザ名</span>
    <input type="text" name="name" value="{{current_user.name}}">
    <br>
    <span>紹介文</span>
    <input type="text" name="description" value="{{current_user.description}}">
    <br>
    <span>アイコン</span>
    <img src="/static/uploads/{{current_user.icon_file_name}}">
    <input type="file" name="icon">
    <span>（png,jpgのみ）</span>
    <br>
    <input type="submit" value="更新">
</form>

<a href="/reset_password">パスワードをリセット</a>

{% endblock %}
```

```app.py
from PIL import Image
import random,string
import os

@app.route("/config_submit",methods=["POST"])
@login_required
def config_submit():
    user = User.query.filter_by(id=current_user.id).all()[0]
    user.name = request.form["name"]
    user.description = request.form["description"]
    icon = request.files["icon"]
    if icon.filename == "":
        db.session.add(user)
        db.session.commit()
    else:
        file_extension = icon.filename.rsplit('.', 1)[1]
        if file_extension in ["jpg","png"]:
            icon = Image.open(request.files["icon"])
            icon_resize = icon.resize((256,256))
            old_file_name = user.icon_file_name
            new_file_name = "".join(random.choices(string.ascii_letters+string.digits,k=20))+".jpg"
            user.icon_file_name = new_file_name
            db.session.add(user)
            db.session.commit()
            icon_resize.save("app/static/uploads/"+new_file_name)
            if (old_file_name is not None) and os.path.exists("app/static/uploads/"+old_file_name):
                os.remove("app/static/uploads/"+old_file_name)
        else:
            abort(404)
    return redirect("/mypage/" + current_user.name)
```

ユーザーアップロード画像は`app/static/uploads/`下に保存しています。画像のリサイズはPILのImageクラスを利用しています。画像加工では割とよく使うライブラリらしいです。また、画像保存の際にファイル名をランダム生成してDBに保存し、いつでもファイルを取得できるようにしておきます。画像更新の場合は、既存画像の削除も忘れずに行なっています。
ちなみに、巨大なファイルを送りつけられても大丈夫なように、`app.config`でアップできるファイルサイズの上限を定めることができます。とりあえず1MBに設定しておきました。

```app.py
app.config["MAX_CONTENT_LENGTH"] = 1 * 1024 * 1024;
```

## ajaxでいいね機能の実装
jQueryのajaxを利用して、ページ遷移なしで「いいね」「いいね解除」をして、その結果をサーバへ送る機構を実装します。
### html
いいね数を表示する部分をこのように変えました。ログイン済みの場合にはアイコンに`good-btn`クラスを付与しており、さらにいいね済みのコンテンツには`good-btn-active`クラスを付与しています。未ログインユーザにはいいねボタンを押させたくないので、good-btnクラスは付与していません。

```index.html
{% if current_user.is_authenticated %}
    {% if content.good %}
    <i class="far fa-heart good-btn good-btn-active" data-content_id="{{content.id}}"></i>
    {% else %}
    <i class="far fa-heart good-btn" data-content_id="{{content.id}}"></i>
    {% endif %}
{% else %}
<i class="far fa-heart" data-content_id="{{content.id}}"></i>
{% endif %}
<span class="content_good_count">  {{content.good_count}}</span>
```

### css
`.good-btn`上にカーソルを持ってきた際に、色がピンクに・大きさが1.1倍に・カーソルがポインターになるように設定しています。またいいね済みのアイコン色を赤にするよう、`.good-btn-active`に定義しています。

```style.css
.good-btn:hover{
    color : pink;
    font-size : 1.1em;
    cursor : pointer;
}

.good-btn-active{
    color : red;
}
```

### js
`.good-btn`が押された時の挙動を`good.js`に定義します。押されたボタンのDOMからcontent_idを取得して、JSON形式で`/good`にPOSTしています。サーバー側の処理は後ほど書きます。その後、サーバーから最新のいいね数を取得して描画を更新しています。また、`good-btn-active`クラスをスイッチングすることで、「未いいね」⇄「いいね済み」を視覚的に判断できるようにしています。

```good.js
$(function(){
    var $good = $('.good-btn'),
                contentId;
    $good.on('click',function(e){
        e.stopPropagation();
        var $this = $(this);
        var data = JSON.stringify({"content_id":$this.data('content_id')});
        $.ajax({
            type: 'POST',
            url: '/good',
            data: data,
            contentType:'application/json'
        }).done(function(data){
            $this.next().text(data);
            $this.toggleClass('good-btn-active');
        }).fail(function(msg) {
            console.log('Ajax Error');
        });
    });
});
```

### controller
サーバー側での受け取りは例の如く`app.py`に記載していきます。ajaxで送られてきた`content_id`とセッション情報の`user_id`を使って、コンテンツ毎のいいねユーザ情報を管理しているテーブル（後述）に検索をかけます。1つ以上存在していれば「いいね済み」なので管理情報を削除していいねカウントを一つ減らします。存在しなければ「未いいね」なので管理テーブルに情報を追加していいねカウントを一つ増やします。最後にクライアント側にいいね数を返しています。

```app.py
@app.route("/good", methods=["POST"])
@login_required
def good():
    content_id = request.json['content_id']
    content = Content.query.filter_by(id=content_id).all()[0]
    user_id = current_user.id
    content_good_user = ContentGoodUser.query.filter_by(content_id=content_id,user_id=user_id).all()
    if len(content_good_user) >= 1:
        db.session.delete(content_good_user[0])
        content.good_count = content.good_count - 1
    else:
        content_good_user = ContentGoodUser(content_id=content_id,user_id=user_id)
        db.session.add(content_good_user)
        content.good_count = content.good_count + 1
    db.session.commit()
    return str(content.good_count)
```

### model
コンテンツ毎のいいねユーザを管理するテーブルを`models.py`に追加します。

```models.py
class ContentGoodUser(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    content_id = db.Column(db.Integer, db.ForeignKey('content.id'))
    content = db.relationship('Content', backref=db.backref('content_good_user', lazy=True))
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    user = db.relationship('User', backref=db.backref('content_good_user', lazy=True))
```

### 描画
「いいね済み」のコンテンツには`good-btn-active`クラスを付与して描画する必要があります。そこで、いいね済みかの判定機構を追加します。
まず、`Content`クラスに、いいね済みかどうかを保持する`good`変数を追加します。（デフォルトでは`False`にしています。）

```models.py
class Content(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(80), nullable=False)
    body = db.Column(db.Text, nullable=False)
    pub_date = db.Column(db.DateTime, nullable=False,default=datetime.utcnow)
    good_count = db.Column(db.Integer, default=0)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'),nullable=False)
    user = db.relationship('User',backref=db.backref('content', lazy=True))
    good = False
```
そして、ログインユーザのいいね情報とマッチングさせて、表示対象コンテンツにいいね済みのものが含まれているかチェックします。いいね済みであれば`good`を`True`にしています。

```app.py
@app.route("/")
def index():
    contents = Content.query.join(User).order_by(Content.pub_date.desc()).all()
    if current_user.is_authenticated:
        content_good_users = ContentGoodUser.query.filter_by(user_id=current_user.id).all()
        good_content = []
        for content_good_user in content_good_users:
            good_content.append(content_good_user.content_id)
        for content in contents:
            if content.id in good_content:
                content.good = True
    return render_template("index.html", contents=contents)
```
これでいいね済みのコンテンツには`good-btn-active`クラスが無事付与されるようになります。

# Day7
## herokuでサービス公開
### gunicornのインストール
heroku上のWebサーバとflaskをつなぐWSGIをインストールします。

```
$ pipenv install gunicorn
```

### Procfileの作成
herokuへデプロイ後に実行するスクリプトをProcfileに定義します。

```
$ touch Procfile
$ vim Procfile
---
web: gunicorn run:app --log-file=-
```

### git管理ファイルの変更
key.pyをheroku上にアップしたいのと、ユーザーアイコン保存フォルダをdefault.jpg以外heroku上にアップしたくないので、以下のように`.gitignore`を編集します。

```
!/app/key.py
/app/static/uploads/*.jpg
!/app/static/uploads/default.jpg
```
heroku上にアップし終わったらkey.pyを再度`.gitignore`の対象にすることを忘れないようにしましょう。

```
$ git rm --cached app/key.py
```

```
/app/key.py
/app/static/uploads/*.jpg
!/app/static/uploads/default.jpg
```

### DB接続先を変更
heroku上ではPostgreをアドオンとして利用します。その接続先URLがheroku上の環境変数`DATABASE_URL`に記載されているので、それを取得します。「環境変数`DATABASE_URL`が存在しない場合はローカルのものを利用する」と記載することで、ローカルでの実行時とheroku上での実行時でDB接続先を意識しなくても済みます。

```models.py
db_uri = os.environ.get('DATABASE_URL') or 'postgresql://postgres:@localhost/tutorial_blog'
app.config['SQLALCHEMY_DATABASE_URI'] = db_uri
```

### ここまでの変更をgitに保存
リモートへのpushはしません。key.pyが漏れるので。

```
$ git add .
$ git commit -m "prepare for heroku"
```

### herokuへログイン

```
$ heroku login
```

### アプリケーションの作成

```
$ heroku create kiyokiyo-tutorial-blog
```

### herokuへのアップ

```
$ git push heroku master
```

### Postgreアドオンの設定
herokuのWebページからPostgreのアドオンを追加します。
無料で使える「Hobby Dev」プランを利用します。

アドオン追加後にPostgreの初期化を行います。
`heroku run python`でheroku上のPythonConsoleを呼び出して、`db.create_all()`を実行します。
`exit()`でheroku上のPythonConsoleから抜け出します。

```
$ heroku run python
>>> from app.app import app
>>> from app.models import db
>>> db.create_all()
>>> exit()
```

### 起動確認
実際にWebページを開いてみて、想定通り動いているか確認しましょう。

```
$ heroku open
```
意図しない挙動になった場合、`heroku logs`でサーバーログを確認できるので、それ見てエラーハンドリングしましょう。

```
$ heroku logs
```
ちなみに今回作成したブログ投稿サービスはこちらです。
ご自由に投稿してみてください。
https://kiyokiyo-tutorial-blog.herokuapp.com/