学習日記

Ruby on Rails勉強してます

今日やったこと10

第10章ユーザーの更新・表示・削除  続きから



フレンドリーフォワーディングを実装する。
ユーザーを希望のページに転送するようにさせるため、リクエスト時点のページをどこかに保存しておき、その場所にリダイレクトさせる必要がある。
Sessionsヘルパーでstore_locationとredirect_back_orの2つのメソッドを定義して使用する。

app/helpers/sessions_helper.rb

 module SessionsHelper
  .
  .
  .
  # 記憶したURL (もしくはデフォルト値) にリダイレクト
  def redirect_back_or(default)
    # リクエストされたURLが存在する場合はそこにリダイレクトし、ない場合は何らかのデフォルトのURLにリダイレクト
    redirect_to(session[:forwarding_url] || default)
    # 転送用のURLを削除
    session.delete(:forwarding_url)
  end

  # アクセスしようとしたURLを覚えておく
  def store_location
    # GETリクエストが送られたときだけrequest.original_urlでリクエスト先を取得しsessionに保存
    session[:forwarding_url] = request.original_url if request.get?
  end
end
app/controllers/users_controller.rb

def logged_in_user
    unless logged_in?
      store_location #ログインユーザー用beforeフィルターにstore_locationを追加
      flash[:danger] = "Please log in."
      redirect_to login_url
    end
end
app/controllers/sessions_controller.rb

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 #redirect_back_orを追加
  else
    flash.now[:danger] = 'Invalid email/password combination'
    render 'new'
  end
end


10.3 すべてのユーザーを表示する

indexアクションを追加し、すべてのユーザーを一覧表示する。
ユーザーのshowページについては、ログインしているかどうかに関わらずサイトを訪れたすべてのユーザーから見えるようにするが、ユーザーのindexページはログインしたユーザーにしか見せないようにして、未登録のユーザーがデフォルトで表示できるページを制限する。

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

test/controllers/users_controller_test.rb

 require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

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

  # indexアクションのリダイレクトをテスト
  test "should redirect index when not logged in" do
    get users_path
    assert_redirected_to login_url
  end
  .
  .
  .
end

次に、beforeフィルターのlogged_in_userに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
  .
  .
  .
end

すべてのユーザーを表示するために、全ユーザーが格納された変数を作成し、順々に表示するindexビューを実装する。

def index
  @users = User.all
end
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>

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>


演習 10.3.1.1

test/integration/site_layout_test.rb

require 'test_helper'

class SiteLayoutTest < ActionDispatch::IntegrationTest
  
  def setup
    @user = users(:michael)
  end
  ・
  ・
  ・
  # ログイン時のレイアウトリンクに対するテスト
  test "layout links when logged in" do
    log_in_as(@user)
    get root_path
    assert_template 'static_pages/home'
    assert_select "a[href=?]", users_path
    assert_select "a[href=?]", user_path(@user)
    assert_select "a[href=?]", edit_user_path(@user)    
    assert_select "a[href=?]", logout_path
  end
  
end


サンプルのユーザーを追加する。
まず、GemfileにFaker gem(実際にいそうなユーザー名を作成するgem)を追加する。

gem 'faker',          '1.7.3'

bundle installを実行

$ bundle install

サンプルユーザーを生成するRubyスクリプト (Railsタスクとも呼ぶ) を追加ためにRailsではdb/seeds.rbというファイルを標準として使う。

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


ページネーションを使い1つのページに表示するユーザーの数を制限する。
Railsには豊富なページネーションメソッドがあり、その中のwill_paginateメソッドを使う。
Bootstrapのページネーションスタイルも使いたいので、
will_paginate gem とbootstrap-will_paginate gemをGemfileに追加する。

gem 'will_paginate',           '3.1.6'
gem 'bootstrap-will_paginate', '1.0.0'

bundle installを実行

$ bundle install

ページネーションを動作させるため、ユーザーのページネーションを行うようにRailsに指示するwill_paginateメソッドををindexビューに追加する。

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

will_paginateではpaginateメソッドを使った結果が必要なので
indexアクション内のallをpaginateメソッドに置き換える。
ここで:pageパラメーターにはparams[:page]が使われているが、これはwill_paginateによって自動的に生成されたものである。

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



ページネーションに対するテストを書いていく。
テスト用のデータベースに31人以上のユーザーがいる必要があるので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') %>

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
test/integration/users_index_test.rb

 require 'test_helper'

class UsersIndexTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  
  # ページネーションを含めたUsersIndexのテスト
  test "index including pagination" do
    log_in_as(@user) #ログイン
    get users_path #indexページにアクセス
    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">
  <%= render @users %>
</ul>

<%= will_paginate %>
app/views/users/_user.html.erb

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




10.4 ユーザーを削除する

削除を実行できる権限を持つ管理 (admin) ユーザーを識別するために、論理値をとるadmin属性をUserモデルに追加する。

属性の型をbooleanと指定しマイグレーションを実行する。

$ rails generate migration add_admin_to_users admin:boolean
$ rails db:migrate

admin属性が追加されて値はデフォルトでfalseとなり、疑問符の付いたadmin?メソッドも利用できるようになる。

>> user.admin?
=> false

最初のユーザーだけをデフォルトで管理者にするようサンプルデータを更新し、
データベースをリセットして、サンプルデータを再度生成する。

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

演習 10.4.1.1

test/controllers/users_controller_test.rb
 require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user       = users(:michael)
    @other_user = users(:archer)
  end
  .
  .
  .
  
  # admin属性の変更が禁止されていることをテスト
  test "should not allow the admin attribute to be edited via the web" do
    log_in_as(@other_user)
    assert_not @other_user.admin?
    patch user_path(@other_user), params: {
                                    user: { password:              @other_user,
                                            password_confirmation: @other_user,
                                            admin: true } }
    assert_not @other_user.reload.admin?
  end
  .
  .
  .
end

ユーザーindexページの各ユーザーに削除用のリンクを追加し、続いて管理ユーザーへのアクセスを制限する。
これによって、現在のユーザーが管理者のときに限り [delete] リンクが表示されるようになる。

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アクションを追加する。
このアクションでは、該当するユーザーを見つけてActive Recordのdestroyメソッドを使って削除し、最後にユーザーindexに移動するようにする。
ユーザーを削除するためにはログインしていなくてはならないので、destroyアクションもlogged_in_userフィルターに追加する。

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

サイトを正しく防衛するには、destroyアクションにもアクセス制御を行う必要があるので、
今回はbeforeフィルター(admin_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]
  before_action :admin_user,     only: :destroy
  .
  .
  .
  private
    .
    .
    .
    # 管理者かどうか確認
    def admin_user
      redirect_to(root_url) unless current_user.admin?
    end
end

ユーザー削除のテストを書く。
まずはユーザー用fixtureファイルを修正し、今いるサンプルユーザーの一人を管理者にする。

test/fixtures/users.yml

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

管理者権限の制御をアクションレベルでテストする

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



すべての変更をmasterブランチにマージ

$ git add -A
$ git commit -m "Finish user edit, update, index, and destroy actions"
$ git checkout master
$ git merge updating-users
$ git push