見出し画像

railsチュートリアル挑戦記 第14章 ユーザーをフォローする

railsチュートリアルをやりながらメモしたことをそのまま記述しています。

第14章ユーザーをフォローする

他のユーザーをフォロしたりフォロー解除したり、
フォローしているユーザーの投稿をステータスフィードに表示する機能を追加する。

モデリングについて
モデリング結果に対応するWebインターフェースを実装

Ajaxについても紹介

ステータスフィードの完成版を実装

本書の中で最も難易度の高い手法をいくつか使う。
ステータスフィード作成のためにRuby/SQLを「だます」テクニックも含まれる

これまでよりも複雑なデータモデルを使う・

ここで学んだデータモデルは、今後自分用のWebアプリケーションを開発するときに必ず役に立つ

この章で扱っている手法は本書全体の中で最も難易度が高いため、理解を助けるためにコードを書く前にはいったん立ち止まってインターフェースを探検する。

ページ操作の全体的なフローは次の通り

あるユーザーは自分のプロフィールページを最初に表示し、
フォローするユーザーを選択するためにUsersページに移動し、
フォローするユーザーを選択するためにUsersページに移動し、
ユーザーは2番目のユーザーを表示し、フォローする
フォローするとフォロー解除が現れ
2番目のユーザーのフォロワーカウントが1人増え
2番目のユーザーのマイクロポストがステータスフィードに表示されるようになる

このフローの実現に専念


・14 1 Relationshipモデル

has_manyを使えば実現できるようなものではない。たちまち壁に突き当たる。
それを解決するためにhas_many throughについて説明する

いつものようにトピックブランチを作成

git checkout -b following-users

・14 1 1 データモデルの問題 (および解決策)

userとuserを繋ぐテーブルactive_relationshipsを作成する
follower_idはフォロー元のユーザー
followed_idはフォロー先のユーザー

なので、以下のコマンドで作成する
rails generate model Relationship follower_id:integer followed_id:integer


ログ
----------------------------
ec2-user:~/environment/sample_app (following-users) $ rails generate model Relationship follower_id:integer followed_id:integer
Running via Spring preloader in process 4097
     invoke  active_record
     create    db/migrate/20200106101425_create_relationships.rb
     create    app/models/relationship.rb
     invoke    test_unit
     create      test/models/relationship_test.rb
     create      test/fixtures/relationships.yml
ec2-user:~/environment/sample_app (following-users) $  

----------------------------

それぞれのカラムにインデックスを追加する

リスト 14.1: relationshipsテーブルにインデックスを追加する
db/migrate/[timestamp]_create_relationships.rb
----------------------------
class CreateRelationships < ActiveRecord::Migration[5.0]
 def change
   create_table :relationships do |t|
     t.integer :follower_id
     t.integer :followed_id
     t.timestamps
   end
   add_index :relationships, :follower_id
   add_index :relationships, :followed_id
   add_index :relationships, [:follower_id, :followed_id], unique: true
 end
end

一番下のadd_indexは、uniqueと書かれている
これは必ずユニークであることを保証する仕組み

いつものようにマイグレーション
rails db:migrate

・14 1 2 User/Relationshipの関連付け

UserとRelationshipの関連付けを行う


リスト 14.2: 能動的関係に対して1対多 (has_many) の関連付けを実装する
app/models/user.rb
----------------------------
class User < ApplicationRecord
 has_many :microposts, dependent: :destroy
 has_many :active_relationships, class_name:  "Relationship",
                                 foreign_key: "follower_id",
                                 dependent:   :destroy
 .
 .
 .
end
リスト 14.3: リレーションシップ/フォロワーに対してbelongs_toの関連付けを追加する
app/models/relationship.rb
----------------------------
class Relationship < ApplicationRecord
 belongs_to :follower, class_name: "User"
 belongs_to :followed, class_name: "User"
end

ここまでで、使えるようになったメソッドと、その用途を以下にまとめる

メソッド 用途
active_relationship.follower フォロワーを返します
active_relationship.followed フォローしているユーザーを返します
user.active_relationships.create(followed_id: other_user.id) userと紐付けて能動的関係を作成/登録する
user.active_relationships.create!(followed_id: other_user.id) userを紐付けて能動的関係を作成/登録する (失敗時にエラーを出力)
user.active_relationships.build(followed_id: other_user.id) userと紐付けた新しいRelationshipオブジェクトを返す

・14 1 3 Relationshipのバリデーション

まずはテストを作成する
fixtureはいったん空にしておく

リスト 14.4: Relationshipモデルのバリデーションをテストする
test/models/relationship_test.rb
----------------------------
require 'test_helper'
class RelationshipTest < ActiveSupport::TestCase
 def setup
   @relationship = Relationship.new(follower_id: users(:michael).id,
                                    followed_id: users(:archer).id)
 end
 test "should be valid" do
   assert @relationship.valid?
 end
 test "should require a follower_id" do
   @relationship.follower_id = nil
   assert_not @relationship.valid?
 end
 test "should require a followed_id" do
   @relationship.followed_id = nil
   assert_not @relationship.valid?
 end
end
リスト 14.5: Relationshipモデルに対してバリデーションを追加する
app/models/relationship.rb
----------------------------
class Relationship < ApplicationRecord
 belongs_to :follower, class_name: "User"
 belongs_to :followed, class_name: "User"
 validates :follower_id, presence: true
 validates :followed_id, presence: true
end
リスト 14.6: Relationship用のfixtureを空にする green
test/fixtures/relationships.yml
----------------------------
# 空にする


ここの時点でのテストは成功するはず

リスト 14.7: green
----------------------------
rails test

通った!!!


・14 1 4 フォローしているユーザー

followingがフォロー先ユーザー
followersはフォロー元ユーザー

リスト 14.8: Userモデルにfollowingの関連付けを追加する
app/models/user.rb
----------------------------
class User < ApplicationRecord
 has_many :microposts, dependent: :destroy
 has_many :active_relationships, class_name:  "Relationship",
                                 foreign_key: "follower_id",
                                 dependent:   :destroy
 has_many :following, through: :active_relationships, source: :followed
 .
 .
 .
end

これにより、フォロー先ユーザーを配列のように扱えるようになった
user.following.include?(other_user) 指定したユーザーはフォローしてる?
user.following.find(other_user) 指定したユーザーがフォロー先ユーザー情報にいるか探索して返す
user.following << other_user 指定したユーザーを、フォロー先ユーザーとして追加
user.following.delete(other_user) 指定したユーザーを、フォロー先ユーザーから削除

followやunfollowといった便利メソッドを追加する
テストから先に書く

リスト 14.9: “following” 関連のメソッドをテストする red
test/models/user_test.rb
----------------------------
require 'test_helper'
class UserTest < ActiveSupport::TestCase
 .
 .
 .
 test "should follow and unfollow a user" do
   michael = users(:michael)
   archer  = users(:archer)
   assert_not michael.following?(archer)
   michael.follow(archer)
   assert michael.following?(archer)
   michael.unfollow(archer)
   assert_not michael.following?(archer)
 end
end

実際にフォローするメソッドとフォロー解除するメソッドと
現在のユーザーがフォローしてたらtrueを返すメソッドを追加する

リスト 14.10: "following" 関連のメソッド green
app/models/user.rb
----------------------------
class User < ApplicationRecord
 .
 .
 .
 def feed
   .
   .
   .
 end
 # ユーザーをフォローする
 def follow(other_user)
   following << other_user
 end
 # ユーザーをフォロー解除する
 def unfollow(other_user)
   active_relationships.find_by(followed_id: other_user.id).destroy
 end
 # 現在のユーザーがフォローしてたらtrueを返す
 def following?(other_user)
   following.include?(other_user)
 end
 private
 .
 .
 .
end

これでテストが動く!

リスト 14.11: green
----------------------------
rails test

動いた!

・14 1 5 フォロワー

フォロワー部分を実装していく
フォローと似たようなことをしていく

リスト 14.12: 受動的関係を使ってuser.followersを実装する
app/models/user.rb
----------------------------
sclass User < ApplicationRecord
 has_many :microposts, dependent: :destroy
 has_many :active_relationships,  class_name:  "Relationship",
                                  foreign_key: "follower_id",
                                  dependent:   :destroy
 has_many :passive_relationships, class_name:  "Relationship",
                                  foreign_key: "followed_id",
                                  dependent:   :destroy
 has_many :following, through: :active_relationships,  source: :followed
 has_many :followers, through: :passive_relationships, source: :follower
 .
 .
 .
end

テストを書いていく

リスト 14.13: followersに対するテスト green
test/models/user_test.rb
----------------------------
srequire 'test_helper'
class UserTest < ActiveSupport::TestCase
 .
 .
 .
 test "should follow and unfollow a user" do
   michael  = users(:michael)
   archer   = users(:archer)
   assert_not michael.following?(archer)
   michael.follow(archer)
   assert michael.following?(archer)
   assert archer.followers.include?(michael)
   michael.unfollow(archer)
   assert_not michael.following?(archer)
 end
end


ここで、テストは成功するはず

rails test


・14 2 [Follow] のWebインターフェイス


・14 2 1 フォローのサンプルデータ

あらかじめ自動的にフォローとフォロワーの関係を作っておく

リスト 14.14: サンプルデータにfollowing/followerの関係性を追加する
db/seeds.rb
# ユーザー
User.create!(name:  "Example User",
            email: "example@railstutorial.org",
            password:              "foobar",
            password_confirmation: "foobar",
            admin:     true,
            activated: true,
            activated_at: Time.zone.now)
99.times do |n|
 name  = Faker::Name.name
 email = "example-#{n+1}@railstutorial.org"
 password = "password"
 User.create!(name:  name,
              email: email,
              password:              password,
              password_confirmation: password,
              activated: true,
              activated_at: Time.zone.now)
end
# マイクロポスト
users = User.order(:created_at).take(6)
50.times do
 content = Faker::Lorem.sentence(5)
 users.each { |user| user.microposts.create!(content: content) }
end
# リレーションシップ
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) }
rails db:migrate:reset
rails db:seed


・14 2 2 統計と [Follow] フォーム

フォローしているユーザーとフォロワーの統計情報を表示するためのパーシャルを作成していく。

リスト 14.15: Usersコントローラにfollowingアクションとfollowersアクションを追加する
config/routes.rb
----------------------------
Rails.application.routes.draw do
 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'
 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]
end

これにより、以下の表のようになる
HTTPリクエスト URL アクション 名前付きルート
GET /users/1/following following following_user_path(1)
GET /users/1/followers followers followers_user_path(1)

フォローとフォロワーのリンクを追加する

リスト 14.16: フォロワーの統計情報を表示するパーシャル
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にも追加する

リスト 14.17: Homeページにフォロワーの統計情報を追加する
app/views/static_pages/home.html.erb
----------------------------
<% if logged_in? %>
 <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>
<% else %>
 .
 .
 .
<% end %>

見た目を整える

リスト 14.18: Homeページのサイドバー用のSCSS
app/assets/stylesheets/custom.scss
----------------------------
.
.
.
/* sidebar */
.
.
.
.gravatar {
 float: left;
 margin-right: 10px;
}
.gravatar_edit {
 margin-top: 15px;
}
.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 */
.
.
.

今のうちにフォロー・フォロー解除ボタン用のパーシャルも作っておく

リスト 14.19: フォロー/フォロー解除フォームのパーシャル
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リソース用の新しいルーティングが必要
以下の通り

リスト 14.20: Relationshipリソース用のルーティングを追加する
config/routes.rb
----------------------------
Rails.application.routes.draw do
 root                'static_pages#home'
 get    'help'    => 'static_pages#help'
 get    'about'   => 'static_pages#about'
 get    'contact' => 'static_pages#contact'
 get    'signup'  => 'users#new'
 get    'login'   => 'sessions#new'
 post   'login'   => 'sessions#create'
 delete 'logout'  => '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

フォローとフォロー解除用のパーシャルは以下の通り

リスト 14.21: ユーザーをフォローするフォーム
app/views/users/_follow.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 %>
リスト 14.22: ユーザーをフォロー解除するフォーム
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 %>

プロフィールにフォローとフォロー解除のボタンをそれぞれ表示するようにする

リスト 14.23: プロフィールページにフォロー用フォームとフォロワーの統計情報を追加する
app/views/users/show.html.erb
----------------------------
<% 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 class="stats">
     <%= 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>

・14 2 3 [Following] と [Followers] ページ

フォロー全員を表示するページと、
フォロワー全員を表示するページを作っていく

ログインしていないとアクセスできないようにしておく
そのためのテストをまずは書く

リスト 14.24: フォロー/フォロワーページの認可をテストする red
test/controllers/users_controller_test.rb
----------------------------
require 'test_helper'
class UsersControllerTest < ActionDispatch::IntegrationTest
 def setup
   @user = users(:michael)
   @other_user = users(:archer)
 end
 .
 .
 .
 test "should redirect following when not logged in" do
   get following_user_path(@user)
   assert_redirected_to login_url
 end
 test "should redirect followers when not logged in" do
   get followers_user_path(@user)
   assert_redirected_to login_url
 end
end

リスト 14.25: followingアクションとfollowersアクション red
app/controllers/users_controller.rb
----------------------------
class UsersController < ApplicationController
 before_action :logged_in_user, only: [:index, :edit, :update, :destroy,
                                       :following, :followers]
 .
 .
 .
 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
 private
 .
 .
 .
end

テスト通りになるように実装

リスト 14.26: フォローしているユーザーとフォロワーの両方を表示するshow_followビュー green
app/views/users/show_follow.html.erb
----------------------------
<% provide(:title, @title) %>
<div class="row">
 <aside class="col-md-4">
   <section class="user_info">
     <%= gravatar_for @user %>
     <h1><%= @user.name %></h1>
     <span><%= link_to "view my profile", @user %></span>
     <span><b>Microposts:</b> <%= @user.microposts.count %></span>
   </section>
   <section class="stats">
     <%= render 'shared/stats' %>
     <% if @users.any? %>
       <div class="user_avatars">
         <% @users.each do |user| %>
           <%= link_to gravatar_for(user, size: 30), user %>
         <% end %>
       </div>
     <% end %>
   </section>
 </aside>
 <div class="col-md-8">
   <h3><%= @title %></h3>
   <% if @users.any? %>
     <ul class="users follow">
       <%= render @users %>
     </ul>
     <%= will_paginate %>
   <% end %>
 </div>
</div>

テストでエラーが出ないことを確認


リスト 14.27: green
----------------------------
rails test
ログ
----------------------------
ec2-user:~/environment/sample_app (following-users) $ rails test
Running via Spring preloader in process 6106
Traceback (most recent call last):
       29: from -e:1:in `<main>'
       28: from /home/ec2-user/.rvm/rubies/ruby-2.6.3/lib/ruby/site_ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require'
       27: from /home/ec2-user/.rvm/rubies/ruby-2.6.3/lib/ruby/site_ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require'
       26: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/activesupport-5.1.6/lib/active_support/dependencies.rb:286:in `load'
       25: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/activesupport-5.1.6/lib/active_support/dependencies.rb:258:in `load_dependency'
       24: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/activesupport-5.1.6/lib/active_support/dependencies.rb:286:in `block in load'
       23: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/activesupport-5.1.6/lib/active_support/dependencies.rb:286:in `load'
       22: from /home/ec2-user/environment/sample_app/bin/rails:9:in `<top (required)>'
       21: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/activesupport-5.1.6/lib/active_support/dependencies.rb:292:in `require'
       20: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/activesupport-5.1.6/lib/active_support/dependencies.rb:258:in `load_dependency'
       19: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/activesupport-5.1.6/lib/active_support/dependencies.rb:292:in `block in require'
       18: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/activesupport-5.1.6/lib/active_support/dependencies.rb:292:in `require'
       17: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/railties-5.1.6/lib/rails/commands.rb:16:in `<top (required)>'
       16: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/railties-5.1.6/lib/rails/command.rb:44:in `invoke'
       15: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/railties-5.1.6/lib/rails/command/base.rb:63:in `perform'
       14: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/thor-1.0.1/lib/thor.rb:392:in `dispatch'
       13: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/thor-1.0.1/lib/thor/invocation.rb:127:in `invoke_command'
       12: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/thor-1.0.1/lib/thor/command.rb:27:in `run'
       11: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/railties-5.1.6/lib/rails/commands/test/test_command.rb:38:in `perform'
       10: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/railties-5.1.6/lib/rails/test_unit/runner.rb:39:in `run'
        9: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/railties-5.1.6/lib/rails/test_unit/runner.rb:50:in `load_tests'
        8: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/railties-5.1.6/lib/rails/test_unit/runner.rb:50:in `each'
        7: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/railties-5.1.6/lib/rails/test_unit/runner.rb:50:in `block in load_tests'
        6: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/activesupport-5.1.6/lib/active_support/dependencies.rb:292:in `require'
        5: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/activesupport-5.1.6/lib/active_support/dependencies.rb:258:in `load_dependency'
        4: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/activesupport-5.1.6/lib/active_support/dependencies.rb:292:in `block in require'
        3: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/activesupport-5.1.6/lib/active_support/dependencies.rb:292:in `require'
        2: from /home/ec2-user/environment/sample_app/test/models/user_test.rb:3:in `<top (required)>'
        1: from /home/ec2-user/environment/sample_app/test/models/user_test.rb:96:in `<class:UserTest>'
/home/ec2-user/.rvm/gems/ruby-2.6.3/gems/activesupport-5.1.6/lib/active_support/testing/declarative.rb:14:in `test': test_should_follow_and_unfollow_a_user is already defined in UserTest (RuntimeError)
----------------------------


一瞬びびったけどすぐ解決
テスト名が重複している箇所があっておかしなことになった

治したらテスト成功!

統合テストも書いていく
いつものように生成
rails generate integration_test following

ログ
----------------------------
ec2-user:~/environment/sample_app (following-users) $ rails generate integration_test following
Running via Spring preloader in process 6197
     invoke  test_unit
     create    test/integration/following_test.rb
----------------------------

fixtureを書いていく
リスト 14.28: following/followerをテストするためのリレーションシップ用fixture
test/fixtures/relationships.yml
----------------------------
one:
 follower: michael
 followed: lana
two:
 follower: michael
 followed: malory
three:
 follower: lana
 followed: michael
four:
 follower: archer
 followed: michael

正しいURLか、フォローとフォロワーの数が正しいかどうか、をテストする

リスト 14.29: following/followerページのテスト green
test/integration/following_test.rb
----------------------------
require 'test_helper'
class FollowingTest < ActionDispatch::IntegrationTest
 def setup
   @user = users(:michael)
   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
 test "followers page" do
   get followers_user_path(@user)
   assert_not @user.followers.empty?
   assert_match @user.followers.count.to_s, response.body
   @user.followers.each do |user|
     assert_select "a[href=?]", user_path(user)
   end
 end
end

テスト通りになることを確認

リスト 14.30: green
----------------------------
rails test

おっけい!!!


・14 2 4 [Follow] ボタン (基本編)

ボタンが動作するようにしていく

Relationshipsコントローラが必要なので以下のようにする
rails generate controller Relationships

ログ
----------------------------
ec2-user:~/environment/sample_app (following-users) $ rails generate controller Relationships
Running via Spring preloader in process 6661
     create  app/controllers/relationships_controller.rb
     invoke  erb
     create    app/views/relationships
     invoke  test_unit
     create    test/controllers/relationships_controller_test.rb
     invoke  helper
     create    app/helpers/relationships_helper.rb
     invoke    test_unit
     invoke  assets
     invoke    coffee
     create      app/assets/javascripts/relationships.coffee
     invoke    scss
     create      app/assets/stylesheets/relationships.scss
ec2-user:~/environment/sample_app (following-users) $ 
----------------------------


ログイン済みでなければRelationshipのカウントが変わっていないことを確認する
テストをきっちり書く

リスト 14.31: リレーションシップの基本的なアクセス制御に対するテスト red
test/controllers/relationships_controller_test.rb
----------------------------
require 'test_helper'
class RelationshipsControllerTest < ActionDispatch::IntegrationTest
 test "create should require logged-in user" do
   assert_no_difference 'Relationship.count' do
     post relationships_path
   end
   assert_redirected_to login_url
 end
 test "destroy should require logged-in user" do
   assert_no_difference 'Relationship.count' do
     delete relationship_path(relationships(:one))
   end
   assert_redirected_to login_url
 end
end

Relationshipsコントローラのアクションに対してbeforeを追加する

リスト 14.32: リレーションシップのアクセス制御 green
app/controllers/relationships_controller.rb
----------------------------
class RelationshipsController < ApplicationController
 before_action :logged_in_user
 def create
 end
 def destroy
 end
 
end

ボタンを正常動作させるために、対応するユーザーを見つけてこなければならない
以下の通り

リスト 14.33: Relationshipsコントローラ
app/controllers/relationships_controller.rb
----------------------------
class RelationshipsController < ApplicationController
 before_action :logged_in_user
 def create
   user = User.find(params[:followed_id])
   current_user.follow(user)
   redirect_to user
 end
 def destroy
   user = Relationship.find(params[:id]).followed
   current_user.unfollow(user)
   redirect_to user
 end
end

これで、フォローとフォロー解除がちゃんと動く


・14 2 5 [Follow] ボタン (Ajax編)

Ajaxを使うと、サーバーに非同期でページを移動することなくリクエストを送信することが可能

WebフォームにAjaxを採用するのは今や当たり前になりつつあるので、RailsでもAjaxを簡単に実装できるようになっている。

ほんの数文字書き足す程度

便利!

以下の通り

リスト 14.34: Ajaxを使ったフォローフォーム
app/views/users/_follow.html.erb
----------------------------
<%= form_for(current_user.active_relationships.build, remote: true) do |f| %>
 <div><%= hidden_field_tag :followed_id, @user.id %></div>
 <%= f.submit "Follow", class: "btn btn-primary" %>
<% end %>
リスト 14.35: Ajaxを使ったフォロー解除フォーム
app/views/users/_unfollow.html.erb
----------------------------
<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
            html: { method: :delete },
            remote: true) do |f| %>
 <%= f.submit "Unfollow", class: "btn" %>
<% end %>

ajaxに対応するために、respond_toをcreateとdestroyに追加

リスト 14.36: RelationshipsコントローラでAjaxリクエストに対応する
app/controllers/relationships_controller.rb
----------------------------
class RelationshipsController < ApplicationController
 before_action :logged_in_user
 def create
   @user = User.find(params[:followed_id])
   current_user.follow(@user)
   respond_to do |format|
     format.html { redirect_to @user }
     format.js
   end
 end
 def destroy
   @user = Relationship.find(params[:id]).followed
   current_user.unfollow(@user)
   respond_to do |format|
     format.html { redirect_to @user }
     format.js
   end
 end
end

ブラウザ側でJavaScriptが無効になっていた場合でも上手く動くようにしておく

リスト 14.37: JavaScriptが無効になっていたときのための設定
config/application.rb
----------------------------
require File.expand_path('../boot', __FILE__)
.
.
.
module SampleApp
 class Application < Rails::Application
   .
   .
   .
   # 認証トークンをremoteフォームに埋め込む
   config.action_view.embed_authenticity_token_in_remote_forms = true
 end
end

javascriptを動かせるようにするために、以下も必要。
追加する。


リスト 14.38: JavaScriptと埋め込みRubyを使ってフォローの関係性を作成する
app/views/relationships/create.js.erb
----------------------------
$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>");
$("#followers").html('<%= @user.followers.count %>');
リスト 14.39: Ruby JavaScript (RJS) を使ってフォローの関係性を削除する
app/views/relationships/destroy.js.erb
----------------------------
$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>");
$("#followers").html('<%= @user.followers.count %>');


これで、プロフィールページを更新させずにフォローとフォロー解除ができるようになった。


・14 2 6 フォローをテストする

フォローのテストを書いていく

リスト 14.40: [Follow] / [Unfollow] ボタンをテストする green
test/integration/following_test.rb
----------------------------
require 'test_helper'
class FollowingTest < ActionDispatch::IntegrationTest
 def setup
   @user  = users(:michael)
   @other = users(:archer)
   log_in_as(@user)
 end
 .
 .
 .
 test "should follow a user the standard way" do
   assert_difference '@user.following.count', 1 do
     post relationships_path, params: { followed_id: @other.id }
   end
 end
 test "should follow a user with Ajax" do
   assert_difference '@user.following.count', 1 do
     post relationships_path, xhr: true, params: { followed_id: @other.id }
   end
 end
 test "should unfollow a user the standard way" do
   @user.follow(@other)
   relationship = @user.active_relationships.find_by(followed_id: @other.id)
   assert_difference '@user.following.count', -1 do
     delete relationship_path(relationship)
   end
 end
 test "should unfollow a user with Ajax" do
   @user.follow(@other)
   relationship = @user.active_relationships.find_by(followed_id: @other.id)
   assert_difference '@user.following.count', -1 do
     delete relationship_path(relationship), xhr: true
   end
 end
end

テスト結果は問題なしになるはず。

リスト 14.41: green
----------------------------
rails test

よし!!!エラーにならずに済んだ!

・14 3 ステータスフィード

フォロー中のフィードとかを表示していく。
高度。

・14 3 1 動機と計画

まずはテストを書いていく


リスト 14.42: ステータスフィードのテスト red
test/models/user_test.rb
----------------------------
require 'test_helper'
class UserTest < ActiveSupport::TestCase
 .
 .
 .
 test "feed should have the right posts" do
   michael = users(:michael)
   archer  = users(:archer)
   lana    = users(:lana)
   # フォローしているユーザーの投稿を確認
   lana.microposts.each do |post_following|
     assert michael.feed.include?(post_following)
   end
   # 自分自身の投稿を確認
   michael.microposts.each do |post_self|
     assert michael.feed.include?(post_self)
   end
   # フォローしていないユーザーの投稿を確認
   archer.microposts.each do |post_unfollowed|
     assert_not michael.feed.include?(post_unfollowed)
   end
 end
end
リスト 14.43: red
----------------------------
rails test
ログ
----------------------------
ec2-user:~/environment/sample_app (following-users) $ rails test
Running via Spring preloader in process 7770
Started with run options --seed 27766
FAIL["test_feed_should_have_the_right_posts", UserTest, 2.793541930000174]
test_feed_should_have_the_right_posts#UserTest (2.79s)
       Expected false to be truthy.
       test/models/user_test.rb:102:in `block (2 levels) in <class:UserTest>'
       test/models/user_test.rb:101:in `block in <class:UserTest>'
 73/73: [========================] 100% Time: 00:00:02, Time: 00:00:02
Finished in 2.83447s
73 tests, 334 assertions, 1 failures, 0 errors, 0 skips
ec2-user:~/environment/sample_app (following-users) $ 
----------------------------

・14 3 2 フィードを初めて実装する

ユーザーのステータスフィードを返す

リスト 14.44: とりあえず動くフィードの実装 green
app/models/user.rb
----------------------------
class User < ApplicationRecord
 .
 .
 .
 # パスワード再設定の期限が切れている場合はtrueを返す
 def password_reset_expired?
   reset_sent_at < 2.hours.ago
 end
 # ユーザーのステータスフィードを返す
 def feed
   Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
 end
 # ユーザーをフォローする
 def follow(other_user)
   following << other_user
 end
 .
 .
 .
end

これでテストは一応成功するはず

リスト 14.45: green
----------------------------
rails test

たしかに


・14 3 3 サブセレクト

投稿されたマイクロポストの数が膨大になったときにうまくスケールしなくなる。
フォローしているユーザーが5000人程度になるとWebサービス全体が遅くなる可能性がある。
フォローしているユーザー数に応じてスケールできるように、ステータスフィードを改善していく

SQLのサブセレクトを使っていく

まずはリファクタリング

リスト 14.46: whereメソッド内の変数に、キーと値のペアを使う green
app/models/user.rb
----------------------------
class User < ApplicationRecord
 .
 .
 .
 # ユーザーのステータスフィードを返す
 def feed
   Micropost.where("user_id IN (:following_ids) OR user_id = :user_id",
    following_ids: following_ids, user_id: id)
 end
 .
 .
 .
end

完成形は以下の通りとなる

リスト 14.47: フィードの最終的な実装 green
app/models/user.rb
----------------------------
class User < ApplicationRecord
 .
 .
 .
 # ユーザーのステータスフィードを返す
 def feed
   following_ids = "SELECT followed_id FROM relationships
                    WHERE follower_id = :user_id"
   Micropost.where("user_id IN (#{following_ids})
                    OR user_id = :user_id", user_id: id)
 end
 .
 .
 .
end

テストもうまく行くはず

リスト 14.48: green
----------------------------
rails test

うむ

大規模なWebサービスでは、バックグラウンド処理を使ってフィードを非同期で生成するなどのさらなる改善が必要。

ただし、Webサービスをスケールさせる技術は非常に高度かつデリケートな問題なので、今回はここまでの改善でやめておく。

演習
Homeページで表示される1ページ目のフィードに対して、統合テストを書いてみましょう。リスト 14.49はそのテンプレートです。

リスト 14.49のコードでは、期待されるHTMLをCGI.escapeHTMLメソッドでエスケープしています (このメソッドは11.2.3で扱ったCGI.escapeと同じ用途です)。このコードでは、なぜHTMLをエスケープさせる必要があったのでしょうか? 考えてみてください。ヒント: 試しにエスケープ処理を外して、得られるHTMLの内容を注意深く調べてください。マイクロポストの内容が何かおかしいはずです。また、ターミナルの検索機能 (Cmd-FもしくはCtrl-F) を使って「sorry」を探すと原因の究明に役立つはずです。

リスト 14.49: フィードのHTMLをテストする green
test/integration/following_test.rb
----------------------------
require 'test_helper'
class FollowingTest < ActionDispatch::IntegrationTest
 def setup
   @user = users(:michael)
   log_in_as(@user)
 end
 .
 .
 .
 test "feed on Home page" do
   get root_path
   @user.feed.paginate(page: 1).each do |micropost|
     assert_match CGI.escapeHTML(micropost.content), response.body
   end
 end
end

ここまでやったらBitbucketとHerokuにpushする

rails test
git add -A
git commit -m "Add 14"
git checkout master
git merge following-users
git push origin master

source <(curl -sL https://cdn.learnenough.com/heroku_install)
heroku login --interactive
heroku create
git remote set-url heroku *******.git
heroku rename jun-killer-rails14
heroku maintenance:on
heroku config:get S3_ACCESS_KEY
heroku config:get S3_SECRET_KEY
heroku config:get S3_BUCKET
heroku config:get S3_REGION

heroku config:set S3_ACCESS_KEY="
heroku config:set S3_SECRET_KEY="
heroku config:set S3_BUCKET="
heroku config:set S3_REGION="ap-northeast-1"

※ここでconfig/environments/production.rbのアプリ名を変えないといけない
heroku addons:create sendgrid:starter
git push heroku master
heroku pg:reset DATABASE
jun-killer-rails14
heroku run rails db:migrate
heroku run rails db:seed
heroku restart
heroku maintenance:off

https://jun-killer-rails14.herokuapp.com/

全部終わったーーーーーー!!!!!
完走!


・14 4 最後に

ここまでで、主要な機能を多数取り扱った
モデル
ビュー
コントローラ
テンプレート
パーシャル
beforeフィルター
バリデーション
コールバック
has_many
belongs_to
has_many through
セキュリティ
テスティング
デプロイ

・14 4 1 サンプルアプリケーションの機能を拡張する

Railsアプリケーションに何らかの機能を実装していて困ったときは、以下の2つをチェックする

Railsガイド
https://railsguides.jp/

Rails API
https://api.rubyonrails.org/

できるだけ念入りにGoogleで検索し、自分が調べようとしているトピックに言及しているブログやチュートリアルがないかどうか、よく探してみること。

Webアプリケーションの開発には常に困難がつきまとう。
他人の経験と失敗から学ぶことも重要。

Gemfileのバージョンを固定しているので、なるべく最新のバージョンを扱うべき。
古いバージョンだと既に対策済みの脆弱性が残っていたり、それが原因でユーザーのログイン情報などが漏れる恐れもある

参考記事:Railsエンジニアのためのウェブセキュリティ入門
https://yasslab.jp/ja/news/secure-programming-with-rails

RailsガイドにRailsアップグレードガイドがあるので、この演習課題に取り組むときは参考にしてみる

https://yasslab.jp/ja/news/secure-programming-with-rails

has_many :throughを使うと、複雑なデータ関係をモデリングできる
has_manyメソッドには、クラス名や外部キーなど、いくつものオプションを渡すことができる
適切なクラス名と外部キーと一緒にhas_many/has_many :throughを使うことで、能動的関係(フォローする)や受動的関係(フォローされる)がモデリングできる
ルーティングは、実はネストさせて使うことができる
whereメソッドを使うと、柔軟で協力なデータベースへの問い合わせが作成できる
railsは(必要に応じて)低級なSQLクエリを呼び出すことができる
本書で学んだすべてを駆使することで、フォローしているユーザーのマイクロポスト一覧をステータスフィールドに表示させることができた

返信機能

micropostsテーブルのin_reply_toカラム
including_repliesスコープをMicropostモデルに追加する必要がある

以下が参考になる
https://railsguides.jp/active_record_querying.html#%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%97

また、ユーザー名が重なり得るので、ユーザー名を一意に表す方法も考えなければならない。
1つの方法が、idと名前を組み合わせて@1-michael-hartlのようにすること
もう1つが、ユーザー登録の項目に一意のユーザー名を追加し、@replyで使えるようにすること

メッセージ機能

フォロワーの通知

RSSフィード

REST API

検索機能

他の拡張機能

いいね

シェア

minitestの代わりにRSpecで書き直す

erbの代わりにHamlで書き直す

エラーメッセージをI18nで日本語化する

・14 4 2 読み物ガイド

良さげなものをピックアップ

Everyday rails - RSpecによるRailsテスト入門
railsチュートリアル完走者を対象にしたテストの入門書籍
プロのエンジニアはRSpecでテストを書く

プロを目指す人のためのRuby入門-言語仕様からテスト駆動開発・デバッグ技法まで
プロとして通用するRubyのコードを書きたい人にオススメ
Kindle版と書籍版の2つがある

現場で使えるRuby on Rails5速習実践ガイド
Railsチュートリアルではminitestやerbなど、Railsのデフォルト機能を使ってSNS開発したが、実際の現場ではより多様なgemを駆使して開発が進む。
RSpecやSlimなど、Railsチュートリアルでは紹介しきれなかった様々なgemや、現場で役立つ実践的な考え方に触れることができる
Kindle版と書籍版の2つがある

・14 4 3 本章のまとめ


has_many :throughを使うと、複雑なデータ関係をモデリングできる
has_manyメソッドには、クラス名や外部キーなど、いくつものオプションを渡すことができる
適切なクラス名と外部キーと一緒にhas_many/has_many :throughを使うことで、能動的関係(フォローする)や受動的関係(フォローされる)がモデリングできるようになたt・
実はルーティングはネストさせて使うことができる
whereメソッドを使うと、柔軟で強力なデータベースへの問い合わせが作成できる
Railsは必要に応じて低級なSQLクエリを呼び出すことができる
フォローしているユーザーのマイクロポスト一覧をステータスフィードに表示させることができるようになった。


この記事が気に入ったらサポートをしてみませんか?