今日やったこと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