###modelだけ単数形の理由
modelは型に当たるので、それが複数形というのはちょっとということらしい。
#edit updateアクションを実装しよう
###まずはeditアクションをコントローラに実装
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
###privateについて
Rubyはクラスメソッドのオーバーライドなどもでき、外側からメソッドを変更される恐れがある。
そのため、privateを宣言しそれ以下に定義したメソッドに関しては外側からアクセスできないようにしている。これで勝手にadminユーザーを作られたりなどが防げる。
###編集フォームのviewを生成する。
<% 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>
これはnewアクションのviewと同じなのに、なんでupdateアクションに飛ぶの??
これはnewの場合は全く新しいインスタンスなのに対して、editの場合は、すでにDBに保存されたもので中身もあるインスタンスだから。
この違いがform_forで生成されるhtmlに影響を与えているから。
$ rails console
>> User.new.new_record?
=> true
>> User.first.new_record?
=> false
これは上記のようにrailsでtrueの時はpostリクエストを、falseの時はpatchリクエストを送るように判断するから。
###updateアクションの実装
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
###もう一度update_attributesメソッドを確認しよう
update_attributesとは、Ruby on Railsのモデルに備わるメソッドで、
モデルオブジェクト.update_attributes(キー: 値, キー: 値 … )
のようにHashを引数に渡してデータベースのレコードを複数同時に更新することができるメソッドです。
データベースへの更新タイミングは、update_attributesメソッドの実行と同時で、validationも実行されます。
###編集失敗時のテストを書こう
$ rails generate integration_test users_edit
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
失敗時のコードは実装済みなのでテストは成功する。
###次は編集に成功した時のテストを書こう
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
今の時点では成功時の処理を書いていないので、テストは失敗する。
@user.reloadはユーザーの情報をDBの情報と同期するというもの。
###具体的にupdateアクションを実装する
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
flashとredirectを実装したので、テストが通りそうに思えるが、これでもまだ失敗してしまう。
それはテストコードで書いたパスワードが空のためだ。
パスワードには、空ではなく、さらに6文字以上というバリデーションを設定しているため、それに引っかかってしまったのである。
この状態だと、名前とemailを変えるだけなのに、passwordを毎回入力しなければいけなくなってしまう。
これを解決するにはallow_nil: trueを指定してあげると良い。
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
has_secure_passwordでは (追加したバリデーションとは別に) オブジェクト生成時に存在性を検証するようになっているため、空のパスワード (nil) が新規ユーザー登録時に有効になることはありません。(空のパスワードを入力すると存在性のバリデーションとhas_secure_passwordによるバリデーションがそれぞれ実行され、2つの同じエラーメッセージが表示されるというバグがありましたが (7.3.3)、これで解決できました。)
つまり、allow_nilを指定してもオブジェクト生成時にはhas_secure_passwordで存在性のバリデーションをしてくれるので安心である。
これでテストが通る。
#認可について
今の状態だと、url直打ちでログインしてないのにuser_pathに入れたりなどが起こってしまう。
そのため、適切なユーザーでないとそのページにアクセスできないようにしたい。
###beforeフィルターを使ってユーザーログインを要求する。
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
before_action :logged_in_user, only: [:edit, :update]
これは userコントローラのeditメソッドとupdateメソッドを呼び出す前にlogged_in_userメソッドを呼び出してねーって意味。
ここで、createアクションを直打ちでしたらエラーになるんじゃね?って思うでしょ?
でも、resourcesで /usersになっているため直打ちしてもindexアクション呼び出してーってなるから大丈夫。
いや、でもshowアクションなんかは直打ちでアクセスできちゃうぞ?
これどうするんだ?
###beforeフィルターをした時の注意点
before_フィルターをすると、今まで通っていたテストが失敗してしまう。
理由はテスト時にedit,updateアクションを呼び出すときにログインを要求するようになったからだ。
これを解決するには、テスト時のユーザーに事前にログインさせる必要がある。
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
これでテストは通るようになる。
ただ、今度はbefore_actionが万が一コメントアウトされたときにテストで検知しないといけなくなってしまった。
これを解決していこう。
これはuser_controller_testに書いていく。
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
このテストはログインしてない状態でedit,updateアクションを呼び出そうとすると、フラッシュメッセージが表示され、さらにログインページにリダイレクトされてますよね?ってテスト。
これを書いておくことで、万が一before_actionが消されるとテストが失敗するようになるので安心だよねーって話。
ただ、今の状態だと、ログインしてさえいれば他人の情報を変更できるという状態。
これを解決するにはどうすれば良いか?
このテストを書くにあたって、2人以上のサンプルデータがないとダメなので、fixtureにデータをたす
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') %>
次にコントローラの単体テストを書く
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_actionを追加しよう
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
ここで気をつけなければいけないのは、
before_actionには順序があるということだ。
上から順番に実行されるので、ログインしている、かつ、正しいユーザーか?という順序で実行させる。
また、def logged_in_userでcurrent_userがいることは確定しているので、
無駄な処理を書かずに、params[:id]から取得したユーザーとcurrent_userを比較すれば良い
これでテストは通る。
一応リファクタリングもしておこう
unless @user == current_user
この部分を綺麗にしたい。
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
ヘルパーメソッドを定義したので、
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
redirect_to(root_url) unless current_user?(@user)というように書き換えることができる。
###フレンドリーフォアーディング
親切なリダイレクトを作ろう、もともとアクセスしたかったページにリダイレクトしてあげるようにしようということ。
まずは統合テストを書いていこう。
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
このようにuser_editが成功するストーリーに追加してあげる。
このままだと、ログイン後userページに飛んでしまうため、テストは失敗する。
次は、
ユーザーがもともと行きたかったページを覚えていたらそのページにアクセスし、覚えていなかったら普通のユーザーページにアクセスできるようなヘルパーメソッドを定義する。
sessionを使うと便利。
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
request.original_urlについて見ていこう。
requestはもともとある特殊な変数である。
original_urlメソッドを渡してあげると、userがもともと行きたかったurlを参照することができる。
if request.get?について見ていこう。
本来ログインせずにpatchリクエストをしてupdateアクションを呼び出すのは意味がない。
なので、if request.get?でgetリクエストだった時だけ、session[:forwarding_url]に情報を格納するよーって話。
もともと行きたかったurlが発生するのは必ずログイン前のことなので、
store_locationメソッドをbefore_actionで指定したメソッドの中に記載する。
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
もしログインしてなかったら、store_locationメソッドが呼び出されて、もともと行きたかったurlが参照できる。
そして、redirect_back_orメソッドをsessionコントローラのcreateメソッドの中に入れる。
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
本当はここでテスト通るはずなんだが、まさかの失敗。
NameError: NameError: undefined local variable or method ` store_location' for #<UsersController:0x0000000006837bc8>
Did you mean? store_location
app/controllers/users_controller.rb:51:in `logged_in_user'
test/integration/users_edit_test.rb:22:in `block in <class:UsersEditTest>'
このようにエラーが出た。
で、これは全角スペースがcontrollerの方に含まれていたからなんだね。
全角スペースもrubyではメソッド名に入る。
なのでこれを取り除くとテストが通った。
#全てのユーザーを表示する
まずはログインしてないユーザーがindexアクションをリクエストしたときにログインページに飛ぶかをテストする。
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
この状態だとテストは失敗してしまう。
テストを通るようにするのは簡単。
users_controllerにindexアクションを定義して、before_actionにindexアクションを追加してあげればいいだけ。
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アクションに対応するviewを作っていこう。
<% 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メソッドに引数が2つ渡されている。
users_helperに定義したメソッドは引数が1つしか渡せない。
これを直す必要がある。
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
これでエラーはなくなるが、cssが整ってないので、
/* Users index */
.users {
list-style: none;
margin: 0;
li {
overflow: auto;
padding: 10px 0;
border-bottom: 1px solid $gray-lighter;
}
}
それが終わったらheaderにリンクを設定する。
###サンプルユーザーの作成
ページネーションを試したいが、30人手動で作成するのはかなり面倒。
なのでコンピュータに作ってもらおう。
まずはGemfileにFaker gemを追加します
source 'https://rubygems.org'
gem 'rails', '5.1.6'
gem 'bcrypt', '3.1.12'
gem 'faker', '1.7.3'
bundleをする
###データベース上にサンプルユーザーを生成するRailsタスク
次にサンプルユーザーの生成するためのコードを書く。
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
注目すべきはUser.create!
db/seedsはべきとうせいではないので、一度実行して、2度目を実行しようとすると、再度100人サンプルユーザーが作られてしまう。
ただ、メールアドレスはユニークネスのバリデーションを設定しているので、100回間違えることになる。
これを避けるために、!を追加して、1人目の時点でバリデーションに引っかかったら例外を出すようにしている。
###faker gemの機能を試してみよう
$ rails db:migrate:reset
このコマンドを実行すると、開発用のDBのデータをリセットすることができる。
そして、
$ rails db:seed
このコマンドを実行すると、サンプルユーザーが100人作られる。
###ページネーション
サンプルユーザーが100人できたので、ページネーション機能を作っていこう
source 'https://rubygems.org'
gem 'rails', '5.1.6'
gem 'bcrypt', '3.1.12'
gem 'faker', '1.7.3'
gem 'will_paginate', '3.1.6'
gem 'bootstrap-will_paginate', '1.0.0'
will_pagenateというgemをインストールする。
gem 'bootstrap-will_paginate', '1.0.0'はページネーション機能とbootstrapを関連つける
###pagenationを表示させる
pagenationを表示させるのは簡単で、
<% 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 %>
pagenationを表示させたいところで<%= will_paginate %>と書くだけで良い。
ここで少し注意点
<%= will_paginate %>は本来<%= will_paginate @users %>となる。
今いるコンテキストの扱っているリソースを推測してくれて、pagenateする機能なのである。
これだけでは、ダメらしい。
具体的には、indexアクション内のallをpaginateメソッドに置き換えます (リスト 10.46)。ここで:pageパラメーターにはparams[:page]が使われていますが、これはwill_paginateによって自動的に生成されます。
UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update]
.
.
.
def index
@users = User.paginate(page: params[:page])
end
.
.
.
end
実装はこれで終わり。
###pagenationテストを行う
pagenationのテストを行うにはテスト環境にユーザーが30人以上いないとダメ。
それを解決するには、
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 %>
というようにしてあげる。
統合テストのファイルを作成しよう
$ rails generate integration_test users_index
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
今回のテストでは、paginationクラスを持ったdivタグをチェックして、最初のページにユーザーがいることを確認します。
30人以上のユーザーがいるとpaginationクラスを持つようになるらしい。
###indexアクションのviewをリファクタリングしよう
<% provide(:title, 'All users') %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">
<% @users.each do |user| %>
<%= render user %>
<% end %>
</ul>
<%= will_paginate %>
ここでは、renderをパーシャル (ファイル名の文字列) に対してではなく、Userクラスのuser変数に対して実行している点に注目してください。この場合、Railsは自動的に_user.html.erbという名前のパーシャルを探しにいくので、このパーシャルを作成する必要があります
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
これはさらにリファクタリングができる。
<% provide(:title, 'All users') %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">
<%= render @users %>
</ul>
<%= will_paginate %>
<%= render @users %>をすると、railsが自動的に
<% @users.each do |user| %>
<%= render user %>
<% end %>
のコードを展開してくれる。
なので、これを分かっていれば、
パーシャルを先に用意して、あとはrender @usersとすれば簡単にユーザーの情報を並べて表示することができる。
この状態でテストも通る。
このリファクタリングはかなりショートカットできるので覚えるように!!!
#ユーザーを削除する
管理権限を持っていればユーザーを削除できるようにする。
まずは、
特権を持つ管理ユーザーを識別するために、論理値をとるadmin属性をUserモデルに追加します。この後で説明しますが、こうすると自動的にadmin?メソッド (論理値を返す) も使えるようになりますので、これを使って管理ユーザーの状態をテストできます。
$ rails generate migration add_admin_to_users admin:boolean
今回は作成されたマイグレーションファイルにデフォルト値を設定する
class AddAdminToUsers < ActiveRecord::Migration[5.0]
def change
add_column :users, :admin, :boolean, default: false
end
end
booleanなのでtrueかfalseかしか入らないと思いがちだが、どうやらnilも入るらしい。
なので、あらかじめdefaultオプションでfalseを指定しておく。
adminユーザーを作ってみよう
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
###destroyアクションを実装しよう
まずはadminユーザーだけリンクが見えるようにしよう
<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>
userはuser_path(user)の略。
destroyアクションを作る
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
これだけではダメ、
ログインしていて、かつadminユーザーじゃないとだめ
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
end
###ユーザーの削除のテスト
まずはテスト用のサンプルユーザの中にadminユーザーを作る。
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
admin: true
テストコードを書く。
以下は、adminユーザーだけどログインしてない、そして、ログインしてるけどadminユーザーじゃない場合はどちらもdestroyアクション使えないよねーってテスト
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
統合テストも書いてみる。
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
unless user == @admin
assert_select 'a[href=?]', user_path(user), text: 'delete'
end
これは、@adminとuserが異なる場合、deleteのリンクが見えるよね?というテスト
逆に@admin = userの場合、deleteリンク見えませんよねーってこと
assert_difference 'User.count', -1 do
delete user_path(@non_admin)
end
また、do endでUserの数が1少なくなることを表す時は-1を指定していることにも注目。
###herokuのDBを初期化する
$ heroku pg:reset DATABASE
$ heroku run rails db:migrate
このコマンドって、pushしてからじゃないと、マイグレーションファイルがそもそもないので意味がないってことなんじゃない?
桜を作る
$ heroku run rails db:seed
$ heroku restart