今回のテーマ
次のテーマは
— きよきよ (@kiyokiyo_kzsby) 2019年6月29日
・仮想環境使う
・GitHub使う
・Postgre使う
・Flask-session/Flask-SQLAlchemy使う
・フロント側に少し力を入れる
でいきます
題材は無難にブログとかでいいかな?
ということで、これらを踏まえて1週間でブログ投稿サービスを作っていきたいと思います。
#Day1
仮想環境(pipenv)を使う
pythonの仮想環境系のツールはたくさんあってどれを使えばいいのか分からなかったのですが、この記事曰く「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基本構成を作る
この記事を参考にFlaskの基本構成を作っていきます。
まずFlaskのインストールから。pipenv install [パッケージ名]
でパッケージをインストールできます。
$ pipenv install Flask
続いて以下のフォルダ・ファイルを作っていきます。
TutorialBlog/
├app/
│ ├templates/
│ ├static/
│ └app.py
└run.py
app.pyとrun.pyの中身は一旦こんな感じで作っておきます。
from flask import Flask
app = Flask(__name__)
if __name__ == "__main__":
app.run(debug=True)
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
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、コンテンツタイトル、いいね数を表示させます。
<!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から飛んで、特定コンテンツの詳細を表示させます。
<!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,User
をapp = 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を介せばアクセスできます。
最終的にこんな感じになりました。
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として格納しましょう。
SECRET_KEY = "lae0GBuXY19le4QqZmIG"
SALT = "14qO12UpjEiaeeewz5pl"
modelの修正
ユーザー情報を管理するクラスにUserMixinクラスを継承させます。
これにより、login_user/logout_userの処理に、そのクラスを利用することができるようになります。
UserMixinをインポートして、Userクラスの引数にUserMixinを与えてあげます。
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}}
で現在ログインしているユーザのユーザ名を取得できます。
追加修正したページは以下です。
<!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>
<!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>
<!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>
<!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を継承しているオブジェクトを返す処理を書いています。これが無いと、セッション情報をうまく管理できないようです。
あとは、ルーティングに合わせて書きたい処理を書いています。
主に、ログイン、ユーザの登録、ログアウト、マイページへの遷移等の記述を追加しました。
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も載せます。
<!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>
{% 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を読み込むスクリプトを追加したり、地味に変更加えています。
プロフィール画像のアップロード
画像アップロード用フォーム
{% 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 %}
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.config["MAX_CONTENT_LENGTH"] = 1 * 1024 * 1024;
ajaxでいいね機能の実装
jQueryのajaxを利用して、ページ遷移なしで「いいね」「いいね解除」をして、その結果をサーバへ送る機構を実装します。
html
いいね数を表示する部分をこのように変えました。ログイン済みの場合にはアイコンにgood-btn
クラスを付与しており、さらにいいね済みのコンテンツにはgood-btn-active
クラスを付与しています。未ログインユーザにはいいねボタンを押させたくないので、good-btnクラスは付与していません。
{% 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
に定義しています。
.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
クラスをスイッチングすることで、「未いいね」⇄「いいね済み」を視覚的に判断できるようにしています。
$(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.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
に追加します。
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
にしています。)
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.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接続先を意識しなくても済みます。
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/