66,67日目
今日の学習
Ruby on Rails
Railsチュートリアル 第14章
前回の続きをやっていく。
フォローに関するデータのモデリングを経て、今回からはWebインターフェイスに取り掛かっていく。
Webインターフェイス
前回作ったメソッドなどを、実際にWebインターフェイスで使用していく。フォローする、フォロー解除するといった仕組みを実装する。
まずデザインを設定するために、最低限のデータを用いたいためサンプルデータを用意する。
Railsチュートリアルでは、最初のユーザーにユーザー3〜51までをフォローさせ、逆にユーザー4〜41には最初のユーザーをフォローさせるサンプルデータを追加している。
db/seeds.rb
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) }
統計とFollowフォーム
サンプルユーザーにフォローしているユーザーをフォロワーができたため、今度はプロフィールページとHomeページにこれらの統計情報を表示するパーシャルを作成する。また、フォロー用とフォロー解除用のフォームも作成する。
統計とは、「50 following」「55followers」のように、現在そのユーザーが何人フォローしているのか、また何人からフォローされているのかを表す。
この表示はリンクにして、フォローしているユーザー、もしくはフォロワーの一覧を表示する専用ページも後ほど作成することにして、とりあえずルーティングの実装をする。
memberメソッドとcollectionメソッド
ルーティングを設定。
config/routes.rb
resources :users do member do get :following, :followers end end
resouces
ブロックで、:member
メソッドを使っている。このメソッドを利用すると、ユーザーidが含まれているURLを扱えるようになる。
生成されるURL: /users/1/following
/users/1/followers
初めて見るメソッドだが、Railsが用意している7つのデフォルトアクション以外を利用したい場合に使うメソッドのようだ。
特に、特定のデータにアクションを使いたい時に設定する。
特定のデータではない(idを含まない)、全てのデータを指定したい場合は、collection
メソッドを使う。
resources :users do collection do get :tigers end end
このコードでは、/users/tigers
というURLに応答し、アプリケーションにある全てのtiger
のリストを表示する。(tigerって一体どこから出てきたのだろうか...)
このメソッドについて調べてみたところ、どうやら検索機能などを実装する際に使えるようだ。
統計を表示する
定義したルーティングを用いて、統計情報のパーシャルを実装していく。
app/views/shared/_stats.html.erb
<% @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 ||= current_user %>
これは、変数@user
がnil
でない場合(=プロフィールページの場合)は何もせず、nil
の場合(=Homeページの場合)には@user
にcurrent_user
を代入する。
@user.following.count
などのコードは、Railsが高速化のためにデータベース内でフォロー・フォロワーの合計を計算している。
<strong>
タグは、言葉通り要素を強調するタグ。
今回はclass
の他に、一意性を持たせるためにid
でもCSSを指定している。これは後ほどAjax
というものを実装するときに利用するらしい。
統計情報のパーシャルは以上で完成したため、Homeページに統計情報を表示するようにする。
また、Railsチュートリアルには今回の章で作成するページのSCSSが記載されていたので、それも追加した。
フォロー/フォロー解除フォーム
[Follow] / [Unfollow]ボタン用のパーシャルを作成する。
follow
とunfollow
のパーシャルに作業を振るためのパーシャル。
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 %>
フォロー、フォロー解除のアクションを実行するために、Relationships
リソース用のルーティングが必要なので、フォロー(create
)とフォロー解除(destroy
)のみを追加する。
resources :relationships, only: [:create, :destroy]
次に、実際にfollow
、unfollow
フォームのパーシャルをそれぞれ作成する。
app/views/users/_follow.html.erb
<%= form_with(model: current_user.active_relationships.build, local: true) do |f| %> <div><%= hidden_field_tag :followed_id, @user.id %></div> <%= f.submit "Follow", class: "btn btn-primary" %> <% end %>
まず、フォローのフォームでは、新しくRelationshipを作成する。そのため、POST
リクエストをRelationships
コントローラに送信し、build
メソッドを使ってリレーションシップをcreate
するボタンを作成する。
followed_id
をコントローラに送信する必要があるため、hidden_field_tag
メソッドを使い、ブラウザ上には表示させずに情報を含めて送信する。
app/views/users/_unfollow.html.erb
<%= form_with(model :current_user.active_relationships.find_by(followed_id: @user.id), html: { method: :delete }, local: true) do |f| %> <%= f.submit "Unfollow", class: "btn" %> <% end %>
フォロー解除フォームでは、DELETE
リクエストを送信してリレーションシップをdestroy
する。
build
と異なり、今度は特定のユーザーを探し出すためにfind_by
メソッドを利用している。
パーシャルが完成したので、プロフィールページにフォロー用のボタン(フォーム)を設置する。
このボタンの動作の実装は後ほど行い、ひとまずフォローしているユーザーとフォロワーを表示するページをそれぞれ作成してしまうことになる。
[Following]と[Followers]ページ
フォローしているユーザー、フォロワーを表示するページをそれぞれ作成する。Railsチュートリアルで既に作成しているプロフィールページや、ユーザー一覧ページを合わせたような作りになるとせちめいされている。
サイドバーに、小さめのユーザープロフィール画像のリンクを格子状に並べて表示するらしい。
最初は、この二つのページにログイン必須の設定をする。
それぞれfollowing
、followers
というアクションをusers_controller.rb
に追加し、before_action :logged_in_user
に二つのアクションを追加する。
before_action :logged_in_user, only: [(省略), :following, :wollowers] 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
二つのアクションでは、タイトルを設定し、ユーザーを検索し、どちらかのデータを取り出し、ページネーションを行ってからページを出力している。
Railsでは慣習に従い、アクションに対応するビュー(show
ならshow.html.erb
)を暗黙的に呼び出している。
今回は、render
を使って明示的に呼び出し、show_follow
という同じビューを呼び出している。
コントローラ側で表示するべき内容を指定しておくことで、作成が必要なビューを一つだけとなり、show_follow
でどちらにも対応できるようにしている。
初めて見たテクニックだが、最初「なぜrender
の引数がどちらも同じなのだろうか」と書きながら疑問に思ったことがおかしくなかったことが分かり、なんとなくRailsに自分が慣れていている実感が湧いた。
では、どちらのアクションにも対応しているビューを作成する(必要最低限のコードだけを抜粋する)。
app/views/users/show_follow.html.erb
<% provide(:title, @title) %> # サイドバーの情報 <%= gravatar_for @user %> <%= @user.name %> <%= link_to "view my profile", @user %> Microposts: <%= @user.microposts.count %> # フォローしている人、もしくはフォロワーの小さい一覧画像 <%= render 'shared/stats' %> <% if @users.any? %> <@users.each do |user| %> <%= link_to gravatar_for(user, size: 30), user %> <% end %> <% end %> # ここに[Following]か[Followers]が表示される <%= @title %> <% if @users.any? %> # フォローしている人、もしくはフォロワーが表示される <%= render @users %> <% will_paginate %> <% end %>
[Following]も[Followers]もしっかり表示されている。
次に、show_follow
の描画結果を確認するための統合テストを作成する。あまりに網羅的なチェックをすると、却って生産性を落としかねないので現実性のテストだけに留めてある。
正しい数が表示されているかどうか、正しいURLが表示されているかどうかのチェックを行う。
フォロー関係のテストを行うために、Relationship
用のfixture
にテストデータを追加する。
text/fixtures/relationships.yml
one: follower: user1 followed: user2 two: follower: user1 followed: user3 three: follower: user2 followed: user1
このように記述をすることで、テストユーザーのフォロー、フォロワーの関係を作成することが出来る。
上のuser1を見てみると、followingは2、followersは1になる(1なのでfollowerだが)。
この数が正しいかどうかを確認するために、assert_match
メソッドを使ってプロフィール画像のマイクロポスト数をテストし、さらに正しいURLかどうかをテストする。
# ログイン必須なのでログインする def setup @user = users(:user1) 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 # ほぼ一緒なのでfollowersは省略
assert_not @user.following.empty?
このコードでは、each文のコードを確かめている。もしも@user.following.empty?
の結果がtrue
であれば、assert_select
内のブロックが実行されなくなり、適切なケースが確認できなくなることを防いでいる。
[Follow]ボタン
[Follow] / [Unfollow]ボタンを動作させる。この動作は、Relationship
のcreate
とdelete
に対応しているので、まずはRelationships
コントローラを生成する。
Railsチュートリアルでは、まず最初のにテストを書き、それをパスするように実装していく形を採っている。
test/controllers/relationships_controller_test.rb
test "create should require logged-in user" do # Relationshipの数に変化がないことを確かめる assert_no_difference "Relationship.count" do # ログインせずにフォローをする post relationships_path end # ログインURLにリダイレクトされる assert_redirected_to login_url end test "destroy should require logged-in user" do assert_no_difference "Relationship.count" do # fixtureにあるデータの削除(フォロー解除)をする delete relationship_path(relationships(:one)) end assert_redirected_to login_url end
続いて、コントローラの中身も作成していく。最初にbeforeフィルターでログイン必須にする。今回はcreate
とdestroy
の二種類しかアクションがないため、only
で指定する必要はない。
before_action :logged_in_user
アクションを動作させるためには、先程作成したフォームから送信されたパラメータを使い、followed_id
に対応するユーザーを見つける必要がある。
見つけたユーザーに対して、follow
もしくはunfollow
メソッドを使う。
以上のことを実装すると、以下のようになる。
def create # Userテーブルからparams[:followed_id]のデータを取得 user = User.find(params[:followed_id]) current_user.follow(user) redirect_to user end def destroy # Relationshipテーブルからfollowedカラムがparams[:id]のデータを取得 user = Relationship.find(params[:id]).followed current_user.unfollow(user) redirect_to user end
もしもログインしていないと、current_user
がnil
となり、どのみちエラーになるが、logged_in_user
でセキュリティを保持する方が良い。
これで、フォローしたりフォロー解除したりできるようになった。
Ajaxを利用した[Follow]ボタン
先程作成したアクションでは、フォロー、フォロー解除の動作の後、redirect_to
で対象ユーザーのページにリダイレクトしていた。
これを、非同期通信...Ajax
を使って実装してみようとのこと。
以前、いいね機能の実装練習のときに学習した記憶があり、今回はフォローに関する動作をAjax(Asynchronous(非同期の) JavaScript And XMLの略らしい)で実装する。
「非同期」で、ページを移動することなくリクエストを送信するこの機能は、現在当たり前になっているのでRailsでは簡単に実装できるようになっている。
やり方は驚くほど簡単で、form_with(model: ......, local: true)の、
localの部分を
remote: trueに置き換えるだけ。これでRailsは自動的に
Ajax`を使ってくれる。
こんなに簡単だったかなと思ったが、とりあえずフォロー、フォロー解除フォームのlocal
部分を書き換える。
書き換えた後は、これに対応するRelationships
コントローラがAjax
リクエストに応答できるようにしなければならない。
リクエストの種類によって応答を場合分けするときは、respond_to
メソッドを利用する。
respond_to do |format| format.html { redirect_to user } format.js end
二つあるブロック内のコードのうち、いずれか1行が実行される(if文のような構造)。
これをcreate
、destroy
アクションに追加し、リダイレクトの行を削除する。追加する際はuser
をインスタンス変数@user
に変更することを忘れないように。
次に、ブラウザ側でJavaScriptが無効になっていた場合でも動くように設定する。
config/application.rb
module SampleApp class Application < Rails::Application ... # 認証トークンをremoteフォームに埋め込む config.action_view.embed_authenticity_token_in_remote_forms = true end end
これだけではまだ不十分で、JavaScript用の埋め込みRuby(.js.erb)ファイルを用意しなければならない。
Railsは、Ajax
リクエストを受信した場合、自動的にアクションと同じ名前の.js.erb
を呼び出そうとする。
今回の場合は、create.js.erb
とdestroy.js.erb
のふたつ。
これらのファイルは、JavaScriptと埋め込みRuby(ERb)をミックスして現在のページに対するアクションを実行してくれる。
JS-ERbファイルは、DOM
(Document Object Model)を使ってページを操作するので、RailsがjQuery JavaScript
ヘルパーを自答的に提供してくれる。
このDOMというものは、プログラミングとWebページを繋いでくれるらしく、HTMLの要素をJavaScriptで操作する場合に利用するようだ。
自動的にヘルパーが用意されるおかげで、膨大なDOM操作用メソッドが使えるようになるらしいが、今回利用するものは2つ。
まず、follow_form
の要素をjQueryで操作するには、次のようにドル記号$
とCSS id#
を使ってアクセスする。
$("#follow_form")
次に、html
メソッドを利用する。これは、引数の中で指定された要素の内側にあるHTMLを更新する。
JS-ERbファイルではERbが使えるため、create.js.erb
ファイルではフォロー用のフォームをunfollow
パーシャルで更新し、フォロワーのカウントを更新するのにERbを利用している。
app/views/relationships/create.js.erb
$("#follow_form").html("<%= escape_javascript(render('user/unfollow')) %>"); $("#followers").html('<%= @user.followers.count %>'); destroyの場合、 (render('users/follow'))となる
escape_javascript
メソッドを使い、JavaScriptファイル内にHTMLを挿入するとき、実行結果をエスケープして、文字化けを防いでいる。
一行目でフォローボタンの内容が変化するようにしてあり、二行目でフォロー数が変化するようにしてある。
これで、プロフィールページを更新させずにフォロー、フォロー解除ができるようになった。
フォローのテスト
フォローに対するテストで、/relationships
に対してPOST
リクエストを送り、フォローされたユーザーが1人増えたかどうかをチェックする。
assert_difference '@user.following.count', 1 do post relationships_path, params: { followed_id: @other.id } end
これは、Ajax
を利用していない標準的なフォローに対するテスト。
Ajax
のテストでは、xhr :true
というオプションを付け加える。そうすると、Ajax
でリクエストを発行してくれるようになる。
post relationships_path, params: { followed_id: @other.id }, xhr: true
フォロー解除の場合は、post
メソッドをdelete
メソッドに置き換えてテストをする。
assert_difference '@user.following.count', -1 do delete relationship_path(relationship), xhr: true end
今日のやらかし
出力しない箇所を<%= ... %>にしてしまう
具体的には、if文で失敗してしまった。
# 正しい形 <% if @users.any? %> # 間違えた形 <%= if @users.any? %>
エラーが出てもしばらく分からなかった。埋め込みの場合はそこにも注意しよう。
インスタンス変数が必要なのに@をつけ忘れ
Ajax
を利用してフォロー、フォロー解除をする部分で、何度やってみても非同期でページが切り替わらない。
理由は、Relationships
コントローラで、@user
というインスタンス変数を用意しなければならなかったのに、ローカル変数user
に代入してしまっていたからだった。
ずいぶん前にも同じミスをしたことがあるが、またやってしまった。
そのアクションで定義したものはコントローラ以外でも使うかどうかを意識して見るようにしたい。