14
21

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.

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

Last updated at Posted at 2019-06-30

今回のテーマ

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

#Day1

仮想環境(pipenv)を使う

pythonの仮想環境系のツールはたくさんあってどれを使えばいいのか分からなかったのですが、この記事曰く「pipenvがpipとかvenvとかをラップしているのでとりあえずpipenvを使えばOK」とのことだったので、今回はpipenvを使います。

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

$ pip install pipenv

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

$ mkdir TutotialBlog
$ cd TutotialBlog/

そして、そのフォルダに対し仮想環境を構築します。

$ pipenv install

こんな感じで、TurorialBlog/以下に.venvPipfilePipfile.lockが作成されていればOKです。

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

Flask基本構成を作る

この記事を参考にFlaskの基本構成を作っていきます。
まずFlaskのインストールから。pipenv install [パッケージ名]でパッケージをインストールできます。

$ pipenv install Flask

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

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

app.pyとrun.pyの中身は一旦こんな感じで作っておきます。

app.py
from flask import Flask

app = Flask(__name__)

if __name__ == "__main__":
    app.run(debug=True)
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のインストール

このページを参考にして行いました。
主に使用したコマンドは以下。

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でイケると思ったらエラー吐いて失敗しました。
このサイトを参考に再度インストールを試してみたらうまくいきました。
なんでエラー吐いたのかは分からずじまいです。

関係モジュールを削除

$ 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で以下のコードを入力してテーブルを作成します。

>>> from app.models import db
>>> db.create_all()

ちゃんと作成されているか試し打ちしてみます。

>>> 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テーブルの方にもサンプルデータを格納しておきます。

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

Day3

メインページ続き

前回はDB設定とModel構築までしか終わらなかったので、今回はViewとControllerを実装します。

index.html

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

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

コントローラです。ここが一番詰まりました(もしかしたら自分だけかもですが)。

  • 詰まりポイント1:python interpreter
    app.pyを書いている途中でPycharmにinvalid python interpreter selected for the projectというメッセージが出ました。Python語を機械語に変換するためのインタープリタの設定が間違っているようです。Pycharmの公式リファレンスを参考に、Pythonインタープリタの設定を行いました。

  • 詰まりポイント2: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,Userapp = Flask(__name__)より下に書いて、appが作成されてからmodels.pyを呼び出すようにしました。

  • 詰まりポイント3:レコードの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で以下のコードを叩くと、ランダムに文字列が生成されます。

>>> 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を使うにあたっての追加事項はこちらです。

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

そして、ログインユーザのいいね情報とマッチングさせて、表示対象コンテンツにいいね済みのものが含まれているかチェックします。いいね済みであればgoodTrueにしています。

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/

14
21
0

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
14
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?