Railsチュートリアル 第14章 ユーザーをフォローする - [Follow] のWebインターフェイス - 統計と [Follow] フォーム

# 概要

ここまでで、「フォロー」「フォロワー」という概念が実装できました。今度は、これらの概念をビューで使えるようにするための部品を実装していくパートになります。「ビューにおけるフォロー・フォロワーという概念の利用」としては、例えば以下のような手段があります。

* フォローしているユーザーとフォロワーの統計情報（数）を表示するためのパーシャル
* フォローおよびフォロー解除用のフォーム
* フォローしているユーザー("following")の一覧・フォロワー("followers")の一覧を表示する専用ページ

「フォローしているユーザー」という概念を指す語としては、Twitterに倣って「following」という語を使うこととします。

# 統計情報パーシャル

まずは、「フォローしているユーザーとフォロワーの数を表示するパーシャル」を実装していきます。

統計情報パーシャルのモックアップは、Railsチュートリアル本文では[図 14.10](https://railstutorial.jp/chapters/following_users?version=5.1#fig-stats_partial_mockup)として掲載されています。

統計情報パーシャルには、以下のような機能が必要となります。

* 現在のユーザーがフォローしている数が表示される
* 現在のフォロワーの数が表示される
* 各表示にはハイパーリンクが貼られ、それぞれの一覧ページにジャンプすることができる

## `following`アクションと`followers`アクションに対するルーティングの実装

新たに実装するルーティングの実体は以下のようになります。これらのアクションは、「ユーザー一覧を表示するためのアクション」なので、HTTPリクエストの種類は`GET`を使います。

```ruby:
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ガイド](https://railsguides.jp/routing.html#restful%E3%81%AA%E3%82%A2%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E3%81%95%E3%82%89%E3%81%AB%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B)を参照するのがよいでしょう。

### メンバールーティングの定義の省略記法

なお、メンバールーティングを1つだけ定義する場合は、以下のような省略記法の使用が可能です。

```ruby:メンバールーティングの定義の省略記法の例
resources :photos do
  get 'preview', on: :member
end
```

```ruby:省略しない記法
resources :photos do
  member do
    get 'preview'
  end
end
```

今回はメンバールーティングを2つ定義しているため、省略しない記法を用いています。

### メンバールーティングの発展 - 「全てのメンバーを対象としたリソース」の実装

メンバールーティングにおいて、「IDを指定しない、全てのメンバーを対象としたリソースを実装する」という場合には、`member`メソッドの代わりに`collection`メソッドを使います。

```ruby:すべてのメンバーを表示する実装の例
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`に適用する変更の内容は以下のようになります。

```diff: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つのリソースに対するルーティングには影響ありません[^基本のルーティングに影響なし]。既存のルーティングが存在するリソースに対しても、安心してメンバールーティングを追加していくことができます。

[^基本のルーティングに影響なし]: アプリケーションの他の部分とここまで成功しているテストスイートの実装、いずれも変更がなければ、当該変更を適用しても、テストスイート全体を対象としたテストは成功します。

### /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`とします。内容は以下の通りです。

```erb: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ページとプロフィールページの両方で使われる」という前提があります。両方のページで有効なユーザー情報を取得できるような実装になっていなければなりません。そのような実装を実現するために、下記のコードによって現在のユーザーを取得します。

```erb:
<% @user ||= current_user %>
```

`||=`を使った上記のコードは、「`@user`が偽と評価される場合、`current_user`の内容を`@user`に代入する」という動作をします。具体的には、「`@user`が`nil`である場合」ですね。逆に、`@user`が真と評価される場合は何もしません。

### フォローしているユーザーの数とフォロワーの数をカウントし、表示する

`@user`に有効なユーザー情報が代入されていれば、当該ユーザーがフォローしているユーザーの数は以下のメソッド呼び出しにより取得できます。

```ruby:
@user.following.count
```
一方、当該ユーザーのフォロワーの数は、以下のメソッド呼び出しにより取得できます。

```ruby:
@user.followers.count
```

あとはこれらの数字を`<%= %>`でそのまま表示させれば、「フォローされているユーザーの数とフォロワーの数をビューで表示する」という動作が実現できます。

なお、[本文14.1.5項後の演習](https://railstutorial.jp/chapters/following_users?version=5.1#sec-exercises_followers)でも確認したように、フォローしているユーザーの数とフォロワーの数の計算はRDB内で行っています。例えば以下のようなSQL文ですね。

```sql:フォローしているユーザーの数を数える
SELECT COUNT(*)
  FROM "users"
    INNER JOIN "relationships"
      ON "users"."id" = "relationships"."follower_id"
  WHERE "relationships"."followed_id" = ?
  [
    ["followed_id", 1]
  ]
```

### CSS idを指定する意味

```erb:
<strong id="following" class="stat">
...略
</strong>
```

ここで、CSS idを指定しています。直ちに使用する場面はないのですが、今後実装を予定している「Ajaxを使用したFollowerボタン」を実装するときに重要になります。Ajaxでは、一意のidを用いてページ要素にアクセスすることができます。

### Homeページにフォロワーの統計情報を追加する

Homeページにフォロワーの統計情報を追加するためには、以下のようなerbコードを用います。

```erb:
<section class="stats">
  <%= render 'shared/stats' %>
</section>
```

私の場合、これまでの演習で、ログイン時と非ログイン時のHomeページの実装をそれぞれ別のパーシャルに分離していました。そのため、実装先は「ログイン時のHomeページで用いるパーシャル」となります。具体的には`app/views/shared/_home_logged_in.erb`というファイルですね。

```diff: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`に追加していきます。変更内容は以下のようになります。

```diff: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ページにフォロー関係の統計情報が表示されるようになります。レイアウトが崩れることもありません。

<img width="1161" alt="スクリーンショット 2020-02-01 16.42.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/f740a181-9592-62e0-005e-469caa138d74.png">

# [follow]/[unfollow]ボタンの実装

## フォロー/フォロー解除フォームのパーシャル

まずは、「[follow]ボタン/[unfollow]ボタンのいずれを表示するか、表示しないか」を決めるためのパーシャルを実装していきます。ファイル名は`app/views/users/_follow_form.html.erb`とします。

```erb: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`というパーシャルを実装するためには、これらの動作に対応するルーティングを先に実装しておく必要があります。コードとしては以下のようになります。

```ruby:
resources :relationships, only: [:create, :destroy]
```

`config/routes.rb`に適用する変更の全体像は以下のようになります。

```diff: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
```

## ユーザーをフォローするフォーム

```erb:app/views/users/_follow_form.html.erb
<%= 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`要素が生成されます。

```html:
<input id="follower_id" name="followed_id" type="hidden" value="3">
```

このようにすることにより、フォロー対象のユーザーのidが、`followed_id`属性の値として`POST`リクエストに無事渡されるのです。

## ユーザーをフォロー解除するフォーム

```erb:app/views/users/_unfollow.html.erb
<%= 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]ボタンに関するパーシャルの実装が完成したことにより、先に実装していたフォロワーの統計情報パーシャルとともに、各ユーザーのプロフィールページに必要なパーシャルの実装が出揃いました。各ユーザーのプロフィールページにこれらのパーシャルを追加していきましょう。

```diff:
  <% 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]ボタンが表示されるようになります。

<img width="1161" alt="スクリーンショット 2020-02-01 22.59.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/088e238b-7fda-aedb-ab1b-40d130c22b3a.png">

上記スクリーンショットは[Follow]ボタンの表示例です。

<img width="1161" alt="スクリーンショット 2020-02-01 22.59.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/39fe0772-ad51-3600-9816-af8b8f0a150f.png">

上記スクリーンショットは[Unfollow]ボタンの表示例です。

なお、これらのボタンの動作はまだ実装されていませんので、現在のところは、これらのボタンをクリックしても何も起こりません。