DB
リツイートについても、ベースとしたudemyの講座では以下のような仕組みを採用していました。
- Post.retweetUser に投稿をリツイートしたユーザーのIDを配列で保持
- リツイートをした際は Post.retweetData にリツイートした投稿のIDをもち、本文が空の Post を保存する
- User.retweets にユーザーがリツイートした投稿のIDを配列で保持
いいねと同様に、Post.retweetUser と User.retweets は廃止したいので Retweet のテーブルを作成しましょう。
- あるユーザーがリツイートした投稿を、リツイートした日時があたらしい順に表示する
→ posts.retweet_data が空でないものを検索すれば良い - ある投稿をリツイートしたユーザーを、リツイートした日時があたらしい順に表示する
→ retweets を (post, retweeted_at) で検索 - タイムラインに表示する投稿それぞれにリツイートされた件数を表示する。
→ posts.retweet_count - タイムラインに表示する投稿のうち、ログインユーザーがリツイートした投稿はボタンをハイライトする。
→ retweets を (retweeted_by, posted_at) で検索
server/models/retweet.py
from bson.objectid import ObjectId
from database import db
from datetime import datetime
class Retweet:
_collection = db['retweets']
def __init__(self,
post: str,
posted_at: datetime,
retweeted_by: str,
retweeted_at: datetime = None):
self.post = post
self.posted_at = posted_at
self.retweeted_by = retweeted_by
if not retweeted_at:
retweeted_at = datetime.now()
self.retweeted_at = retweeted_at
def create(self):
data = vars(self)
data['post'] = ObjectId(data['post'])
data['retweeted_by'] = ObjectId(data['retweeted_by'])
return self._collection.insert_one(data)
こちらも一応インデックスを貼っておきましょう。
$ python
>>> from models.retweet import Retweet
>>> import pymongo
>>> Retweet._collection.create_index([('post', pymongo.ASCENDING), ('retweeted_at', pymongo.DESCENDING)], name='post__retweeted_at')
>>> Retweet._collection.create_index([('retweeted_by', pymongo.ASCENDING), ('posted_at', pymongo.DESCENDING)], name='retweeted_by__posted_at')
>>> Retweet._collection.create_index([('post', pymongo.ASCENDING), ('retweeted_by', pymongo.DESCENDING)], name='post__retweeted_by', unique=True)
>>> Retweet._collection.index_information()
{'_id_': {'v': 2, 'key': [('_id', 1)]}, 'post__retweeted_at': {'v': 2, 'key': [('post', 1), ('retweeted_at', -1)]}, 'post__retweeted_by': {'v': 2, 'key': [('post', 1), ('retweeted_by', -1)], 'unique': True}, 'retweeted_by__posted_at': {'v': 2, 'key': [('retweeted_by', 1), ('posted_at', -1)]}}
APIの実装
いいねの実装を参考にリツイート処理を実装しようと思いましたが、少し困ったことがでてきました。
自分がいいねしたかどうかをAPIのlikingというフィールドで表現していましたがこれはDBに保存している値ではなくAPIで設定している値なのでretweetのAPIで取得するのが面倒だという点です。
POSTとDELETEの/api/posts//retweet では liking を返さなければ、storeで行っているObject.assignでlikingを上書きしないので一旦その作戦で進めましょう。
実装はいいねとほぼ同じです。
server/models/post.py
from bson.objectid import ObjectId
from database import db
from datetime import datetime
class Post:
_collection = db['posts']
MAX_CONTENT_LENGTH = 140
def __init__(self,
content: str,
posted_by: str,
retweeted_post: str = None,
like_count: int = 0,
retweet_count: int=0,
created_at: datetime = None,
updated_at: datetime = None):
self.content = content
self.posted_by = posted_by
self.retweeted_post = retweeted_post
self.like_count = like_count
self.retweet_count = retweet_count
if not created_at:
created_at = datetime.now()
self.created_at = created_at
if not updated_at:
updated_at = datetime.now()
self.updated_at = updated_at
def create(self):
data = vars(self)
data['posted_by'] = ObjectId(data['posted_by'])
if 'retweeted_post' in data and data['retweeted_post']:
data['retweeted_post'] = ObjectId(data['retweeted_post'])
return self._collection.insert_one(data)
...
server/apis/posts.py
from flask import jsonify, request, session
from bson.objectid import ObjectId
import pymongo
from models.like import Like
from models.post import Post
from models.retweet import Retweet
from models.user import User
from main import app
from apis import login_required
...
@app.route('/api/posts', methods=['GET'])
@login_required()
def list_posts():
results = []
for post in Post._collection.find(sort=[('created_at', pymongo.DESCENDING)]):
results.append(_populate(post))
if results:
user_id = ObjectId(session['user']['_id'])
term = {
'$gte': results[-1]['created_at'],
'$lte': results[0]['created_at']
}
liked_posts = { str(like['post']) for like in Like._collection.find({'liked_by': user_id, 'posted_at': term}) }
retweeted_posts = { str(retweet['post']) for retweet in Retweet._collection.find({'retweeted_by': user_id, 'posted_at': term}) }
for result in results:
result['liking'] = result['_id'] in liked_posts
result['retweeting'] = result['_id'] in retweeted_posts
return jsonify(results)
...
@app.route('/api/posts/<post_id>/retweet', methods=['POST'])
@login_required()
def retweet_post(post_id):
post = Post._collection.find_one({'_id': ObjectId(post_id)})
if not post:
return jsonify({'error': { 'message': '投稿が存在しません' }}), 404
user_id = session['user']['_id']
retweet = Retweet._collection.find_one({'post': ObjectId(post_id), 'retweeted_by': ObjectId(user_id)})
if retweet:
post['retweeting'] = True
return jsonify(_populate(post))
else:
retweet = Retweet(post=post_id,
posted_at=post['created_at'],
retweeted_by=user_id)
try:
retweet.create()
retweeting_post = Post(content=None,
posted_by=user_id,
retweeted_post=post_id)
retweeting_post.create()
except pymongo.errors.DuplicateKeyError:
pass
retweet_count = Retweet._collection.count_documents({'post': ObjectId(post_id)})
post = Post._collection.find_one_and_update(
{'_id': ObjectId(post_id)},
{'$set': {'retweet_count': retweet_count}},
return_document=pymongo.ReturnDocument.AFTER)
post['retweeting'] = True
return jsonify(_populate(post))
@app.route('/api/posts/<post_id>/retweet', methods=['DELETE'])
@login_required()
def unretweet_post(post_id):
post = Post._collection.find_one({'_id': ObjectId(post_id)})
if not post:
return jsonify({'error': { 'message': '投稿が存在しません' }}), 404
user_id = session['user']['_id']
retweet = Retweet._collection.find_one_and_delete({'post': ObjectId(post_id), 'retweeted_by': ObjectId(user_id)})
Post._collection.find_one_and_delete({'posted_by': ObjectId(user_id), 'retweeted_post': ObjectId(post_id)})
if not retweet:
post['retweeting'] = False
return jsonify(_populate(post))
else:
retweet_count = Retweet._collection.count_documents({'post': ObjectId(post_id)})
post = Post._collection.find_one_and_update(
{'_id': ObjectId(post_id)},
{'$set': {'retweet_count': retweet_count}},
return_document=pymongo.ReturnDocument.AFTER)
post['retweeting'] = False
return jsonify(_populate(post))
def _populate(post):
if not post:
return post
post['_id'] = str(post['_id'])
posted_by = User._collection.find_one({'_id': post['posted_by']})
if posted_by:
posted_by['_id'] = str(posted_by['_id'])
post['posted_by'] = posted_by
if 'retweeted_post' in post and post['retweeted_post']:
post['retweeted_post'] = _populate(Post._collection.find_one({'_id': post['retweeted_post']}))
return post
画面
アクションや呼び出し部分はいいねとほぼ同じです。
client/src/store.js
import { createStore } from 'vuex'
import router from '@/router';
import axios from 'axios';
import UIkit from 'uikit';
export default createStore({
state: {
userLoggedIn: undefined,
posts: []
},
getters: {
},
mutations: {
...
},
actions: {
...
},
retweetPost ({ commit }, post) {
axios.post(`/api/posts/${post._id}/retweet`).then(resp => {
commit('updatePost', resp.data);
})
.catch(error => {
if (error.response.status === 401) {
router.push('/login');
} else {
UIkit.notification(error.response.data.error.message, {status: 'danger'});
}
});
},
unretweetPost ({ commit }, post) {
axios.delete(`/api/posts/${post._id}/retweet`).then(resp => {
commit('updatePost', resp.data);
})
.catch(error => {
if (error.response.status === 401) {
router.push('/login');
} else {
UIkit.notification(error.response.data.error.message, {status: 'danger'});
}
});
}
},
modules: {
}
})
ひとまずリツイート自体はできるようになりましたが、リツイートすると空っぽの投稿が表示されるようになってしまいました。
次回はここを修正していきましょう。