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クエリを呼び出すことができる
フォローしているユーザーのマイクロポスト一覧をステータスフィードに表示させることができるようになった。
この記事が気に入ったらサポートをしてみませんか?