プログラミング備忘録

プログラミングの学習状況をメモしています

66,67日目

今日の学習

Ruby on Rails

Railsチュートリアル 第14章

前回の続きをやっていく。

フォローに関するデータのモデリングを経て、今回からはWebインターフェイスに取り掛かっていく。

railstutorial.jp

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 %>

これは、変数@usernilでない場合(=プロフィールページの場合)は何もせず、nilの場合(=Homeページの場合)には@usercurrent_userを代入する。

@user.following.countなどのコードは、Railsが高速化のためにデータベース内でフォロー・フォロワーの合計を計算している。

<strong>タグは、言葉通り要素を強調するタグ。

今回はclassの他に、一意性を持たせるためにidでもCSSを指定している。これは後ほどAjaxというものを実装するときに利用するらしい。

統計情報のパーシャルは以上で完成したため、Homeページに統計情報を表示するようにする。

また、Railsチュートリアルには今回の章で作成するページのSCSSが記載されていたので、それも追加した。

フォロー/フォロー解除フォーム

[Follow] / [Unfollow]ボタン用のパーシャルを作成する。

followunfollowのパーシャルに作業を振るためのパーシャル。

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]

次に、実際にfollowunfollowフォームのパーシャルをそれぞれ作成する。

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チュートリアルで既に作成しているプロフィールページや、ユーザー一覧ページを合わせたような作りになるとせちめいされている。

サイドバーに、小さめのユーザープロフィール画像のリンクを格子状に並べて表示するらしい。

最初は、この二つのページにログイン必須の設定をする。

それぞれfollowingfollowersというアクションを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 %>

f:id:hasegawa_note:20210714181331p:plain

f:id:hasegawa_note:20210714181334p:plain

[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]ボタンを動作させる。この動作は、Relationshipcreatedeleteに対応しているので、まずは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フィルターでログイン必須にする。今回はcreatedestroyの二種類しかアクションがないため、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_usernilとなり、どのみちエラーになるが、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文のような構造)。 これをcreatedestroyアクションに追加し、リダイレクトの行を削除する。追加する際は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.erbdestroy.js.erbのふたつ。

これらのファイルは、JavaScriptと埋め込みRuby(ERb)をミックスして現在のページに対するアクションを実行してくれる。

JS-ERbファイルは、DOM(Document Object Model)を使ってページを操作するので、RailsjQuery JavaScriptヘルパーを自答的に提供してくれる。

このDOMというものは、プログラミングとWebページを繋いでくれるらしく、HTMLの要素をJavaScriptで操作する場合に利用するようだ。

【初心者向け】DOMをわかりやすく解説 | のほほんIT

自動的にヘルパーが用意されるおかげで、膨大な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に代入してしまっていたからだった。

ずいぶん前にも同じミスをしたことがあるが、またやってしまった。

そのアクションで定義したものはコントローラ以外でも使うかどうかを意識して見るようにしたい。