0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-02-02

概要

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

  • フォローしているユーザーとフォロワーの統計情報(数)を表示するためのパーシャル
  • フォローおよびフォロー解除用のフォーム
  • フォローしているユーザー("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に適用する変更の内容は以下のようになります。

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

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に代入する」という動作をします。具体的には、「@usernilである場合」ですね。逆に、@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というファイルですね。

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

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

スクリーンショット 2020-02-01 16.42.02.png

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

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

まずは、「[follow]ボタン/[unfollow]ボタンのいずれを表示するか、表示しないか」を決めるためのパーシャルを実装していきます。ファイル名はapp/views/users/_follow_form.html.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というパーシャルを実装するためには、これらの動作に対応するルーティングを先に実装しておく必要があります。コードとしては以下のようになります。

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

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

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

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要素が生成されます。

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

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

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

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

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

スクリーンショット 2020-02-01 22.59.29.png

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

スクリーンショット 2020-02-01 22.59.35.png

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

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

  1. アプリケーションの他の部分とここまで成功しているテストスイートの実装、いずれも変更がなければ、当該変更を適用しても、テストスイート全体を対象としたテストは成功します。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?