概要
ここまでで、「フォロー」「フォロワー」という概念が実装できました。今度は、これらの概念をビューで使えるようにするための部品を実装していくパートになります。「ビューにおけるフォロー・フォロワーという概念の利用」としては、例えば以下のような手段があります。
- フォローしているユーザーとフォロワーの統計情報(数)を表示するためのパーシャル
- フォローおよびフォロー解除用のフォーム
- フォローしているユーザー("following")の一覧・フォロワー("followers")の一覧を表示する専用ページ
「フォローしているユーザー」という概念を指す語としては、Twitterに倣って「following」という語を使うこととします。
統計情報パーシャル
まずは、「フォローしているユーザーとフォロワーの数を表示するパーシャル」を実装していきます。
統計情報パーシャルのモックアップは、Railsチュートリアル本文では図 14.10として掲載されています。
統計情報パーシャルには、以下のような機能が必要となります。
- 現在のユーザーがフォローしている数が表示される
- 現在のフォロワーの数が表示される
- 各表示にはハイパーリンクが貼られ、それぞれの一覧ページにジャンプすることができる
following
アクションとfollowers
アクションに対するルーティングの実装
新たに実装するルーティングの実体は以下のようになります。これらのアクションは、「ユーザー一覧を表示するためのアクション」なので、HTTPリクエストの種類はGET
を使います。
resources :users do
member do
get :following, :followers
end
end
メンバールーティング
リソースブロック中で呼び出されるmember
ブロックは、「メンバールーティング」という、ここまでに登場したことがない新概念で用いるブロックです。メンバールーティングは、resources
で指定したコレクションの個別メンバーに対してアクセスするためのリソース名・HTTPリクエストを定義する記法です。
例えば上記の例の場合、usersリソースに対する基本的なルーティングに加え、以下のようなURLへのGET
リクエストが有効になります。リソースidはparam[:id]
に渡されます。
- /users/1/following
- /users/1/followers
メンバールーティングについてのより詳細な説明が必要ならば、Rails のルーティング - Railsガイドを参照するのがよいでしょう。
メンバールーティングの定義の省略記法
なお、メンバールーティングを1つだけ定義する場合は、以下のような省略記法の使用が可能です。
resources :photos do
get 'preview', on: :member
end
resources :photos do
member do
get 'preview'
end
end
今回はメンバールーティングを2つ定義しているため、省略しない記法を用いています。
メンバールーティングの発展 - 「全てのメンバーを対象としたリソース」の実装
メンバールーティングにおいて、「IDを指定しない、全てのメンバーを対象としたリソースを実装する」という場合には、member
メソッドの代わりにcollection
メソッドを使います。
resources :users do
collection do
get :spellcards
end
end
上記は /users/spellcards というURLに対するGET
リクエストに反応します。「全てのユーザーの全てのspellcardのリストを表示する」という動作を実現するためのルーティングですね。
新たに有効になるリクエスト・名前付きルート
上記ルーティングの実装により、以下のURLに対する以下のリクエスト・名前付きルートが有効になります。
HTTPリクエスト | URL | アクション | 名前付きルート |
---|---|---|---|
GET |
/users/1/following | following |
following_user_path(1) |
GET |
/users/1/followers | followers |
followers_user_path(1) |
config/routes.rb
の変更内容
上記を踏まえ、config/routes.rb
に適用する変更の内容は以下のようになります。
Rails.application.routes.draw do
get 'password_resets/new'
get 'password_resets/edit'
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
+ 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
なお、:users
に対するルーティングを上記のように変更しても、RESTの基本となる7つのリソースに対するルーティングには影響ありません1。既存のルーティングが存在するリソースに対しても、安心してメンバールーティングを追加していくことができます。
/users/1/followers というリソースへのアクセスに対する現状の応答
現時点でRailsサーバーを起動し、Webブラウザから /users/1/followers へアクセスすると、Railsサーバーには以下のようなエラーログが出力されます。
Started GET "/users/1/followers" for 172.17.0.1 at 2020-01-30 11:09:53 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
(1.8ms) SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC
AbstractController::ActionNotFound (The action 'followers' could not be found for UsersController):
...略
RoutingError
ではないのがポイントですね。「ルーティングは正しく定義されているが、リクエストを受け付けるコントローラーの実装が存在しない」という趣旨のエラーです。
統計情報パーシャル本体の実装
following
アクションとfollowers
アクションに対するルーティングが実装できたことにより、統計情報パーシャル本体の実装に必要な前提条件が達成されました。続いては、統計情報パーシャル本体の実装に移っていきます。統計情報パーシャルのファイル名は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>
||=
演算子を用いて、Homeページとプロフィールページ両方で統計情報パーシャルを使えるようにする
統計情報パーシャルの実装には、「統計情報パーシャルは、Homeページとプロフィールページの両方で使われる」という前提があります。両方のページで有効なユーザー情報を取得できるような実装になっていなければなりません。そのような実装を実現するために、下記のコードによって現在のユーザーを取得します。
<% @user ||= current_user %>
||=
を使った上記のコードは、「@user
が偽と評価される場合、current_user
の内容を@user
に代入する」という動作をします。具体的には、「@user
がnil
である場合」ですね。逆に、@user
が真と評価される場合は何もしません。
フォローしているユーザーの数とフォロワーの数をカウントし、表示する
@user
に有効なユーザー情報が代入されていれば、当該ユーザーがフォローしているユーザーの数は以下のメソッド呼び出しにより取得できます。
@user.following.count
一方、当該ユーザーのフォロワーの数は、以下のメソッド呼び出しにより取得できます。
@user.followers.count
あとはこれらの数字を<%= %>
でそのまま表示させれば、「フォローされているユーザーの数とフォロワーの数をビューで表示する」という動作が実現できます。
なお、本文14.1.5項後の演習でも確認したように、フォローしているユーザーの数とフォロワーの数の計算はRDB内で行っています。例えば以下のようなSQL文ですね。
SELECT COUNT(*)
FROM "users"
INNER JOIN "relationships"
ON "users"."id" = "relationships"."follower_id"
WHERE "relationships"."followed_id" = ?
[
["followed_id", 1]
]
CSS idを指定する意味
<strong id="following" class="stat">
...略
</strong>
ここで、CSS idを指定しています。直ちに使用する場面はないのですが、今後実装を予定している「Ajaxを使用したFollowerボタン」を実装するときに重要になります。Ajaxでは、一意のidを用いてページ要素にアクセスすることができます。
Homeページにフォロワーの統計情報を追加する
Homeページにフォロワーの統計情報を追加するためには、以下のようなerbコードを用います。
<section class="stats">
<%= render 'shared/stats' %>
</section>
私の場合、これまでの演習で、ログイン時と非ログイン時のHomeページの実装をそれぞれ別のパーシャルに分離していました。そのため、実装先は「ログイン時のHomeページで用いるパーシャル」となります。具体的にはapp/views/shared/_home_logged_in.erb
というファイルですね。
<div class="row">
<aside class="col-md-4">
<section class="user_info">
<%= render 'shared/user_info' %>
</section>
+ <section class="stats">
+ <%= render 'shared/stats' %>
+ </section>
<section class="micropost_form">
<%= render 'shared/micropost_form' %>
</section>
</aside>
<div class="col-md-8">
<h3>Micropost Feed</h3>
<%= render 'shared/feed' %>
</div>
</div>
統計情報へのスタイルの適用
統計情報パーシャル、並びにRailsチュートリアル第14章本文で用いる全てのスタイルをapp/assets/stylesheets/custom.scss
に追加していきます。変更内容は以下のようになります。
...略
/* sidebar */
aside {
...略
}
.gravatar {
...略
}
.gravatar_edit {
...略
}
+ .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 */
...略
統計情報パーシャル、Homeページでの統計情報パーシャルの使用、統計情報パーシャルの表示に関するCSS。ここまで実装できれば、Homeページにフォロー関係の統計情報が表示されるようになります。レイアウトが崩れることもありません。
[follow]/[unfollow]ボタンの実装
フォロー/フォロー解除フォームのパーシャル
まずは、「[follow]ボタン/[unfollow]ボタンのいずれを表示するか、表示しないか」を決めるためのパーシャルを実装していきます。ファイル名はapp/views/users/_follow_form.html.erb
とします。
<% unless current_user?(@user) %>
<div id="follow_user">
<% if current_user.following?(@user) %>
<%= render 'unfollow' %>
<% else %>
<%= render 'follow' %>
<% end %>
</div>
<% end %>
このパーシャルの動作は、全体としては以下となります。
- 対象ユーザーが現在ログイン中のユーザー自身であれば、何も描画しない
- 現在ログイン中のユーザーが対象ユーザーをフォローしていれば、
unfollow
パーシャルを描画する - 現在ログイン中のユーザーが対象ユーザーをフォローしていなければ、
follow
パーシャルを描画する
Relationshipリソース用のルーティングを実装する
RESTアーキテクチャによれば、「フォロー」「フォロー解除」という動作とモデルオブジェクトに対する操作の関連付けは、以下のようになるのでしたね。
- 「フォロー」という動作は、Relationshipモデルオブジェクトの
create
により実現される - 「フォロー解除」という動作は、Relationshipモデルオブジェクトの
destroy
により実現される
ということで、follow
およびunfollow
というパーシャルを実装するためには、これらの動作に対応するルーティングを先に実装しておく必要があります。コードとしては以下のようになります。
resources :relationships, only: [:create, :destroy]
config/routes.rb
に適用する変更の全体像は以下のようになります。
Rails.application.routes.draw do
get 'password_resets/new'
get 'password_resets/edit'
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]
+ 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
メソッドを使ってRelationshipモデルオブジェクトを操作する」というのが動作の中核となります。
- HTTPの
POST
リクエストを発行し、新たなRelationshipモデルオブジェクトをcreate
している - 対象は「単独のRelationshipモデルオブジェクト」ではなく、「Relationshipモデル全体」である
存在しないオブジェクトを新たに生成する処理なので、処理の対象は単独のオブジェクトではなく、モデル全体となります。
また、「どのユーザーをフォローするか」という情報をPOST
リクエストに含めるために、erbのhidden_field_tag
メソッドを用いています。このhidden_field_tag
メソッドでは、例えば以下のようなinput
要素が生成されます。
<input id="follower_id" name="followed_id" type="hidden" value="3">
このようにすることにより、フォロー対象のユーザーのidが、followed_id
属性の値としてPOST
リクエストに無事渡されるのです。
ユーザーをフォロー解除するフォーム
<%= 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モデルオブジェクトを操作する」というのが動作の中核となります。一方で、ユーザーをフォローするフォームとの明確な違いが3点あります。
- 既存のリレーションシップを見つけ出す処理が含まれる
- HTTPの
DELETE
リクエストを発行し、単独のRelationshipモデルオブジェクトをdestroy
している -
hidden_field_tag
メソッドが含まれない
各ユーザーのプロフィールページに、フォロー用フォームとフォロワーの統計情報を追加する
[follow]/[unfollow]ボタンに関するパーシャルの実装が完成したことにより、先に実装していたフォロワーの統計情報パーシャルとともに、各ユーザーのプロフィールページに必要なパーシャルの実装が出揃いました。各ユーザーのプロフィールページにこれらのパーシャルを追加していきましょう。
<% provide(:title, @user.name) %>
<div class="row">
<aside class="col-md-4">
<section class="user_info">
<h1>
<%= gravatar_for @user %>
<%= @user.name %>
</h1>
</section>
+ <section>
+ <%= render 'shared/stats' %>
+ </section>
</aside>
<div class="col-md-8">
+ <%= render 'follow_form' if logged_in? %>
<% if @user.microposts.any? %>
<h3>Microposts (<%= @user.microposts.count %>)</h3>
<ol class="microposts">
<%= render @microposts %>
</ol>
<%= will_paginate @microposts %>
<% end %>
</div>
</div>
ここまでの実装が完了すると、各ユーザーのプロフィールページに、[Follow]ボタンまたは[Unfollow]ボタンが表示されるようになります。
上記スクリーンショットは[Follow]ボタンの表示例です。
上記スクリーンショットは[Unfollow]ボタンの表示例です。
なお、これらのボタンの動作はまだ実装されていませんので、現在のところは、これらのボタンをクリックしても何も起こりません。
-
アプリケーションの他の部分とここまで成功しているテストスイートの実装、いずれも変更がなければ、当該変更を適用しても、テストスイート全体を対象としたテストは成功します。 ↩