Rodhos Soft

備忘録を兼ねた技術的なメモです。Rofhos SoftではiOSアプリ開発を中心としてAndroid, Webサービス等の開発を承っております。まずはご相談下さい。

チュートリアルをやってみる。ユーザフォロー編

これは以下のチュートリアルをやってみたログである。

railstutorial.jp

  1. フォロー、フォロー解除できるようにする。
    1. ユーザ一覧から他の人のユーザプロファイル画面に行き、そこでフォローボタンを押すとフォロー(フォロー解除)できる。
  2. ユーザのhomeページにフォロー数とリフォロー数を表示する。

データモデル

あるユーザがあるユーザをフォローしているという関係(Relationships)テーブルが必要だろう。
RelationShipsにはfollower_id(あるユーザのID)とfollwed_id(フォローしているユーザのID)があれば良い。

rails generate model Relationship follower_id:integer followed_id:integer


頻繁に使うものなのでカラムにindexを追加する。
マイグレーションファイルで
db/migrate/20150918041458_create_relationships.rb

class CreateRelationships < ActiveRecord::Migration
  def change
    create_table :relationships do |t|
      t.integer :follower_id
      t.integer :followed_id

      t.timestamps
    end
    add_index :relationships, :follower_id
    add_index :relationships, :followed_id
    add_index :relationships, [:follower_id, :followed_id], unique: true
  end
end

複合インデックスをユニークにして定義しているのは、2重にフォローなどができないようにするため。

マイグレーション実行

 bundle exec rake db:migrate
bundle exec rake db:test:prepare

ユーザとrelationshipを関連付ける

app/models/user.rb

class User < ActiveRecord::Base
    has_many :microposts, dependent: :destroy
    has_many :relationships, foreign_key: "follower_id", dependent: :destroy


has_manyにしたのは、一人のユーザに多数のrelationshipsがあるから。
外部キーとしてfollower_idを通して両者はつながる。逆にmicropostsの1:多関係の外部キーが
省略できていたのは、指定しないとrailsが外部キーをクラス_idつまりuser_idと自動で認識するから。

relationship側
app/models/relationship.rb

class Relationship < ActiveRecord::Base
    belongs_to :follower, class_name: "User"
    belongs_to :followed, class_name: "User"
end

さらに検証を追加

>|ruby|
class Relationship < ActiveRecord::Base
    belongs_to :follower, class_name: "User"
    belongs_to :followed, class_name: "User"
    validates :follower_id, presence: true
    validates :followed_id, presence: true
end

同様にrailsはfollowerクラス、followedクラスに属すると推測してしまうのでクラス名Userを指定している。

relationshipsを通してユーザからフォロー、フォロワーを関係付ける。

フォロー

app/models/user.rb

class User < ActiveRecord::Base
    has_many :microposts, dependent: :destroy
    has_many :relationships, foreign_key: "follower_id", dependent: :destroy
    has_many :followed_users, through: :relationships, source: :followed

...
    
    def feed
...
    end
    
    def following?(other_user)
        relationships.find_by(followed_id: other_user.id)
    end
    
    def follow!(other_user)
        relationships.create!(followed_id: other_user.id)
    end

  def unfollow!(other_user)
    relationships.find_by(followed_id: other_user.id).destroy
  end
...
end

フォロワー

ユーザとrelationshipとの関連として今度は"followed_id"を外部キーとすれば良い。

app/models/user.rb

class User < ActiveRecord::Base
...    
    has_many :reverse_relationships, foreign_key: "followed_id",
    class_name:  "Relationship",
    dependent:   :destroy
    has_many :followers, through: :reverse_relationships, source: :follower
....

ここもReverseRelationshipクラスはないのでRelationshipクラスを明示している。

ユーザインターフェース作成

フォローしているユーザのサンプルを作る。

メソッド分割している。
すでに作成しているfollowメソッドを使うことでシンプルにフォロー関係を作れる。

lib/tasks/sample_data.rake

namespace :db do
  desc "Fill database with sample data"
  task populate: :environment do
    make_users
    make_microposts
    make_relationships
  end
end


def make_users
  admin = User.create!(name: "Example User",
                   email: "example@railstutorial.jp",
                   password: "foobar",
                   password_confirmation: "foobar",
                   admin: true)

  99.times do |n|
    name  = Faker::Name.name
    email = "example-#{n+1}@railstutorial.jp"
    password  = "password"
    User.create!(name: name,
                 email: email,
                 password: password,
                 password_confirmation: password)
  end
end

def make_microposts
  users = User.all(limit: 6)
  50.times do
    content = Faker::Lorem.sentence(5)
    users.each { |user| user.microposts.create!(content: content)}
  end
end

def make_relationships
  users = User.all
  user  = users.first
  followed_users = users[2..50]
  followers      = users[3..40]
  followed_users.each { |followed| user.follow!(followed) }
  followers.each      { |follower| follower.follow!(user) }
end

サンプル生成

 bundle exec rake db:reset
 bundle exec rake db:populate
 bundle exec rake db:test:prepare

フォロー、フォロワー数表示

ルーティング設定

urlとして
"/users/1/following"や"/users/1/followers"のように表示したい。

そのようなルーティングを設定する。

config/routes.rb

SampleApp::Application.routes.draw do
    resources :users do
        member do
            get :following, :followers
        end
    end
...

名前付きルートは
followingアクションでfollowing_user_path(1)
followersアクションでfollowers_user_path(1)
となる。

パーシャル作成

app/views/shared/_stats.html.erb

<% @user ||= current_user %>
<div class="stats">
  <a href="<%= following_user_path(@user) %>">
    <strong id="following" class="stat">
      <%= @user.followed_users.count %>
    </strong>
    following
  </a>
  <a href="<%= followers_user_path(@user) %>">
    <strong id="followers" class="stat">
      <%= @user.followers.count %>
    </strong>
    followers
  </a>
</div>


ホームページに表示

app/views/static_pages/home.html.erb

<% if signed_in? %>
...
      <section>
        <%= render 'shared/user_info' %>
      </section>
      <section>
        <%= render 'shared/stats' %>
      </section>
      <section>
        <%= render 'shared/micropost_form' %>
      </section>
...
<% else %>
...
<% end %>

スタイル追加

...
/* sidebar */
...
.stats {
  overflow: auto;
  a {
    float: left;
    padding: 0 10px;
    border-left: 1px solid $grayLighter;
    color: gray;
    &:first-child {
      padding-left: 0;
      border: 0;
    }
    &:hover {
      text-decoration: none;
      color: $blue;
    }
  }
  strong {
    display: block;
  }
}

.user_avatars {
  overflow: auto;
  margin-top: 10px;
  .gravatar {
    margin: 1px 1px;
  }
}
....
フォロー、フォロー解除用パーシャル

app/views/users/_follow_form.html.erb

<% unless current_user?(@user) %>
  <div id="follow_form">
  <% if current_user.following?(@user) %>
    <%= render 'unfollow' %>
  <% else %>
    <%= render 'follow' %>
  <% end %>
  </div>
<% end %>

app/views/users/_follow.html.erb

<%= form_for(current_user.relationships.build(followed_id: @user.id)) do |f| %>
  <div><%= f.hidden_field :followed_id %></div>
  <%= f.submit "Follow", class: "btn btn-large btn-primary" %>
<% end %>

新しいrelationshipを作成している。

app/views/users/_unfollow.html.erb

<%= form_for(current_user.relationships.find_by(followed_id: @user.id),
             html: { method: :delete }) do |f| %>
  <%= f.submit "Unfollow", class: "btn btn-large" %>
<% end %>

既存relationshipを見つけそれをdeleteする。

ルーティングにrelationship用のルートを追加
config/routes.rb

SampleApp::Application.routes.draw do
...    
    resources :sessions, only: [:new, :create, :destroy]
    resources :microposts, only: [:create, :destroy]
    resources :relationships, only: [:create, :destroy]
ユーザプロファイルページに追加
<% provide(:title, @user.name) %>
<div class="row">
  <aside class="span4">
    <section>
      <h1>
        <%= gravatar_for @user %>
        <%= @user.name %>
      </h1>
    </section>
    <section>
      <%= render 'shared/stats' %>
    </section>
  </aside>

  <div class="span8">
  <%= render 'follow_form' if signed_in? %>
    ....
  </div>

</div>

フォローしているユーザページとフォロワーページ

  1. ユーザのプロファイルがサイドバーにあり、その下にユーザプロファイルの画像が格子状に並ぶ。
  2. 右側にフォローしているユーザーのリストがページネーションされる。

フォロワーページも同様の表示にしたい。

コントローラにfollowingアクション追加
class UsersController < ApplicationController
    
    before_action :signed_in_user,
    only: [:index, :edit, :update, :destroy, :following, :followers]
    
....    
    
    def following
        @title = "Following"
        @user = User.find(params[:id])
        @users = @user.followed_users.paginate(page: params[:page])
        render 'show_follow'
    end
    
    def followers
        @title = "Followers"
        @user = User.find(params[:id])
        @users = @user.followers.paginate(page: params[:page])
        render 'show_follow'
    end
    
    
    
    private
    
...
end

***ビューの作成

app/views/users/show_follow.html.erb
>|html|
<% provide(:title, @title) %>
<div class="row">
  <aside class="span4">    
    <section>
      <%= gravatar_for @user %>
      <h1><%= @user.name %></h1>
      <span><%= link_to "view my profile", @user %></span>
      <span><b>Microposts:</b> <%= @user.microposts.count %></span>
    </section>
    <section>
      <%= render 'shared/stats' %>
      <% if @users.any? %>
        <div class="user_avatars">
          <% @users.each do |user| %>
            <%= link_to gravatar_for(user, size: 30), user %>
          <% end %>
        </div>
      <% end %>
    </section>
  </aside>
  <div class="span8">
    <h3><%= @title %></h3>
    <% if @users.any? %>
      <ul class="users">
        <%= render @users %>
      </ul>
      <%= will_paginate %>
    <% end %>
  </div>
</div>

一度ローカルサーバで確認する。

rails server

NoMethodError in Users#show
undefined method `relationships_path' for #<#:0x007fecd7a6a6a0><%= form_for(current_user.relationships.build(followed_id: @user.id)) do |f| %>
ということでこの調査がいる。

config/routes.rbのresources :relationships...が定義されていなかったので修正した。

フォローボタンの実装

  1. フォローする =>新しいリレーションシップを作成する
  2. フォローを解除する =>リレーションシップを削除する

Relationshipsコントローラのcreate,destroyアクションの実装を行う。

app/controllers/relationships_controller.rb

class RelationshipsController < ApplicationController
  before_action :signed_in_user

  def create
    @user = User.find(params[:relationship][:followed_id])
    current_user.follow!(@user)
    redirect_to @user
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow!(@user)
    redirect_to @user
  end
end

フォローボタンをAjax

app/views/users/_follow.html.erb

<%= form_for(current_user.relationships.build(followed_id: @user.id),
             remote: true) do |f| %>
  <div><%= f.hidden_field :followed_id %></div>
  <%= f.submit "Follow", class: "btn btn-large btn-primary" %>
<% end %>

app/views/users/_unfollow.html.erb

<%= form_for(current_user.relationships.find_by(followed_id: @user.id),
             html: { method: :delete },
             remote: true) do |f| %>
  <%= f.submit "Unfollow", class: "btn btn-large" %>
<% end %>

remote:trueにすることで自動的にAjaxを使用するようになる。

Ajaxリクエストに対応するようにコントローラ側のリダイレクトしてる部分を書き換える。

app/controllers/relationships_controller.rb

class RelationshipsController < ApplicationController
  before_action :signed_in_user

  def create
    @user = User.find(params[:relationship][: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

respond_to はリクエストに応じて続く行の中から一つだけが実行される。

railsは対応するアクションと同じ名前のjsファイルを読みに行くのでそれらを用意する。

app/views/relationships/create.js.erb

$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>")
$("#followers").html('<%= @user.followers.count %>')

app/views/relationships/destroy.js.erb

$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>")
$("#followers").html('<%= @user.followers.count %>')

escape_javascriptは結果を表示させないために使用している。