Rails-tutorialのまとめ14.25(FollowingとFollowersページ)

その14.2から続く

14.2.3 [Following] と [Followers] ページ

フォローしているユーザーを表示するページと、フォロワーを表示するページは、
いずれもプロフィールページとユーザー一覧ページを合わせたような作りになるという点で似ています。
どちらにもフォローの統計情報などのユーザー情報を
表示するサイドバーと、ユーザーのリストがあります。
さらに、サイドバーには小さめのユーザープロフィール画像のリンクを格子状に並べて表示する予定です。

フォローしているユーザーのリンクとフォロワーのリンクを
動くようにすることでTwitterに倣って、
どちらのページでもユーザーのログインを要求するようにします。

フォローしているユーザーのリンクとフォロワーのリンクを動くようにすることです。 Twitterに倣って、どちらのページでもユーザーのログインを要求するようにします。そこで前回のアクセス制御と同様に、まずはテストから書いていきましょう。

フォロー/フォロワーページの認可をテストする red
test/controllers/users_controller_test.rb

require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

 def setup
  @user = users(:michael)
  @other_user = users(:archer)
 end
.
.
.
 test "should redirect following when not logged in" do
  get following_user_path(@user)
  assert_redirected_to login_url
 end

 test "should redirect followers when not logged in" do
  get followers_user_path(@user)
  assert_redirected_to login_url
 end
end

この実装には1つだけトリッキーな部分があります。
それはUsersコントローラに2つの新しいアクションを追加する必要があるということです。

定義した2つのルーティングにもとづいており、これらはそれぞれfollowingおよびfollowersと呼ぶ必要があります。
それぞれのアクションでは、タイトルを設定し、ユーザーを検索し、@user.followingまたは@user.followersからデータを取り出し、
ページネーションを行なって、ページを出力する必要があります。

followingアクションとfollowersアクション red
app/controllers/users_controller.rb

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy,
                                        :following, :followers]
  .
  .
  .
  def following
    @title = "Following"
    @user  = User.find(params[:id])
    @users = @user.following.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

Railsは慣習に従って、アクションに対応するビューを暗黙的に呼び出します。
例えば、showアクションの最後でshow.html.erbを呼び出す、といった具合です。

renderを明示的に呼び出し、show_followという同じビューを出力しています。つまり、作成が必要なビューはこれ1つです。
renderで呼び出しているビューが同じである理由は、
ERbはどちらの場合でもほぼ同じであり、で両方の場合をカバーできるためです。

↓フォローしているユーザーとフォロワーの両方を表示するshow_followビュー green
app/views/users/show_follow.html.erb

<% provide(:title, @title) %>
<div class="row">
 <aside class="col-md-4">
  <section class="user_info">
   <%= 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 class="stats">
  <%= 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="col-md-8">
 <h3><%= @title %></h3>
  <% if @users.any? %>
   <ul class="users follow">
    <%= render @users %>
   </ul>
  <%= will_paginate %>
 <% end %>
 </div>
</div>

 

followingとfollowersアクションは、2通りの方法でビューを呼び出します。“following”をとおって描画したビューを↓に、
“followers”をとおって描画したビューを↓に示します。
このとき、上のコードでは現在のユーザーを一切使っていない点に注目してください。したがって、他のユーザーのフォロワー一覧ページもうまく動きます。

show_followの描画結果を確認するため、統合テストを書いていきます。ただし今回の統合テストは基本的なテストだけに留めており、
網羅的なテストにはしていません。

HTML構造を網羅的にチェックするテストは壊れやすく、
生産性を逆に落としかねないからです。したがって今回は、
正しい数が表示されているかどうかと、
正しいURLが表示されているかどうかの2つのテストを書きます。

統合テストを生成するところから始めます。

rails generate integration_test following

リレーションシップ用のfixtureにデータを追加しましょう。
次のように書くことで、

orange:
content: "I just ate an orange!"
created_at: <%= 10.minutes.ago %>
user: michael

ユーザーとマイクロポストを関連付けできたことを思い出してください。上のコードではユーザー名を次のように書いていますが、

user: michael

これは内部的には次のようなコードに自動的に変換されます。

user_id: 1

この例を参考にしてRelationship用のfixtureにテストデータを追加すると、
↓のようになります。

following/followerをテストするためのリレーションシップ用fixture
test/fixtures/relationships.yml

one:
follower: michael
followed: lana

two:
follower: michael
followed: malory

three:
follower: lana
followed: michael

four:
follower: archer
followed: michael

前半の2つでMichaelがLanaとMaloryをフォローし、後半の2つでLanaとArcherがMichaelをフォローしています。

あとは、正しい数かどうかを確認するために、assert_matchメソッドを使ってプロフィール画面のマイクロポスト数をテストします。
さらに、正しいURLかどうかをテストするコードも加えると、

following/followerページのテスト green
test/integration/following_test.rb

require 'test_helper'

class FollowingTest < ActionDispatch::IntegrationTest

 def setup
   @user = users(:michael)
   log_in_as(@user)
 end

test "following page" do
 get following_user_path(@user)
 assert_not @user.following.empty?
 assert_match @user.following.count.to_s, response.body
 @user.following.each do |user|
  assert_select "a[href=?]", user_path(user)
 end
end

test "followers page" do
 get followers_user_path(@user)
 assert_not @user.followers.empty?
 assert_match @user.followers.count.to_s, response.body
  @user.followers.each do |user|
   assert_select "a[href=?]", user_path(user)
  end
 end
end
assert_not @user.following.empty?

このコードは次のコードを確かめるためのテストであって、

@user.following.each do |user|
  assert_select "a[href=?]", user_path(user)
end

もし@user.following.empty?の結果がtrueであれば、
assert_select内のブロックが実行されなくなるため、
その場合においてテストが適切なセキュリティモデルを確認できなくなることを防いでいます。

演習

1:ブラウザから /users/1/followers と /users/1/following を開き、
それぞれが適切に表示されていることを確認してみましょう。
サイドバーにある画像は、リンクとしてうまく機能しているでしょうか?

機能している

2:assert_selectに関連するコードをコメントアウトしてみて、
テストが正しく red に変わることを確認してみましょう。

#以下をコメントアウトする
<% @users.each do |user| %>
<%= #link_to gravatar_for(user, size: 30), user %>
<% end %>

14.2.4 [Follow] ボタン (基本編)

ビューが整ってきました。いよいよ [Follow] / [Unfollow] ボタンを
動作させましょう。フォローとフォロー解除はそれぞれリレーションシップの作成と削除に対応しているため、まずはRelationshipsコントローラが必要です。
いつものようにコントローラを生成しましょう。

rails generate controller Relationships

Relationshipsコントローラのアクションでアクセス制御することは
そこまで難しくありません。しかし、前回のアクセス制御のときと同様に最初にテストを書き、
それをパスするように実装することでセキュリティモデルを
確立させていきましょう。

コントローラのアクションにアクセスするとき、
ログイン済みのユーザーであるかどうかをチェックします。
もしログインしていなければログインページにリダイレクトされるので、Relationshipのカウントが変わっていないことを確認します

リレーションシップの基本的なアクセス制御に対するテスト
test/controllers/relationships_controller_test.rb

require 'test_helper'

class RelationshipsControllerTest < ActionDispatch::IntegrationTest

  test "create should require logged-in user" do
    assert_no_difference 'Relationship.count' do
      post relationships_path
    end
    assert_redirected_to login_url
  end

  test "destroy should require logged-in user" do
    assert_no_difference 'Relationship.count' do
      delete relationship_path(relationships(:one))
    end
    assert_redirected_to login_url
  end
end

テストをパスさせるために、logged_in_userフィルターを
Relationshipsコントローラのアクションに対して追加します

リレーションシップのアクセス制御
app/controllers/relationships_controller.rb

class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
  end

  def destroy
  end
end

[Follow] / [Unfollow] ボタンを動作させるためには、
フォームから送信されたパラメータを使って、
followed_idに対応するユーザーを見つけてくる必要があります。
その後、見つけてきたユーザーに対して適切にfollow/unfollowメソッドを使います。

Relationshipsコントローラapp/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

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

もしログインしていないユーザーが (curlなどのコマンドラインツールなどを使って) これらのアクションに直接アクセスするようなことがあれば、current_userはnilになり、どちらのメソッドでも2行目で例外が発生します。
エラーにはなりますが、アプリケーションやデータに影響は生じません。

演習

1:ブラウザ上から /users/2 を開き、[Follow] と [Unfollow] を実行してみましょう。うまく機能しているでしょうか?

OK

2:先ほどの演習を終えたら、Railsサーバーのログを見てみましょう。
フォロー/フォロー解除が実行されると、
それぞれどのテンプレートが描画されているでしょうか?

両方とも “/users/2″が描画されている

14.2.5 [Follow] ボタン (Ajax編)

Relationshipsコントローラのcreateアクションとdestroyアクションを単に元のプロフィールにリダイレクトしていました。
つまり、ユーザーはプロフィールページを最初に表示し、
それからユーザーをフォローし、その後すぐ元のページにリダイレクトされるという流れになります。
ユーザーをフォローした後、本当にそのページから離れて元のページに戻らないといけないのでしょうか。この点を考えなおしてみましょう。

Ajaxを使うことで解決できます。Ajaxを使えば、
Webページからサーバーに「非同期」で、
ページを移動することなくリクエストを送信することができます。
WebフォームにAjaxを採用するのは今や当たり前になりつつあるので、RailsでもAjaxを簡単に実装できるようになっています。
フォロー用とフォロー解除用のパーシャルをこれに沿って更新するのは簡単です。
次のコードがあるとすると、

form_for

上のコードを次のように置き換えるだけです。

form_for ..., remote: true

たったこれだけで、Railsは自動的にAjaxを使うようになります。
具体的な更新の結果を、↓に示します。

Ajaxを使ったフォローフォーム

app/views/users/_follow.html.erb

<%= form_for(current_user.active_relationships.build, remote: true) do |f| %>
Ajaxを使ったフォロー解除フォームapp/views/users/_unfollow.html.erb
<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
             html: { method: :delete }, remote: true) do |f| %>
  <%= f.submit "Unfollow", class: "btn" %>
<% end %>

ERbによって実際に生成されるHTML

<form action="/relationships/117"
class="edit_relationship" data-remote="true"
id="edit_relationship_117" method="post">
.
.
.
</form>

formタグの内部でdata-remote=”true”を設定しています。
これは、JavaScriptによるフォーム操作を許可することをRailsに知らせるためのものです。
Rails 2以前では、完全なJavaScriptのコードを挿入する必要がありました。
しかし先ほどの例で見たように、現在のRailsではHTMLプロパティを使って簡単にAjaxが扱えるようになっています。
これは、JavaScriptを前面に出すべからずという哲学に従っています。

フォームの更新が終わったので、
今度はこれに対応するRelationshipsコントローラを改造して、
Ajaxリクエストに応答できるようにしましょう。
こういったリクエストの種類によって応答を場合分けするときは、
respond_toメソッドというメソッドを使います。

respond_to do |format|
  format.html { redirect_to user }
  format.js
end

上の(ブロック内の)コードのうち、
いずれかの1行が実行されるという点が重要です
(このためrespond_toメソッドは、上から順に実行する逐次処理というより、if文を使った分岐処理に近いイメージです)。

RelationshipsコントローラでAjaxに対応させるために、
respond_toメソッドをcreateアクションとdestroyアクション
にそれぞれ追加してみましょう。

変更の結果を↓に示します。このとき、ユーザーのローカル変数 (user)をインスタンス変数 (@user) に変更した点に注目してください。
これは、前のときはインスタンス変数は必要なかったのですが、
先程のAjaxを実装したことにより、インスタンス変数が必要になったためです。

RelationshipsコントローラでAjaxリクエストに対応する
app/controllers/relationships_controller.rb

class RelationshipsController < ApplicationController
before_action :logged_in_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

(ビューで変数を使うため、userが@userに変わった点にも気をつけてください。)

Ajaxリクエストに対応したので、今度はブラウザ側でJavaScriptが
無効になっていた場合(Ajaxリクエストが送れない場合)
でもうまく動くようにします。

JavaScriptが無効になっていたときのための設定
config/application.rb

require File.expand_path('../boot', __FILE__)
.
.
.
module SampleApp
 class Application < Rails::Application
.
.
.
# 認証トークンをremoteフォームに埋め込む
  config.action_view.embed_authenticity_token_in_remote_forms = true
 end
end

一方で、JavaScriptが有効になっていても、
まだ十分に対応できていない部分があります。というのも、
Ajaxリクエストを受信した場合は、
Railsが自動的にアクションと同じ名前を持つJavaScript用の
埋め込みRuby(.js.erb)ファイル(create.js.erbやdestroy.js.erbなど)を
呼び出すからです。

これらのファイルではJavaScriptとERb をミックスして現在のページに対するアクションを実行することができます。
ユーザーをフォローしたときや、フォロー解除したときにプロフィールページを更新するために、
私たちがこれから作成および編集しなければならないのは、まさにこれらのファイルです。

JS-ERbファイルの内部では、DOM(Document Object Model)を使ってページを操作するため、RailsがjQuery JavaScriptヘルパーを
自動的に提供しています。
これによりjQueryライブラリの膨大なDOM操作用メソッドが使えるようになりますが、今回使うのはわずか2つです。1つずつ見ていきましょう。

まずはドル記号 ($) とCSS idを使って、DOM要素にアクセスする文法について知る必要があります。
例えばfollow_formの要素をjQueryで操作するには、
次のようにアクセスします

$("#follow_form")

これはフォームを囲むdivタグであり、フォームそのものではなかったことを思い出してください。jQueryの文法はCSSの記法から影響を受けており、#シンボルを使ってCSSのidを指定します。
ご想像のとおり、jQueryはCSSと同様、ドット.を使ってCSSクラスを操作できます。

次に必要なメソッドはhtmlです。これは、引数の中で指定された要素の内側にあるHTMLを更新します。
例えばフォロー用フォーム全体を”foobar”という
文字列で置き換えたい場合は、次のようなコードになります。

$("#follow_form").html("foobar")

純粋なJavaScriptと異なり、JS-ERbファイルでは組み込みRuby(ERb)が使えます。create.js.erbファイルでは、
フォロー用のフォームをunfollowパーシャルで更新し、
フォロワーのカウントを更新するのにERbを使っています
(もちろんこれは、フォローに成功した場合の動作です)。
変更の結果を↓に示します。このコードではescape_javascriptメソッドを使っている点に注目してください。
escape_javascriptメソッドは、JavaScriptファイル内にHTMLを挿入するときに実行結果をエスケープするために必要です。

JavaScriptと埋め込みRubyを使ってフォローの関係性を作成する
app/views/relationships/create.js.erb

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

各行の末尾にセミコロン ; があることに注目

destroy.js.erbファイルの方も同様です
Ruby JavaScript (RJS) を使ってフォローの関係性を削除するapp/views/relationships/destroy.js.erb

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

これらのコードにより、プロフィールページを更新させずに
フォローとフォロー解除ができるようになったはずです。

演習

確認が終わったら、Railsサーバーのログを閲覧し、フォロー/フォロー解除を実行した直後のテンプレートがどうなっているか確認してみましょう。

Rendered relationships/create.js.erb

Rendered relationships/destroy.js.erb

js.erbファイルがレンダリングされている。

14.2.6 フォローをテストする

フォローボタンが動くようになったので、
バグを検知するためのシンプルなテストを書いていきましょう。
ユーザーのフォローに対するテストでは、
/relationshipsに対してPOSTリクエストを送り、
フォローされたユーザーが1人増えたことをチェックします。

assert_difference '@user.following.count', 1 do
 post relationships_path, params: { followed_id: @other.id }
end

これは標準的なフォローに対するテストではありますが、Ajax版もやり方は大体同じです。Ajaxのテストでは、xhr :trueオプションを使うようにするだけです。

assert_difference '@user.following.count', 1 do
 post relationships_path, params: { followed_id: @other.id }, xhr: true
end

xhr (XmlHttpRequest) というオプションをtrueに設定すると、
Ajaxでリクエストを発行するように変わります。
したがって、respond_toでは、
JavaScriptに対応した行が実行されるようになります。

ユーザーをフォロー解除するときも構造はほとんど同じで、
postメソッドをdeleteメソッドに置き換えてテストします。
つまり、そのユーザーのidとリレーションシップのidを使って
DELETEリクエストを送信し、フォローしている数が1つ減ることを確認します。したがって、実際に加えるテストは、

assert_difference '@user.following.count', -1 do
 delete relationship_path(relationship)
end

上の従来どおりのテストと、下のAjax用のテストの2つになります。

assert_difference '@user.following.count', -1 do
 delete relationship_path(relationship), xhr: true
end

[Follow] / [Unfollow] ボタンをテストする green
test/integration/following_test.rb

require 'test_helper'

class FollowingTest < ActionDispatch::IntegrationTest

  def setup
    @user  = users(:michael)
    @other = users(:archer)
    log_in_as(@user)
  end
  .
  .
  .
  test "should follow a user the standard way" do
    assert_difference '@user.following.count', 1 do
      post relationships_path, params: { followed_id: @other.id }
    end
  end

  test "should follow a user with Ajax" do
    assert_difference '@user.following.count', 1 do
      post relationships_path, xhr: true, params: { followed_id: @other.id }
    end
  end

  test "should unfollow a user the standard way" do
    @user.follow(@other)
    relationship = @user.active_relationships.find_by(followed_id: @other.id)
    assert_difference '@user.following.count', -1 do
      delete relationship_path(relationship)
    end
  end

  test "should unfollow a user with Ajax" do
    @user.follow(@other)
    relationship = @user.active_relationships.find_by(followed_id: @other.id)
    assert_difference '@user.following.count', -1 do
      delete relationship_path(relationship), xhr: true
    end
  end
end

演習

1:respond_toブロック内の各行を順にコメントアウトしていき、
テストが正しくエラーを検知できるかどうか確認してみましょう。
実際、どのテストケースが落ちたでしょうか?

# format.html { redirect_to @user }

ActionController::UnknownFormat:

app/controllers/relationships_controller.rb:7:in `create'

ActionController::UnknownFormat

app/controllers/relationships_controller.rb:16:in `destroy'

2:xhr: trueがある行のうち、片方のみを削除するとどういった結果に
なるでしょうか? このとき発生する問題の原因と、なぜ先ほどで確認したテストがこの問題を検知できたのか考えてみてください。

"@user.following.count" didn't change by 1.
Expected: 3
Actual: 2

Ajaxを用いたフォローで、
Postリクエストを送信していないためフォロー数が増えないからエラーになる。

14.3に続く

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です