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 1 year has passed since last update.

Ruby on Rails チュートリアル第10章をやってみて

Posted at

#ユーザーの更新・表示・削除
■第10章
editupdateindexdestroyアクションを加え、RESTアクションを完成させる。まず、ユーザーが自分のプロフィールを自分で更新できるようにできる。

##10.1 ユーザーを更新する
ユーザー情報を編集するパターンは、新規ユーザーの作成と似てるらしい。

###10.1.1 編集フォーム
Usersコントローラにeditアクションを追加して、それに対応するeditビューを実装する。

users_controller.rb
  def edit
    @user = User.find(params[:id])
  end

editビューも作成。

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>

リンク先のサイトがHTMLドキュメントのwindowオブジェクトを扱えてしまうため、target="_blank"で新しいページを開くときはセキュリティ上の問題がある。

###10.1.2 編集の失敗
編集に失敗した場合について扱っていく。以下のコードを追加。

users_controller.rb
def update
    @user = User.find(params[:id])
    if @user.update_attributes(user_params)
      # 更新に成功した場合を扱う。
    else
      render 'edit'
    end
  end

###10.1.3 編集失敗時のテスト
統合テスト生成から始める。

$ rails generate integration_test users_edit

中身も追記する。無事テストGREENに。

###10.1.4 TDDで編集を成功させる
今度は編集フォームが動作するようにする。

編集の成功に対するテストを追記。

users_edit_test.rb
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

コントローラのupdateアクションを弄って、パスワードが空のままでも更新できるようにする。空だったときの例外処理を加えるallow_nil: trueを追記。

user.rb
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true

これでテストがGREEN。

##10.2 認可
ログインすると誰でも他のユーザー情報を編集できる状態なので、他の人のユーザー編集ページにアクセスしようとしたらルートURLにリダイレクトするようにする。

###10.2.1 ユーザーにログインを要求する
ログイン画面に転送するために、beforeフィルターを使う。

beforeフィルターにlogged_in_userを追加。

users_controller.rb
before_action :logged_in_user, only: [:edit, :update]
  .
  .
  .
   private

# ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end

このままじゃテストRED。原因は、editアクションやupdateアクションでログインを要求するようになったためなので、ログインしておくようにする。

users_edit_test.rb
  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

beforeフィルターの実装が終わってないので追加してく。

editupdateアクションの保護に対するテストする

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

今の状態だとまだログインさえしてしまえば他の人のページを編集できてしまうので、次以降でに進めていく。

###10.2.2 正しいユーザーを要求する
ユーザーが自分の情報だけを編集できるようにする。

ユーザーの情報が互いに編集できないことを確認するために、2人目のユーザーを追加。

users.yml
archer:
  name: Sterling Archer
  email: duchess@example.gov
  password_digest: <%= User.digest('password') %>

間違ったユーザーが編集しようとしたときのテスト。

users_controller_test.rb
def setup
    @user = users(:michael)
    @other_user = users(:archer)
  end

current_user?メソッドも追加。

論理値を返すメソッドにする。

sessions_helper.rb
# 渡されたユーザーがログイン済みユーザーであればtrueを返す
  def current_user?(user)
    user == current_user
  end

beforeフィルターを使って編集/更新ページを保護する。

users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  before_action :correct_user,   only: [:edit, :update]
  .
  .
  .

# 正しいユーザーかどうか確認
    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_url) unless current_user?(@user)
    end

テストも無事GREEN。

###10.2.3 フレンドリーフォワーディング
アクセスしたページ→ログイン→アクセスしたページとなるにする。

統合テストの書き換え。

users_edit_test.rb
test "successful edit with friendly forwarding" do
    get edit_user_path(@user)
    log_in_as(@user)
    assert_redirected_to edit_user_url(@user)

失敗するテストが書けた。
希望のページに転送するには、リクエスト時点のページをどこかに保存しておき、その場所にリダイレクトさせる必要がある。store_locationredirect_back_orの2つのメソッドでこの動作を実現させる。

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

GETリクエストが送られたときだけ格納するようにしておく。

users_controller.rb
# ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        store_location
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end

フォワーディング自体を実装するには、redirect_back_orメソッドを使う。

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

テストも無事GREEN。

##10.3 すべてのユーザーを表示する
indexアクションを追加していく。

###10.3.1 ユーザーの一覧ページ
indexページを不正なアクセスから守るために、まずはindexアクションが正しくリダイレクトするか検証するテストを書いていく。

以下を追記。

users_controller_test.rb
  test "should redirect index when not logged in" do
    get users_path
    assert_redirected_to login_url
  end

次にbeforeフィルターのlogged_in_userindexアクションを追加して、このアクションを保護する。

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

User.allを使ってデータベース上の全ユーザーを取得し、ビューで使えるインスタンス変数@usersに代入させる。一気に読み込むと遅いが、それはあとで修正。

users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update]
  .
  .
  .
  def index
    @users = User.all
  end
  .
  .
  .
end

indexビューの作成。;

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ヘルパーにオプション引数を追加。

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もちょっといじくって、そして、ユーザー一覧ページへのリンクを更新するusers_pathを使う。以下を追加。

_header.html.erb
<li><%= link_to "Users", users_path %></li>

テストして問題ないことを確認。

###10.3.2 サンプルのユーザー
GemfileにFaker gemを追加。これでダミーのユーザーがたくさん作れるらしい。

100人くらい作るコード。スクリーンショット 2022-02-18 18.07.11.png

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

その後データベースをいじったら、無事ダミーユーザーがたくさん出てきました。
スクリーンショット 2022-02-18 18.07.11.png

###10.3.3 ページネーション
1つのページに大量のユーザーが表示されているので、人数制限みたいなのをする。これを解決するのがページネーション。

Gemfileに新たなgemを追加。

indexページでpaginationを使う。

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をページネートする。

users_controller.rb
  def index
    @users = User.paginate(page: params[:page])
  end

###10.3.4 ユーザー一覧のテスト
ページネーションに対する簡単なテストも書いておく。

fixtureにユーザー追加。

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

最後にpaginationクラスを持ったdivタグチェックをして、無事テスト完了。

###10.3.5 パーシャルのリファクタリング
ユーザーのlirender呼び出しに置き換える。

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

各ユーザーを表示するパーシャル。

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

今度はrenderを@users変数に対して直接実行する。

index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">
  <%= render @users %>
</ul>

<%= will_paginate %>

テストもGREEN。

##10.4 ユーザーを削除する
ユーザーを削除するためのリンクの作成。管理者権限を持つユーザーしか削除できないようにする。

###10.4.1 管理ユーザー
論理値をとるadmin属性をUserモデルに追加する。型はbooleanに。

$ rails generate migration add_admin_to_users admin:boolean

デフォルトでは管理者になれないということを示すために、default: falseという引数をadd_columnに追加。
boolean型のadmin属性をUserに追加するマイグレーション

[timestamp]_add_admin_to_users.rb
class AddAdminToUsers < ActiveRecord::Migration[5.1]
  def change
    add_column :users, :admin, :boolean, default: false
  end
end

最初のユーザーだけ管理者にしとく。

seeds.rb
User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar",
             admin: true)

データベース周りをいじって終わり。

###10.4.2 destroyアクション
管理者のみ削除用のリンクを表示。

_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アクションを追加する。

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

サイトを正しく防衛するために、destroyアクションにもアクセス制御を行う。

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

###10.4.3 ユーザー削除のテスト
1人だけadmin:trueで管理者にする。

ログインしていないユーザーであれば、ログイン画面にリダイレクトされること、ログイン済みではあっても管理者でなければ、ホーム画面にリダイレクトされることの2つをチェックする。

テストの中身を書き換えたら完了。

##感想
やっぱりセキュリティ周りのことはとにかくテストが大事なんだなという印象を受けました。あとテストでエラーが出ても、自力で解決できる力がさらについた気がします。

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?