RailsTutorialを参考に、DeviseユーザーでTwitterライクなフォロー機能を作りました。Deviseのルーティングやメソッドなどを使っています。
環境はこちらです
1 2 3 |
ruby 2.5.1 rails 5.1.6 devise 4.4.3 |
Relationshipモデルの作成
1 |
$ rails g model Relationship follower_id:integer followed_id:integer |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class CreateRelationships < ActiveRecord::Migration[5.1] def change create_table :relationships do |t| t.integer :follower_id t.integer :followed_id t.timestamps null: false end add_index :relationships, :follower_id add_index :relationships, :followed_id add_index :relationships, [:follower_id, :followed_id], unique: true end end |
複合キーインデックスは、follower_idとfollowed_idの組み合わせを必ずユニークにし、あるユーザーが同じユーザーを2回以上フォローすることを防いでいます。
1 |
$ rails db:migrate |
UserとRelationshipの関連付け
app/models/relationship.rb
1 2 3 4 5 6 7 |
class Relationship < ApplicationRecord belongs_to :follower, class_name: "User" belongs_to :followed, class_name: "User" validates :follower_id, presence: true validates :followed_id, presence: true end |
仮想的にUserモデルを2つに分けるためにactive_relationshipsとpassive_relatoinshipsというふうに別名をつけています。following, followersはそれぞれsourceから名前を上書きしています。followingによる関連付けを使ってfollow、unfollow、following?メソッドを作成します。ここではUserは2つとなるのでselfではなくother_userを使います。
app/models/user.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class User < ActiveRecord::Base has_many :active_relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy has_many :passive_relationships, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy has_many :following, through: :active_relationships, source: :followed has_many :followers, through: :passive_relationships, source: :follower # ユーザーをフォローする def follow(other_user) active_relationships.create(followed_id: other_user.id) end # ユーザーをアンフォローする def unfollow(other_user) active_relationships.find_by(followed_id: other_user.id).destroy end # 現在のユーザーがフォローしてたらtrueを返す def following?(other_user) following.include?(other_user) end end |
ルーティング
本記事ではdevise_scopeでルーティングを設定しましたが、showページの代わりとしてprofileを作っています。そして、フォローユーザー一覧のfollowingと、フォロワー一覧のfollowersを設定します。また、relationshipsリソースを追加して、リレーションシップの作成(create)、削除(destroy)をできるようにします。
config/routes.rb
1 2 3 4 5 6 7 8 9 10 11 |
Rails.application.routes.draw do devise_scope :user do get 'profile/:id/' => 'users/registrations#profile', as: 'profile' get 'profile/:id/following', to: 'users/registrations#following', as: 'following' get 'profile/:id/followers', to: 'users/registrations#followers', as: 'followers' end resources :relationships, only: [:create, :destroy] end |
必要なテンプレートの作成
フォロワーの統計情報を表示するパーシャルの作成です。idのfollowingとfollowersはのちにajaxリクエストに使うものです。
app/views/devise/shared/_stats.html.erb
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<% @user ||= current_user %> <div class="stats"> <a href="<%= following_path(@user) %>">フォロー <strong id="following" class="stat"> <%= @user.following.count %> </strong> </a> <a href="<%= followers_path(@user) %>">フォロワー <strong id="followers" class="stat"> <%= @user.followers.count %> </strong> </a> </div> |
次の記述では上記の統計情報の表示と、follow/unfollowのボタンを表示させるものですが、ログインしているユーザーのみにとしています。
app/views/devise/registrations/profile.html.erb
1 2 3 4 5 6 |
<section class="stats"> <%= render 'devise/shared/stats' %> </section> <section> <%= render 'devise/shared/follow_form' if user_signed_in? %> </section> |
次のパーシャルでfollowとunfollowのパーシャルに分岐しています。
app/views/devise/shared/_follow_form.html.erb
1 2 3 4 5 6 7 8 9 |
<% unless @user == current_user %> <div id="follow_form"> <% if current_user.following?(@user) %> <%= render 'devise/shared/unfollow' %> <% else %> <%= render 'devise/shared/follow' %> <% end %> </div> <% end %> |
followボタンとunfollowボタンとなるパーシャルは、それぞれremote:trueでajaxリクエストします。
app/views/devise/shared/_follow.html.erb
1 2 3 4 |
<%= form_for(current_user.active_relationships.build, remote: true) do |f| %> <div><%= hidden_field_tag :followed_id, @user.id %></div> <%= f.submit "フォローする", class: "follow-btn" %> <% end %> |
app/views/devise/shared/_unfollow.html.erb
1 2 3 4 |
<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id), html: { method: :delete }, remote:true) do |f| %> <%= f.submit "フォロー中", class: "unfollow-btn" %> <% end %> |
followingアクションとfollowersアクション
次の2つのアクションをshow_followページ一つで読み込むようにしています。
app/controllers/users/registrations_controller.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 |
def following @title = "フォロー" @user = User.friendly.find(params[:id]) @users = @user.following render 'show_follow' end def followers @title = "フォロワー" @user = User.friendly.find(params[:id]) @users = @user.followers render 'show_follow' end |
app/views/devise/registrations/show_follow.html.erb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<div> <h1><%= @user.name %></h1> <section class="stats"> <%= render 'devise/shared/stats' %> </section> <div> <h3><%= @title %></h3> <% if @users.any? %> <ul class="users follow"> <% @users.each do |user| %> <li><%= link_to user.name, "#" %></li> <% end %> </ul> <% end %> </div> </div> |
RelationshipsコントローラでAjaxリクエストに対応
1 |
$ rails g controller Relationships |
app/controllers/relationships_controller.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class RelationshipsController < ApplicationController before_action :authenticate_user! def create @user = User.find(params[:followed_id]) current_user.follow(@user) respond_to do |format| format.html { redirect_to @user } format.js end end def destroy @user = Relationship.find(params[:id]).followed current_user.unfollow(@user) respond_to do |format| format.html { redirect_to @user } format.js end end end |
これらのアクションが呼び出すjs.erbファイルを作成します。
app/views/relationships/create.js.erb
1 2 |
$("#follow_form").html("<%= escape_javascript(render('devise/shared/unfollow')) %>"); $("#followers").html('<%= @user.followers.count %>'); |
app/views/relationships/destroy.js.erb
1 2 |
$("#follow_form").html("<%= escape_javascript(render('devise/shared/follow')) %>"); $("#followers").html('<%= @user.followers.count %>'); |
以上で完成です!最後までご覧いただきありがとうございました!もし、間違っている点などありましたらご指摘いただけると幸いです。