14.2 [Follow]のwebインターフェイス
14.2で、やや複雑なデータモデリングの技術を説明した。これまでに使われた様々な関連付けを理解するのに一番良い方法は。実際にwebインターフェースで使ってみること。
この節では、モックアップで示したようにフォロー/フォロー解除の基本的なインターフェイスを実装する。
また、フォローしているユーザーと、フォロワーにそれぞれ表示用のページを作成する。14.3で、ユーザーのステータスフィードを追加して、サンプルを完成させる。
14.2.1 フォローのサンプルデータ
前章のときと同じように、サンプルデータを自動生成するrails db:seed
を使って、DBにサンプルデータを登録できるとやはり便利。先にサンプルデータを自動生成できるようにしておけば、Webページの見た目のデザインから先に取りかかることができ、バックエンド機能の実装を後に回すことができる。
次は、リレーションシップのサンプルデータを生成するためのコード。
ここでは、最初のユーザーにユーザー3
からユーザー51
をフォローさせ、それから逆にユーザー4
からユーザー41
に最初のユーザーをフォローさせる。
#ユーザー User.create!(name: "Example User", email: "example@railstutorial.org", password: "foobar", password_confirmation: "foobar", admin: true, activated: true, activated_at: Time.zone.now) 99.times do |n| name = Faker::Name.name email = "example-#{n+1}@railstutorial.org" password ="password" User.create!(name: name, email: email, password: password, password_confirmation: password, activated: true, activated_at: Time.zone.now) end #マイクロポスト users = User.order(:created_at).take(6) 50.times do content = Faker::Lorem.sentence(5) users.each { |user| user.microposts.create!(content: content) } end #リレーションシップ users = User.all user = users.first following = users[2..50] followers = users[3..40] following.each { |followed| user.follow(followed) } followers.each { |follower| follower.follow(user) }
DBに反映
$ rails db:migrate:reset $ rails db:seed
14.2.2 統計と[Follow]フォーム
これでサンプルユーザに、フォローしているユーザーとフォロワーができた。
プロフィールページとHomeページを更新して、これを反映する。
最初に、プロフィールページとHomeページに、フォローしているユーザーとフォロワーの統計情報を表示するためのパーシャルを作成する。
次に、フォロー用とフォロー解除用のフォームを作成する。それから、フォローしているユーザーの一覧(following)とフォロワーの一覧(followers)を表示する専用のページを作成する。
フォロー数の単位には「following」を使い、例えば「50 following」といった具合に表示する。
この統計情報には、現在のユーザーがフォローしている人数と、現在のフォロワーの人数が表示されている。
それぞれの表示はリンクになっており、専用の表示ページに移動できる。
5章では、これらのリンクはダミーテキスト'#'を使って無効にしていたが、今回は実装する。
実際のページ作成は14.2.3で行うが、ルーティングは今実装する。このコードでは、resources
ブロックの内側で:member
メソッドが使っている。これは初登場のメソッドだが、まずはどんな動作するのか推測してみる。
Rails.application.routes.draw do root 'static_pages#home' get '/help', to: 'static_pages#help' get '/about', to: 'static_pages#about' get '/contact', to: 'static_pages#contact' get '/signup', to: 'users#new' post '/signup', to: 'users#create' get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy' resources :users do #変更と追加 member do get :following, :followers end end resources :account_activations, only: [:edit] resources :password_resets, only: [:new, :create, :edit, :update] resources :microposts, only: [:create, :destroy] end
そして、上のコードはまさにそれを行っている。また、どちらもデータを表示するページなので、適切なHTTPメソッドはGETリクエストになる。したがって、
get
メソッドを使って適切なレスポンスを返すようにする。ちなみに、member
メソッドを使うとユーザーidが含まれているURLを扱うようになるが、idを指定せずにすべてのメンバーを表示するには、次のようにcollection
メソッドを使う。
resources :users do collection do get :tigers end end
このコードは/users/tiggersというURLに応答する(アプリにあるすべてのtigerのリストを表示)。
生成されるルーティングテーブルは次のとおり。
HTTPリクエスト | URL | アクション | 名前付きルート |
---|---|---|---|
GET | /users/1/following | following | following_user_path(1) |
GET | /users/1/followers | followers | followers_user_path(1) |
この表で示したフォロー用とフォロワー用の名前付きルートを、今後の実装で使っていく。
ルーティングを定義したので、統計情報のパーシャルを実装する準備が整った。このパーシャルでは、divタグの中に2つのリンクを含めるようにする。
<% @user ||= current_user %> #① <div class="stats"> <a href="<%= following_user_path(@user) %>"> <strong id="following" class="stat"> <%= @user.following.count %> #② </strong> following </a> <a href="<%= followers_user_path(@user) %>"> <strong id="followers" class="stat"> <%= @user.followers.count %> #③ </strong> followers </a> </div>
@user
がnil
でない場合(つまりプロフィールページ)は何もせず、nil
の場合には@user
にcurrent_user
に代入する。②③:フォローしているユーザーの人数を、関連付けを使って計算。これはフォロワーについても同様。これは、マイクロポストの投稿数を表示した方法と次と同じ
@user.microposts.count
なお、今回も以前と同様に、Railsは高速化のためにDB内で合計を計算している点に注意!
一部の要素で、次のようにCSS idを指定していることにも注目!
<strong id="following" class="stat"> : </strong>
こうしておくと、14.2.5でAjaxを実装するときに便利。そこでは、一意のidを指定してページ要素にアクセスしている。
これで統計情報パーシャルができあがる。Homeページにこの統計情報を表示するには、次のようにすると簡単。
<% @user ||= current_user %> <div class="stats"> <a href="<%= following_user_path(@user) %>"> <strong id="following" class="stat"> <%= @user.following.count %> </strong> following </a> <a href="<%= followers_user_path(@user) %>"> <strong id="followers" class="stat"> <%= @user.followers.count %> </strong> followers </a> </div>
変更の結果、Homeページは以下のようにする。
/* sidebar */ : .gravatar { float: left; margin-right: 10px; } .gravatar_edit { margin-top: 15px; } .stats { overflow: auto; margin-top: 0; padding: 0; a { float: left; padding: 0 10px; border-left: 1px solid &gray-lighter; 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; } a { padding: 0; } } .users.follow { padding: 0; } /* forms */ :
この後すぐ、プロフィールにも統計情報パーシャルを表示するが、今のうちに[Follow]/[Unfollow]ボタン用のパーシャルを作成する。
<% unless current_user?(@user) %> <div id="follow_form"> <% if current_user.following?(@user) %> <%= render 'unfollow' %> <% else %> <%= render 'follow' %> <% end %> </div> <% end %>
follow
とunfollow
のパーシャルに作業を振っているだけ。(urlのユーザーをログインユーザーがフォローしていればunfollow、フォローしていなければfollowをレンダリング)
パーシャルでは、Relationshipsリソース用の新しいルーティングが必要。これを、Micropostsリソースの例に従って作成する。
Rails.application.routes.draw do root 'static_pages#home' get '/help', to: 'static_pages#help' get '/about', to: 'static_pages#about' get '/contact', to: 'static_pages#contact' get '/signup', to: 'users#new' post '/signup', to: 'users#create' get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy' resources :users do member do get :following, :followers end end resources :users resources :account_activations, only: [:edit] resources :password_resets, only: [:new, :create, :edit, :update] resources :microposts, only: [:create, :destroy] resources :relationships, only: [:create, :destroy] #追加 end
<%= form_for(current_user.active_relationships.build) do |f| %> <div><%= hidden_field_tag :followed_id, @user.id %></div> <%= f.submit "Follow", class: "btn btn-primary" %> <% end %>
<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id), html: { method: :delete }) do |f| %> <%= f.submit "Unfollow", class: "btn" %> <% end %>
form_for
を使ってRelationshipモデルオブジェクトを操作している。これらの2つのフォームの主な違いは、フォローフォームは新しいリレーションシップを作成するのに対し、
フォロー解除フォームは既存のリレーションシップを見つけ出すという点。
すなわち、前者はPOSTリクエストをRelationshipsコントローラに送信してリレーションシップをcreate
(作成)し、
後者はDELETEリクエストを送信してリレーションシップをdestroy
(削除)するということ。
最終的に、このフォロー/フォロー解除フォームにはボタンしかないことが理解できる。
しかし、それでもフォローフォームではfollowed_id
をコントローラに送信する必要がある。これを行うために、hidden_field_tag
メソッドを使う。このメソッドは、次のフォーム用HTMLを生成する。
<input id="followed_id" name="followed_id" type="hidden" value="3" />
12.3で見たように、隠しフィールドのinput
タグを使うことで、ブラウザ上に表示させずに適切な情報を含めることができる。
これでパーシャルとしてフォロー用フォームをプロフィールページに表示できるようになった。
プロフィールには、それぞれ[Follow]、[Unfollow]ボタンが表示される。
<div class="row"> <aside class="col-md-4"> <section class="user_info"> <h1> <%= gravatar_for @user %> <% @user.name %> </h1> </section> <section class="stats"> #追加 <%= render 'shared/stats' %> #追加 </section> </aside> <div class="col-md-8"> <%= render 'follow_form' if logged_in? %> #追加 <% if @user.microposts.any? %> :
これらのボタンの実装には2通りの方法がある。
1つは標準的な方法、もう1つはAjaxを使う方法。
でもその前に、フォローしているユーザーとフォロワーを表示するページをそれぞれ作成してHTMLインターフェイスを完成させる。
14.2.3 [Following]と[Followers]ページ
フォローしているユーザーを表示するページと、フォロワーを表示するページは、いずれもプロフィールページとユーザー一覧ページを合わせたような作りになるという点で似ている。
どちらにもフォローの統計情報などのユーザー情報を表示するサイドバーと、ユーザーのリストがある。
さらに、サイドバーには小さめのユーザープロフィール画像のリンクを格子状に並べて表示する。
ここでの最初の作業は、フォローしているユーザーのリンクとフォロワーのリンクを動くようにすること。
どちらのページでもユーザーのログインを要求するようにする。そこで前回のアクセス制御と同様に、まずはテストから書いていく。今回使うテストは以下の通り。
: 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
それはUsersコントローラに2つの新しいアクションを追加する必要があるということ。
これはroutesで定義した2つのルーティングにもとづいており、これらはそれぞれfollowing
およびfollowers
と呼ぶ必要がある。それぞれのアクションでは、タイトルを認定し、ユーザーを検索し、@user.following
または@user.followers
からデータを取り出し、ページネーションを行なって、ページを出力する必要がある。作成したコードは↓
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はどちらの場合でもほぼ同じであり、両方の場合をカバーできるため。
<% 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>
users_controller.rb
にあるアクションは、2通りの方法で上のビューを呼び出している。followingアクションでfollowingを通してshow_followビューを呼び出し、
followersアクションではfollowersを通してshow_followビューを呼び出す。
このとき、上のコードでは現在のユーザーを一切使っていない点に注目!したがって、他のユーザーのフォロワー一覧ページもうまく動く。
テストは成功。
次に、show_follow
の描画結果を確認するため、統合テストを書いていく。
ただし、今回の統合テストは基本的なテストだけに留めておき、網羅的なテストにはしていない。
これはHTML構造を網羅的にチェックするテストは壊れやすく、生産性を逆に落としかねないから。
したがって今回は、正しい数が表示されているかどうかと、正しいURLが表示されているかどうかの2つのテストを書く。
いつものように統合テストを生成するところから始める。
$ rails g integration_test following invoke test_unit create test/integration/following_test.rb
今度は、テストデータをいくつか揃える。リレーションシップ用のfixtureにデータを追加。
orange: content: "I just ate an orange!" created_at: <%= 10.minutes.ago %> user: michael
ユーザーとマイクロポストは関連付けできることを思い出す。上のコードではユーザー名を書いていたが、
user: michael
次のようにユーザーidでも関連付けできる
user_id: 1
この例を参考に、Relationship用のfixtureにテストデータを追加する。
one: follower: michael followed: lana two: follower: michael followed: malory three: follower: lana followed: michael four: follower: archer followed: michael
上記のfixtureでは、
前半の2つでMichaelがLanaとMaloryをフォローし、
後半の2つでLanaとArcherがMichaelをフォローしている。
あとは、正しい数かどうかを確認するために、assert_match
メソッドを使ってプロフィール画面のマイクロポスト数をテストする。さらに、正しいURLかどうかをテストするコードも加えると、次のようになる。
: 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
assert_not @user.following.empty?
これは次のコードを確かめるためのテストであって
@user.following.each do |user| assert_select "a[href=?]", user_path(user) end
無意味なテストでない(followers
についても同様)。つまり、もし@user.following.empty?
の結果がtrueであれば、assert_select
内のブロックが実行されなくなるため、その場合においてテストが適切なセキュリティモデルを確認できなくなることを防いでいる。
上の変更を加えるとテストが成功
14.2.4 [Follow]ボタン(基本編)
ビューが整ってきた。いよいよ[Follow]/[Unfollow]ボタンを動作させる。
フォローとフォロー解除はそれぞれリレーションシップの作成と削除に対応しているため、まずはRelationshipsコントローラが必要。
Relationshipsコントローラの生成
$ rails g controller Relationships
Relationshipsコントローラのアクションでアクセス制御することはそこまで難しくない。
しかし、前回のアクセス制御の時と同様に最初にテストを書き、それをパスするように実装することでセキュリティモデルを確立させていく。
今回はまず、コントローラのアクションにアクセスするとき、ログイン済みのユーザーであるかどうかをチェックする。
もしログインしていなければログインページにリダイレクトされるので、Relationshipのカウントが変わっていないことを確認する。
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コントローラのアクションに対して追加する。class RelationshipsController < ApplicationController before_action :logged_in_user def create end def destroy end end
followed_id
に対応するユーザーを見つけてくる必要がある。その後、見つけてきたユーザーに対して適切にfollow/unfollow
メソッド(Userモデルで定義した)を使う。
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行目で例外が発生する。エラーにはなるが、アプリやデータに影響は生じない。このままでも支障はないが、このような例外には頼らない方がよいので、上ではひと手間かけて、セキュリティのためのレイヤーを追加した。
これで、フォロー/フォロー解除の機能が完成した。どのユーザーも、他のユーザーをフォローしたりリフォローしたりできる。
ブラウザ上でボタンをクリックして、確かめてみる。振る舞いを検証する統合テストは14.2.6で実装する。
14.2.5 [Follow]ボタン(Ajax編)
フォロー関連の機能の実装は完了したが、ステータスフィードに取りかかる前にもう1つだけ機能を洗練させてみる。
14.2.4では、Relationshipsコントローラのcreate
アクションとdestroy
アクションを単に元のプロフィールにリダイレクトしていた。つまり、
(1)ユーザーはプロフィールページを最初に表示し、
(2)それからユーザーをフォローし、
(3)その後すぐ元のページにリダイレクトされる
という流れになる。
ユーザーをフォローした後、本当にそのページから離れて元のページに戻らないといけないのか。この点を考えなおしてみる。
これは、Ajaxを使えば解決できる。
Ajaxを使えば、Webページからサーバーに「非同期」で、ページを移動させることなくリクエストを送信することができる。
WebフォームにAjaxを採用するのは今や当たり前になりつつあるので、RailsでもAjaxを簡単に実装できるようになっている。
フォロー用とフォロー解除用のパーシャルをこれに沿って更新するのは簡単。次のコードがあるとすると、
form_for
次のように置き換えるだけ
form_for ..., remote: true
これだけでRailsは自動的にAjaxを使うようになる。具体的な更新の結果は次の通り
<%= form_for(current_user.active_relationships.build, remote: true) do |f| %> #変更 <div><%= hidden_field_tag :followed_id, @user.id %></div> <%= f.submit "Follow", class: "btn btn-primary" %> <% end %>
<%= 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 %>
<form action="/relationships/117" class="edit_relationship" data-remote="true" id="edit_relationship_117" method="post" > : </form>
ここでは、formタグの内部でdata-remote="true"
を設定している。
これは、JavaScriptによるフォーム操作を許可することをRailsに知らせるためのもの。現在のRailsではHTMLプロパティを使って簡単にAjaxが扱えるようになっている。
フォームの更新が終わったので、今度はこれに対応する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
)に変更した点に注目!
これは、_follow.html.erb
と_unfollow.html.erb
を実装したことにより、インスタンス変数が必要になったからである。
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
class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 5.1 # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. : # 認証トークンをremoteフォームに埋め込む config.action_view.embed_authenticity_token_in_remote_forms = true #変更 end end
というのも、Ajaxリクエストを受信した場合は、Railsが自動的にアクションと同じ名前を持つJavaScript用の埋め込みRubyファイル(
create.js.erb
やdestroy.js.erb
など)を呼び出すから。これらのファイルではJavaScriptと埋め込みRuby(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
メソッドを使っている点に注目。
このメソッドは、JavaScriptファイル内にHTMLを挿入する時に実行結果をエスケープするために必要。
$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>"); $("#followers").html('<%= @user.followers.count %>');
destroy.js.erb
ファイルの方も同様
$("#follow_form").html("<%= escape_javascript(render(`users/follow`)) %>"); $("#followers").html('<%= @user.followers.count %>');