0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

rails-tutorial第10章

Last updated at Posted at 2020-06-06

###modelだけ単数形の理由
modelは型に当たるので、それが複数形というのはちょっとということらしい。

#edit updateアクションを実装しよう

###まずは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

###privateについて

Rubyはクラスメソッドのオーバーライドなどもでき、外側からメソッドを変更される恐れがある。
そのため、privateを宣言しそれ以下に定義したメソッドに関しては外側からアクセスできないようにしている。これで勝手にadminユーザーを作られたりなどが防げる。

###編集フォームのviewを生成する。

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>

これは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アクションの実装

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

###もう一度update_attributesメソッドを確認しよう

update_attributesとは、Ruby on Railsのモデルに備わるメソッドで、

モデルオブジェクト.update_attributes(キー: 値, キー: 値 … )
のようにHashを引数に渡してデータベースのレコードを複数同時に更新することができるメソッドです。

データベースへの更新タイミングは、update_attributesメソッドの実行と同時で、validationも実行されます。

###編集失敗時のテストを書こう

$ rails generate integration_test users_edit

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

今の時点では成功時の処理を書いていないので、テストは失敗する。

@user.reloadはユーザーの情報をDBの情報と同期するというもの。

###具体的に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

flashとredirectを実装したので、テストが通りそうに思えるが、これでもまだ失敗してしまう。
それはテストコードで書いたパスワードが空のためだ。

パスワードには、空ではなく、さらに6文字以上というバリデーションを設定しているため、それに引っかかってしまったのである。

この状態だと、名前とemailを変えるだけなのに、passwordを毎回入力しなければいけなくなってしまう。

これを解決するにはallow_nil: trueを指定してあげると良い。

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

has_secure_passwordでは (追加したバリデーションとは別に) オブジェクト生成時に存在性を検証するようになっているため、空のパスワード (nil) が新規ユーザー登録時に有効になることはありません。(空のパスワードを入力すると存在性のバリデーションとhas_secure_passwordによるバリデーションがそれぞれ実行され、2つの同じエラーメッセージが表示されるというバグがありましたが (7.3.3)、これで解決できました。)

つまり、allow_nilを指定してもオブジェクト生成時にはhas_secure_passwordで存在性のバリデーションをしてくれるので安心である。

これでテストが通る。

#認可について

今の状態だと、url直打ちでログインしてないのにuser_pathに入れたりなどが起こってしまう。

そのため、適切なユーザーでないとそのページにアクセスできないようにしたい。

###beforeフィルターを使ってユーザーログインを要求する。

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

before_action :logged_in_user, only: [:edit, :update]

これは userコントローラのeditメソッドとupdateメソッドを呼び出す前にlogged_in_userメソッドを呼び出してねーって意味。

ここで、createアクションを直打ちでしたらエラーになるんじゃね?って思うでしょ?

でも、resourcesで /usersになっているため直打ちしてもindexアクション呼び出してーってなるから大丈夫。

いや、でもshowアクションなんかは直打ちでアクセスできちゃうぞ?
これどうするんだ?

###beforeフィルターをした時の注意点

before_フィルターをすると、今まで通っていたテストが失敗してしまう。
理由はテスト時にedit,updateアクションを呼び出すときにログインを要求するようになったからだ。

これを解決するには、テスト時のユーザーに事前にログインさせる必要がある。

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

これでテストは通るようになる。

ただ、今度はbefore_actionが万が一コメントアウトされたときにテストで検知しないといけなくなってしまった。

これを解決していこう。

これはuser_controller_testに書いていく。

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

このテストはログインしてない状態でedit,updateアクションを呼び出そうとすると、フラッシュメッセージが表示され、さらにログインページにリダイレクトされてますよね?ってテスト。

これを書いておくことで、万が一before_actionが消されるとテストが失敗するようになるので安心だよねーって話。

ただ、今の状態だと、ログインしてさえいれば他人の情報を変更できるという状態。
これを解決するにはどうすれば良いか?

このテストを書くにあたって、2人以上のサンプルデータがないとダメなので、fixtureにデータをたす

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_actionを追加しよう

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

ここで気をつけなければいけないのは、
before_actionには順序があるということだ。

上から順番に実行されるので、ログインしている、かつ、正しいユーザーか?という順序で実行させる。

また、def logged_in_userでcurrent_userがいることは確定しているので、
無駄な処理を書かずに、params[:id]から取得したユーザーとcurrent_userを比較すれば良い

これでテストは通る。

一応リファクタリングもしておこう

unless @user == 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

ヘルパーメソッドを定義したので、

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

redirect_to(root_url) unless current_user?(@user)というように書き換えることができる。

###フレンドリーフォアーディング

親切なリダイレクトを作ろう、もともとアクセスしたかったページにリダイレクトしてあげるようにしようということ。

まずは統合テストを書いていこう。

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

このようにuser_editが成功するストーリーに追加してあげる。
このままだと、ログイン後userページに飛んでしまうため、テストは失敗する。

次は、

ユーザーがもともと行きたかったページを覚えていたらそのページにアクセスし、覚えていなかったら普通のユーザーページにアクセスできるようなヘルパーメソッドを定義する。

sessionを使うと便利。

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

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で指定したメソッドの中に記載する。

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

もしログインしてなかったら、store_locationメソッドが呼び出されて、もともと行きたかったurlが参照できる。

そして、redirect_back_orメソッドをsessionコントローラの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

本当はここでテスト通るはずなんだが、まさかの失敗。

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アクションをリクエストしたときにログインページに飛ぶかをテストする。

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

この状態だとテストは失敗してしまう。
テストを通るようにするのは簡単。

users_controllerにindexアクションを定義して、before_actionに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アクションに対応するviewを作っていこう。

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メソッドに引数が2つ渡されている。
users_helperに定義したメソッドは引数が1つしか渡せない。

これを直す必要がある。

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

これでエラーはなくなるが、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;
  }
}

それが終わったら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タスク

次にサンプルユーザーの生成するためのコードを書く。

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

注目すべきは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を表示させるのは簡単で、

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

pagenationを表示させたいところで<%= will_paginate %>と書くだけで良い。

ここで少し注意点

<%= will_paginate %>は本来<%= will_paginate @users %>となる。

今いるコンテキストの扱っているリソースを推測してくれて、pagenateする機能なのである。

これだけでは、ダメらしい。

具体的には、indexアクション内のallをpaginateメソッドに置き換えます (リスト 10.46)。ここで:pageパラメーターにはparams[:page]が使われていますが、これはwill_paginateによって自動的に生成されます。

app/controllers/users_controller.rbclass
UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update]
  .
  .
  .
  def index
    @users = User.paginate(page: params[:page])
  end
  .
  .
  .
end

実装はこれで終わり。

###pagenationテストを行う

pagenationのテストを行うにはテスト環境にユーザーが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 %>

というようにしてあげる。

統合テストのファイルを作成しよう

$ rails generate integration_test users_index

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

今回のテストでは、paginationクラスを持ったdivタグをチェックして、最初のページにユーザーがいることを確認します。

30人以上のユーザーがいるとpaginationクラスを持つようになるらしい。

###indexアクションのviewをリファクタリングしよう

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

ここでは、renderをパーシャル (ファイル名の文字列) に対してではなく、Userクラスのuser変数に対して実行している点に注目してください。この場合、Railsは自動的に_user.html.erbという名前のパーシャルを探しにいくので、このパーシャルを作成する必要があります

app/views/users/_user.html.erb
<li>
  <%= gravatar_for user, size: 50 %>
  <%= link_to user.name, user %>
</li>

これはさらにリファクタリングができる。

app/views/users/index.html.erb
<% 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

今回は作成されたマイグレーションファイルにデフォルト値を設定する

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

booleanなのでtrueかfalseかしか入らないと思いがちだが、どうやらnilも入るらしい。
なので、あらかじめdefaultオプションでfalseを指定しておく。

adminユーザーを作ってみよう

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

###destroyアクションを実装しよう

まずはadminユーザーだけリンクが見えるようにしよう

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>

userはuser_path(user)の略。

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

これだけではダメ、
ログインしていて、かつadminユーザーじゃないとだめ

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
end

###ユーザーの削除のテスト

まずはテスト用のサンプルユーザの中にadminユーザーを作る。

test/fixtures/users.yml
michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>
  admin: true

テストコードを書く。
以下は、adminユーザーだけどログインしてない、そして、ログインしてるけどadminユーザーじゃない場合はどちらもdestroyアクション使えないよねーってテスト

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

0
0
0

Register as a new user and use Qiita more conveniently

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?