チュートリアルをやってみる。ユーザフォロー編
これは以下のチュートリアルをやってみたログである。
- フォロー、フォロー解除できるようにする。
- ユーザ一覧から他の人のユーザプロファイル画面に行き、そこでフォローボタンを押すとフォロー(フォロー解除)できる。
- ユーザの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>
フォローしているユーザページとフォロワーページ
- ユーザのプロファイルがサイドバーにあり、その下にユーザプロファイルの画像が格子状に並ぶ。
- 右側にフォローしているユーザーのリストがページネーションされる。
フォロワーページも同様の表示にしたい。
コントローラに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 #<#
ということでこの調査がいる。
config/routes.rbのresources :relationships...が定義されていなかったので修正した。
フォローボタンの実装
- フォローする =>新しいリレーションシップを作成する
- フォローを解除する =>リレーションシップを削除する
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は結果を表示させないために使用している。