第10章 ユーザーの更新・表示・削除
テストを入れてもREDが解消しない。
自分もそう思うってこともあって、サンプルのチュートリアルを見てみたりしているのだけど。
ってことで、ここでREDで更新したわ。
どこかで解消するか?
- 第10章 ユーザーの更新・表示・削除
- ユーザーを更新する
- 編集フォーム
- 認可
- beforeフィルターにlogged_in_userを追加する
- テストユーザーでログインする
- editとupdateアクションの保護に対するテストする
- fixtureファイルに2人目のユーザーを追加する
- 間違ったユーザーが編集しようとしたときのテスト
- beforeフィルターを使って編集/更新ページを保護する
- current_user?メソッド
- 最終的なcorrect_userの実装
- フレンドリーフォワーディングのテスト
- フレンドリーフォワーディングの実装
- ログインユーザー用beforeフィルターにstore_locationを追加する
- フレンドリーフォワーディングを備えたcreateアクション
- すべてのユーザーを表示する
- indexアクションのリダイレクトをテストする
- indexアクションにはログインを要求する
- ユーザーのindexアクション
- ユーザーのindexビュー
- gravatar_forヘルパーにオプション引数を追加する
- ユーザーのindexページ用のCSS
- ユーザー一覧ページへのリンクを更新する
- GemfileにFaker gemを追加する
- データベース上にサンプルユーザーを生成するRailsタスク
- Gemfileにwill_paginateを追加する
- indexページでpaginationを使う
- indexアクションでUsersをページネートする
- fixtureにさらに30人のユーザーを追加する
- ページネーションを含めたUsersIndexのテスト
- indexビューに対する最初のリファクタリング
- 各ユーザーを表示するパーシャル
- indexページの完全なリファクタリング
- ユーザーの削除
- 最後
- ユーザーを更新する
ユーザーを更新する
編集フォーム
ユーザーのeditアクション
app/controllers/users_controller.rb
class UsersController < ApplicationController def show @user = User.find(params[:id]) end def new @user = User.new end def create @user = User.new(user_params) if @user.save log_in @user flash[:success] = "Welcome to the Sample App!" redirect_to @user else render 'new' end end def edit @user = User.find(params[:id]) end private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end end
ユーザーのeditビュー
app/views/users/edit.html.erb
<% provide(:title, "Edit user") %> <h1>Update your profile</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_for(@user) do |f| %> <%= render 'shared/error_messages' %> <%= f.label :name %> <%= f.text_field :name, class: 'form-control' %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation, "Confirmation" %> <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit "Save changes", class: "btn btn-primary" %> <% end %> <div class="gravatar_edit"> <%= gravatar_for @user %> <a href="http://gravatar.com/emails" target="_blank">change</a> </div> </div> </div>
レイアウトの “Settings” リンクを更新する
app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse"> <div class="container"> <%= link_to "sample app", root_path, id: "logo" %> <nav> <ul class="nav navbar-nav navbar-right"> <li><%= link_to "Home", root_path %></li> <li><%= link_to "Help", help_path %></li> <% if logged_in? %> <li><%= link_to "Users", '#' %></li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown"> Account <b class="caret"></b> </a> <ul class="dropdown-menu"> <li><%= link_to "Profile", current_user %></li> <li><%= link_to "Settings", edit_user_path(current_user) %></li> <li class="divider"></li> <li> <%= link_to "Log out", logout_path, method: :delete %> </li> </ul> </li> <% else %> <li><%= link_to "Log in", login_path %></li> <% end %> </ul> </nav> </div> </header>
newとeditフォーム用のパーシャル
app/views/users/_form.html.erb
<%= form_for(@user) do |f| %> <%= render 'shared/error_messages', object: @user %> <%= f.label :name %> <%= f.text_field :name, class: 'form-control' %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation %> <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit yield(:button_text), class: "btn btn-primary" %> <% end %>
signupページのパーシャル
app/views/users/new.html.erb
<% provide(:title, 'Sign up') %> <% provide(:button_text, 'Create my account') %> <h1>Sign up</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= render 'form' %> </div> </div>
editビューのパーシャル
app/views/users/edit.html.erb
<% provide(:title, 'Edit user') %> <% provide(:button_text, 'Save changes') %> <h1>Update your profile</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= render 'form' %> <div class="gravatar_edit"> <%= gravatar_for @user %> <a href="http://gravatar.com/emails" target="_blank">Change</a> </div> </div> </div>
ユーザーのupdateアクションの初期実装
app/controllers/users_controller.rb
class UsersController < ApplicationController def show @user = User.find(params[:id]) end def new @user = User.new end def create @user = User.new(user_params) if @user.save log_in @user flash[:success] = "Welcome to the Sample App!" redirect_to @user else render 'new' end end def edit @user = User.find(params[:id]) end def update @user = User.find(params[:id]) if @user.update_attributes(user_params) # 更新に成功した場合を扱う。 else render 'edit' end end private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end end
編集の失敗に対するテスト
test/integration/users_edit_test.rb
require 'test_helper' class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end test "unsuccessful edit" do get edit_user_path(@user) assert_template 'users/edit' patch user_path(@user), params: { user: { name: "", email: "foo@invalid", password: "foo", password_confirmation: "bar" } } assert_template 'users/edit' end end
編集の成功に対するテスト
test/integration/users_edit_test.rb
require 'test_helper' class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end . . . test "successful edit" do get edit_user_path(@user) assert_template 'users/edit' name = "Foo Bar" email = "foo@bar.com" patch user_path(@user), params: { user: { name: name, email: email, password: "", password_confirmation: "" } } assert_not flash.empty? assert_redirected_to @user @user.reload assert_equal name, @user.name assert_equal email, @user.email end end
ユーザーのupdateアクション
app/controllers/users_controller.rb
class UsersController < ApplicationController . . . def update @user = User.find(params[:id]) if @user.update_attributes(user_params) flash[:success] = "Profile updated" redirect_to @user else render 'edit' end end . . . end
パスワードが空のままでも更新できるようにする
app/models/user.rb
class User < ApplicationRecord attr_accessor :remember_token before_save { self.email = email.downcase } validates :name, presence: true, length: { maximum: 50 } VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false } has_secure_password validates :password, presence: true, length: { minimum: 6 }, allow_nil: true . . . end
認可
beforeフィルターにlogged_in_userを追加する
app/controllers/users_controller.rb
class UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] . . . private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end # beforeアクション # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? flash[:danger] = "Please log in." redirect_to login_url end end end
テストユーザーでログインする
test/integration/users_edit_test.rb
require 'test_helper' class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end test "unsuccessful edit" do log_in_as(@user) get edit_user_path(@user) . . . end test "successful edit" do log_in_as(@user) get edit_user_path(@user) . . . end end
editとupdateアクションの保護に対するテストする
test/controllers/users_controller_test.rb
require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end . . . test "should redirect edit when not logged in" do get edit_user_path(@user) assert_not flash.empty? assert_redirected_to login_url end test "should redirect update when not logged in" do patch user_path(@user), params: { user: { name: @user.name, email: @user.email } } assert_not flash.empty? assert_redirected_to login_url end end
fixtureファイルに2人目のユーザーを追加する
test/fixtures/users.yml
michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %>
間違ったユーザーが編集しようとしたときのテスト
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 edit when logged in as wrong user" do log_in_as(@other_user) get edit_user_path(@user) assert flash.empty? assert_redirected_to root_url end test "should redirect update when logged in as wrong user" do log_in_as(@other_user) patch user_path(@user), params: { user: { name: @user.name, email: @user.email } } assert flash.empty? assert_redirected_to root_url end end
beforeフィルターを使って編集/更新ページを保護する
app/controllers/users_controller.rb
class UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] before_action :correct_user, only: [:edit, :update] . . . def edit end def update if @user.update_attributes(user_params) flash[:success] = "Profile updated" redirect_to @user else render 'edit' end end . . . private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end # beforeアクション # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? flash[:danger] = "Please log in." redirect_to login_url end end # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless @user == current_user end end
current_user?メソッド
app/helpers/sessions_helper.rb
module SessionsHelper # 渡されたユーザーをログイン def log_in(user) session[:user_id] = user.id end # 永続セッションとしてユーザーを記憶する def remember(user) user.remember cookies.permanent.signed[:user_id] = user.id cookies.permanent[:remember_token] = user.remember_token end # 渡されたユーザーがログイン済みユーザーであればtrueを返す def current_user?(user) user == current_user end # 記憶トークン (cookie) に対応するユーザーを返す def current_user . . . end . . . end
最終的なcorrect_userの実装
app/controllers/users_controller.rb
class UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] before_action :correct_user, only: [:edit, :update] . . . def edit end def update if @user.update_attributes(user_params) flash[:success] = "Profile updated" redirect_to @user else render 'edit' end end . . . private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end # beforeアクション # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? flash[:danger] = "Please log in." redirect_to login_url end end # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless current_user?(@user) end end
フレンドリーフォワーディングのテスト
test/integration/users_edit_test.rb
require 'test_helper' class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end . . . test "successful edit with friendly forwarding" do get edit_user_path(@user) log_in_as(@user) assert_redirected_to edit_user_url(@user) name = "Foo Bar" email = "foo@bar.com" patch user_path(@user), params: { user: { name: name, email: email, password: "", password_confirmation: "" } } assert_not flash.empty? assert_redirected_to @user @user.reload assert_equal name, @user.name assert_equal email, @user.email end end
フレンドリーフォワーディングの実装
app/helpers/sessions_helper.rb
module SessionsHelper . . . # 記憶したURL (もしくはデフォルト値) にリダイレクト def redirect_back_or(default) redirect_to(session[:forwarding_url] || default) session.delete(:forwarding_url) end # アクセスしようとしたURLを覚えておく def store_location session[:forwarding_url] = request.original_url if request.get? end end
ログインユーザー用beforeフィルターにstore_locationを追加する
app/controllers/users_controller.rb
class UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] before_action :correct_user, only: [:edit, :update] . . . def edit end . . . private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end # beforeアクション # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? store_location flash[:danger] = "Please log in." redirect_to login_url end end # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless current_user?(@user) end end
フレンドリーフォワーディングを備えたcreateアクション
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController . . . def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) log_in user params[:session][:remember_me] == '1' ? remember(user) : forget(user) redirect_back_or user else flash.now[:danger] = 'Invalid email/password combination' render 'new' end end . . . end
すべてのユーザーを表示する
indexアクションのリダイレクトをテストする
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 index when not logged in" do get users_path assert_redirected_to login_url end . . . end
indexアクションにはログインを要求する
app/controllers/users_controller.rb
class UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update] before_action :correct_user, only: [:edit, :update] def index end def show @user = User.find(params[:id]) end . . . end
ユーザーのindexアクション
app/controllers/users_controller.rb
class UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update] . . . def index @users = User.all end . . . end
ユーザーのindexビュー
app/views/users/index.html.erb
<% provide(:title, 'All users') %> <h1>All users</h1> <ul class="users"> <% @users.each do |user| %> <li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> </li> <% end %> </ul>
gravatar_forヘルパーにオプション引数を追加する
app/helpers/users_helper.rb
module UsersHelper # 渡されたユーザーのGravatar画像を返す def gravatar_for(user, options = { size: 80 }) gravatar_id = Digest::MD5::hexdigest(user.email.downcase) size = options[:size] gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}" image_tag(gravatar_url, alt: user.name, class: "gravatar") end end
ユーザーのindexページ用のCSS
app/assets/stylesheets/custom.scss
. . . /* Users index */ .users { list-style: none; margin: 0; li { overflow: auto; padding: 10px 0; border-bottom: 1px solid $gray-lighter; } }
ユーザー一覧ページへのリンクを更新する
app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse"> <div class="container"> <%= link_to "sample app", root_path, id: "logo" %> <nav> <ul class="nav navbar-nav navbar-right"> <li><%= link_to "Home", root_path %></li> <li><%= link_to "Help", help_path %></li> <% if logged_in? %> <li><%= link_to "Users", users_path %></li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown"> Account <b class="caret"></b> </a> <ul class="dropdown-menu"> <li><%= link_to "Profile", current_user %></li> <li><%= link_to "Settings", edit_user_path(current_user) %></li> <li class="divider"></li> <li> <%= link_to "Log out", logout_path, method: :delete %> </li> </ul> </li> <% else %> <li><%= link_to "Log in", login_path %></li> <% end %> </ul> </nav> </div> </header>
GemfileにFaker gemを追加する
source 'https://rubygems.org' gem 'rails', '5.1.4' gem 'bcrypt', '3.1.11' gem 'faker', '1.7.3' . . .
$ bundle install
データベース上にサンプルユーザーを生成するRailsタスク
db/seeds.rb
User.create!(name: "Example User", email: "example@railstutorial.org", password: "foobar", password_confirmation: "foobar") 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) end
データベースをリセットしてRailsタスクを実行
$ rails db:migrate:reset $ rails db:seed
Gemfileにwill_paginateを追加する
source 'https://rubygems.org' gem 'rails', '5.1.4' gem 'bcrypt', '3.1.11' gem 'faker', '1.7.3' gem 'will_paginate', '3.1.6' gem 'bootstrap-will_paginate', '1.0.0' . . .
Webサーバーを止めてからにした方がいいよ。
$ bundle install
Webサーバーを再起動
indexページでpaginationを使う
app/views/users/index.html.erb
<% provide(:title, 'All users') %> <h1>All users</h1> <%= will_paginate %> <ul class="users"> <% @users.each do |user| %> <li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> </li> <% end %> </ul> <%= will_paginate %>
indexアクションでUsersをページネートする
app/controllers/users_controller.rb
class UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update] . . . def index @users = User.paginate(page: params[:page]) end . . . end
fixtureにさらに30人のユーザーを追加する
test/fixtures/users.yml
michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %> lana: name: Lana Kane email: hands@example.gov password_digest: <%= User.digest('password') %> malory: name: Malory Archer email: boss@example.gov password_digest: <%= User.digest('password') %> <% 30.times do |n| %> user_<%= n %>: name: <%= "User #{n}" %> email: <%= "user-#{n}@example.com" %> password_digest: <%= User.digest('password') %> <% end %>
indexページに対するテストのために統合テストの生成をする。
$ rails generate integration_test users_index invoke test_unit create test/integration/users_index_test.rb
ページネーションを含めたUsersIndexのテスト
test/integration/users_index_test.rb
require 'test_helper' class UsersIndexTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end test "index including pagination" do log_in_as(@user) get users_path assert_template 'users/index' assert_select 'div.pagination' User.paginate(page: 1).each do |user| assert_select 'a[href=?]', user_path(user), text: user.name end end end
indexビューに対する最初のリファクタリング
app/views/users/index.html.erb
<% provide(:title, 'All users') %> <h1>All users</h1> <%= will_paginate %> <ul class="users"> <% @users.each do |user| %> <%= render user %> <% end %> </ul> <%= will_paginate %>
各ユーザーを表示するパーシャル
app/views/users/_user.html.erb
<li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> </li>
indexページの完全なリファクタリング
app/views/users/index.html.erb
<% provide(:title, 'All users') %> <h1>All users</h1> <%= will_paginate %> <ul class="users"> <%= render @users %> </ul> <%= will_paginate %>
ユーザーの削除
admin属性の追加
$ rails generate migration add_admin_to_users admin:boolean
boolean型のadmin属性をUserに追加するマイグレーション
db/migrate/[timestamp]_add_admin_to_users.rb
class AddAdminToUsers < ActiveRecord::Migration[5.0] def change add_column :users, :admin, :boolean, default: false end end
マイグレーション(サーバー止めようね)
$ rails db:migrate
サンプルデータ生成タスクに管理者を1人追加する
db/seeds.rb
User.create!(name: "Example User", email: "example@railstutorial.org", password: "foobar", password_confirmation: "foobar", admin: true) 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) end
データベースをリセットして、サンプルデータを再度生成する。
$ rails db:migrate:reset $ rails db:seed
ユーザー削除用リンクの実装 (管理者にのみ表示される)
app/views/users/_user.html.erb
<li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> <% if current_user.admin? && !current_user?(user) %> | <%= link_to "delete", user, method: :delete, data: { confirm: "You sure?" } %> <% end %> </li>
実際に動作するdestroyアクションを追加する
app/controllers/users_controller.rb
class UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update, :destroy] before_action :correct_user, only: [:edit, :update] . . . def destroy User.find(params[:id]).destroy flash[:success] = "User deleted" redirect_to users_url end private . . . end
beforeフィルターでdestroyアクションを管理者だけに限定する
app/controllers/users_controller.rb
class UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update, :destroy] before_action :correct_user, only: [:edit, :update] before_action :admin_user, only: :destroy . . . private . . . # 管理者かどうか確認 def admin_user redirect_to(root_url) unless current_user.admin? end
fixture内の最初のユーザーを管理者にする
test/fixtures/users.yml
michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> admin: true archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %> lana: name: Lana Kane email: hands@example.gov password_digest: <%= User.digest('password') %> malory: name: Malory Archer email: boss@example.gov password_digest: <%= User.digest('password') %> <% 30.times do |n| %> user_<%= n %>: name: <%= "User #{n}" %> email: <%= "user-#{n}@example.com" %> password_digest: <%= User.digest('password') %> <% end %>
管理者権限の制御をアクションレベルでテストする
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 destroy when not logged in" do assert_no_difference 'User.count' do delete user_path(@user) end assert_redirected_to login_url end test "should redirect destroy when logged in as a non-admin" do log_in_as(@other_user) assert_no_difference 'User.count' do delete user_path(@user) end assert_redirected_to root_url end end
削除リンクとユーザー削除に対する統合テスト
test/integration/users_index_test.rb
require 'test_helper' class UsersIndexTest < ActionDispatch::IntegrationTest def setup @admin = users(:michael) @non_admin = users(:archer) end test "index as admin including pagination and delete links" do log_in_as(@admin) get users_path assert_template 'users/index' assert_select 'div.pagination' first_page_of_users = User.paginate(page: 1) first_page_of_users.each do |user| assert_select 'a[href=?]', user_path(user), text: user.name unless user == @admin assert_select 'a[href=?]', user_path(user), text: 'delete' end end assert_difference 'User.count', -1 do delete user_path(@non_admin) end end test "index as non-admin" do log_in_as(@non_admin) get users_path assert_select 'a', text: 'delete', count: 0 end end
assert_select 'a[href=?]', user_path(user), text: 'delete'
ここがREDなんだよね。なんでなんだろう?ここ以外はGREENなんだけど。
最後
最後の儀式忘れずに
$ git add -A $ git commit -m "Finish user edit, update, index, and destroy actions" $ git checkout master $ git merge updating-users $ git push
本番展開とサンプルデータを作成する。
$ rails test $ git push heroku $ heroku pg:reset DATABASE
ここで止まります。 何のメッセージ!?って感じになります。
▸ WARNING: Destructive action ▸ postgresql-opaque-91684 will lose all of its data ▸ ▸ To proceed, type アプリケーション名 or re-run ▸ this command with --confirm アプリケーション名
ってなってるので
アプリケーション名
を入力してエンターして次になります。
$ heroku run rails db:migrate $ heroku run rails db:seed $ heroku restart
heroku再起動してるんですけど・・・
その後に確認したら
「We're sorry, but something went wrong.」
どうしたらいいのでしょうかぁ〜!!!
最悪です。