今回はフォロー機能を作っていきます。
ベースの講座と同様に、users.following, users.followers にフォロー中のユーザー
, フォローされているユーザーの ID の配列をそれぞれ保持する形で実装しようと思います。
API
server/models/user.py
...
class User:
_collection = db["users"]
...
def __init__(self, display_name: str, username: str, password: str, email: str):
self.display_name = display_name
self.username = username
self.hashed_password = self.encrypt(password)
self.email = email
self.following = []
self.followers = []
...
server/apis/users.py
...
@app.route("/api/users/<username>/follow", methods=["POST"])
@login_required()
def follow_user(username):
user = User._collection.find_one({"username": username})
if not user:
return jsonify({"error": {"message": "ユーザーが存在しません。"}}), 404
own_id = session['user']['_id']
user_id = user['_id']
User._collection.find_one_and_update(
{"username": username},
{
"$addToSet": {
"followers": ObjectId(own_id)
}
},
return_document=pymongo.ReturnDocument.AFTER,
)
me = User._collection.find_one_and_update(
{"_id": ObjectId(own_id)},
{
"$addToSet": {
"following": ObjectId(user_id)
}
},
return_document=pymongo.ReturnDocument.AFTER,
)
del me['hashed_password']
me['following'] = [str(_id) for _id in me['following']]
me['followers'] = [str(_id) for _id in me['wollowers']]
return jsonify(me)
@app.route("/api/users/<username>/follow", methods=["DELETE"])
@login_required()
def unfollow_user(username):
user = User._collection.find_one({"username": username})
if not user:
return jsonify({"error": {"message": "ユーザーが存在しません。"}}), 404
own_id = session['user']['_id']
user_id = user['_id']
User._collection.find_one_and_update(
{"username": username},
{
"$pull": {
"followers": ObjectId(own_id)
}
},
return_document=pymongo.ReturnDocument.AFTER,
)
me = User._collection.find_one_and_update(
{"_id": ObjectId(own_id)},
{
"$pull": {
"following": ObjectId(user_id)
}
},
return_document=pymongo.ReturnDocument.AFTER,
)
del me['hashed_password']
me['following'] = [str(_id) for _id in me['following']]
me['followers'] = [str(_id) for _id in me['wollowers']]
return jsonify(me)
とりあえずAPIはこんな感じでしょう。
画面
自分以外のユーザーのプロフィール画面で、フォローボタンを表示しましょう。
client/src/store/index.js
...
followUser({ commit }, username) {
return axios.post(`/api/users/${username}/follow`).then((resp) => {
commit("setUserLoggedIn", resp.data);
});
},
unfollowUser({ commit }, username) {
return axios.delete(`/api/users/${username}/follow`).then((resp) => {
commit("setUserLoggedIn", resp.data);
});
},
...
client/src/views/ProfileView.vue
<template>
<div class="uk-width-1-1 uk-position-relative">
<h3 class="uk-margin-small-top uk-margin-small-bottom">
{{ username }}
</h3>
<div class="cover uk-margin-right"></div>
<img
:src="imageUrl(user)"
class="uk-position-absolute uk-border-circle uk-margin-small-left avatar"
/>
<div class="uk-width-1-1 uk-position-absolute" v-if="user._id">
<div
class="uk-flex uk-flex-row uk-flex-top uk-margin-right uk-margin-small-top buttons"
>
<div class="uk-flex-1"></div>
<button
v-if="isMe"
class="uk-button uk-button-default uk-button-small uk-text-bold"
>
プロフィールを編集
</button>
<button
v-else
class="follow uk-button uk-button-small uk-text-bold"
:class="isFollowing ? 'uk-button-default' : 'uk-button-secondary'"
@click="onFollowClick"
>
{{ isFollowing ? "フォロー中" : "フォロー" }}
</button>
</div>
<div class="uk-flex uk-flex-column uk-flex-left uk-margin-small-left">
<div class="uk-text-large uk-text-bold">{{ user.display_name }}</div>
<div>@{{ user.username }}</div>
<div class="uk-margin-small-top">{{ user.description }}</div>
<div class="uk-margin-small-top">
<span class="uk-text-bold">{{ user.following.length }}</span>
<span class="uk-margin-right">フォロー中</span>
<span class="uk-text-bold">{{ user.followers.length }}</span>
<span>フォロワー</span>
</div>
</div>
<div class="uk-margin-top uk-margin-right">
<ul class="uk-child-width-expand" ref="tabs" uk-tab uk-switcher>
<li><a>ツイート</a></li>
<li><a>ツイートと返信</a></li>
<li><a>いいね</a></li>
</ul>
<ul class="uk-switcher">
<div>ツイートです</div>
<div>ツイートと返信です</div>
<div>いいねです</div>
</ul>
</div>
</div>
<div class="uk-position-absolute error-message" v-else>
<h3>ユーザーが存在しません</h3>
</div>
</div>
</template>
<script>
import { computed, onBeforeMount, ref } from "vue";
import { useRoute } from "vue-router";
import { useStore } from "vuex";
import { imageUrl } from "@/functions/avatar.js";
export default {
setup() {
const route = useRoute();
const store = useStore();
const username = route.params.username
? route.params.username
: store.state.userLoggedIn.username;
const isMe = ref(false);
const isFollowing = computed(
() =>
store.state.userLoggedIn.following &&
store.state.userLoggedIn.following.includes(
store.state.profile.user._id
)
);
onBeforeMount(async () => {
await store.dispatch("loadProfileUser", username).catch(() => {
// noop
});
isMe.value = username === store.state.userLoggedIn.username;
});
const user = computed(() => store.state.profile.user);
const onFollowClick = () => {
if (isFollowing.value) {
store
.dispatch("unfollowUser", username)
.then(() => store.dispatch("loadProfileUser", username));
} else {
store
.dispatch("followUser", username)
.then(() => store.dispatch("loadProfileUser", username));
}
};
return {
isMe,
isFollowing,
username,
user,
imageUrl,
onFollowClick,
};
},
};
</script>
<style scoped>
.cover {
background-color: lightslategray;
height: 180px;
}
.avatar {
width: 132px;
height: 132px;
bottom: -66px;
border: 4px solid #fff;
}
.buttons {
height: 60px;
}
.error-message {
margin-top: 30px;
margin-left: 132px;
}
</style>
一応これでフォローとフォロー解除ができるようにはなりました。
そろそろコードがごちゃごちゃしてきたので次回はリファクタリングをしていきたいと思います。