55,56日目
今日の学習
Ruby on Rails
Railsチュートリアル 第10章
昨日はフレンドリーフォワーディングまでを学習した。
今日は引き続きここから学習していく。
フレンドリーフォワーディング
昨日書いたメソッドの部分を実現させる。
まず、記憶したURLにリダイレクトするメソッドredirect_back_or
と、アクセスしようとしたURLを記憶するメソッドstore_location
を作成する。
app/helpers/sessions_helper.rb
# アクセスしようとしたURLを記憶 def store_location session[:forwarding_url] = request.original_url if request.get? end # 記憶したURL(もしくはデフォルト値)にリダイレクト def redirect_back_or(default) redirect_to(session[:forwarding_url] || default) session.delete(:forwarding_url) end
転送先のURLを保存するために、session
変数を使用している。
request.original_url
でリクエスト先が取得できる。
store_location
メソッドで、リクエストが送られたURLをsession
変数の:forwarding_url
キーに格納している。
if request.get?
をつけることで、GET
リクエストが送られてきたとき限定にしてある。
ここで格納したものをredirect_back_or
メソッドで利用し、リダイレクトするようにしてある。
最後はsession.delete(:forwarding_url)
で、転送用URLを削除する。
こうしておかないと、次回ログインしたときに保存されたページに転送されてしまい、ブラウザを閉じるまでこれが繰り返されてしまう。転送は一回だけでいいはずなので、ここで削除する。
ここで作成したstore_location
メソッドは、users_controller.rb
のログイン済みユーザーかどうか確認するbeforeアクションlogged_in_user
に加える。
redirect_back_or
メソッドは、user
を引数にしてsessions_controller.rb
のcreate
アクションに加える。
Railsテストのデフォルトリクエストホスト名
演習で、フレンドリーフォワーディングがしっかり機能しているかどうか、初回のみの転送になっているかのテストを行ったが、エラーが起きる。
--- expected +++ actual @@ -1 +1 @@ -"http://www.example.com/users/762146111/edit" +"/users/762146111/edit" test/integration/users_edit_test.rb:22:in `block in <class:UsersEditTest>'
上のURLが期待されるURLで、下が実際のURL。http://www.example.com
という部分がないよと言われている。
第10章Railsチュートリアル演習問題と解答まとめ - エンジニアになりたい肉体労働者
http://www.example.com
が、Railsテストでのリクエストホスト名のデフォルトのようだ。
session[:forwarding_url]
に、アクセスした`edit_user_path(@user)が格納してあるか調べるためのものに、上のデフォルトURLを繋げればテストが通った。
session[:forwarding_url], "http://www.example.com" + edit_user_path(@user)
すべてのユーザーを表示する
index
アクションを使用して、すべてのユーザーを一覧表示する。
ここでは、データベースにサンプルデータを追加する方法や、ページネーション
のやり方も学ぶ。
ユーザー一覧ページ
ユーザーの個人ページshow
は、ログインの有無に関わらず閲覧できる状態だが、一覧ページindex
はログインしたユーザーにしか見せない様にする。
そのため、logged_in_user
にindex
アクションを追加して、ログインしている状態限定にする。
index
アクションには、ひとまず@users = User.all
と記述しておく。
ユーザー一覧のビューは、ユーザーごとにli
タグで囲み、each
メソッドを使って作成する。
各ユーザーのGravatarと名前を表示する。
<ul> <% @users.each do |user| %> <li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> </li> <% end %> </ul>
以前作成したgravatar_for
アクションを利用して、画像のサイズをオプション引数で指定している。
サンプルユーザーを増やす
現在、サンプルユーザーは1名だけなので、Faker
gemを利用してサンプルユーザーを増やす。
本来は開発環境以外では使わないgemだが、Railsチュートリアルでは本番環境でも適応させる予定らしく、全ての環境で使える状態で導入した。
db/seeds.rb
ファイルに、Faker
を利用したサンプルユーザーを生成するコードを書く。
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:seed
を行い、データを投入。
ページネーション
初期データと、先ほど作成したデータを合わせて100件のサンプルデータが存在する。
今のままでは、一つのページに大量のユーザーが表示されてしまうことになる。
そのため、ページネーション
を導入して、一つのページに表示させるユーザーの数を制限していく。Railsチュートリアルでは、30人だけを表示させるようにしていくようだ。
今回利用するページネーションメソッドはwill_paginate
。
以前ページネーションについて学習したときは、kaminari
というgemを利用したが、今回はwill_paginate
とbootstrap-will_paginate
gemを利用する。
ページネーションを動作させるために、二つの手順が必要になる
- indexビューにページネーションを行うためのコードを書く
- indexアクションの
User.all
を置き換える
まずは1だが、先ほどのgemで利用できるようになったwill_paginate
メソッドを追加する。
<%= 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ビューのコードを、will_paginate
メソッドで囲む。こうすることで、@users
オブジェクトを自動的に見つけ出して、ページネーションリンクを作成してくれる。
次の2で、ビューファイルのwill_paginate
メソッドがしっかり機能するようにする。
gem追加で利用できるようになったpaginate
メソッドでは、キーが:page
で値がページ番号のハッシュを引数に取る。
User.paginate(page: 1) #=> デフォルトではデータベースから30データ分を取り出す
このメソッドを使って、index
アクションのUser.all
のall
を置き換える。
def index @users = User.paginate(page: params[:page]) end
パラメータにparams[:page]
を利用しているが、will_paginate
によって自動的に生成される。
これで、will_paginate
がページネーションを自動で生成してくれる。
ちゃんと他のページにも飛べるようになっている。
will_paginate
は上下に配置したので、下の方も確認してみると...。
ええ...。
サンプルユーザーの情報が列挙されてしまっている...。
index.html.erb
に書き損じがあるのか確認したが、特に問題はない。
結論から言うと、確認する際に、index
ビューのリファクタリングを行ったら表示されなくなった。釈然としない。
<ul class="users"> <%= @users.each do |user| %> # このliタグの部分をrenderに置き換える <li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> </li> <% end %> </ul>
↓
<ul class="users"> <%= @users.each do |user| %> <%= render user %> <% end %> </ul>
each
文の中のuser
変数に対して実行している。Railsは自動的に_user.html.erb
という名前のファイルを探してくれるため、同じ名前のファイルを作成して、先ほどの部分を貼り付ける。
<li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> </li>
さらにindex
ページを簡略化することができる。renderを@users
変数に対して直接実行する。
<ul class="users"> <%= render @users %> </ul>
Railsは、@users
をUserオブジェクトのリストだと推測し、自動的にUserのコレクションを列挙し、それぞれを_user.html.erb
パーシャルで出力する。賢すぎる。
ユーザーの削除
残るアクションはdestroy
のみ。削除を行うリンクや、destroy
アクションの実装をする。
その前に、削除を実行できる権限を持つ管理(admin)
ユーザーのクラスを作成する。
承認(authorization)において、このような特権のセットをrole
と呼ぶ。
管理ユーザー
ユーザーが特権を持つ管理ユーザーなのかどうかを識別するために、論理値を取るadmin
属性をUserモデルに追加する。
属性の型をboolean
とすることで、自動的に論理値を返すadmin?
メソッドが使えるようになり、これを使って管理ユーザーか否かをチェックできる。
rails g migration add_admin_to_users admin:boolean
上を実行し、作成されたマイグレーションファイルにあるadd_column
に、default: false
という引数を追加する。
def change add_column :users, :admin, :boolean, default: false end
もしもfalse
を与えない場合は、admin
の値はデフォルトではnil
となる。これはfalse
と同じ意味になるので、引数を与える意味はないが、明記することにより分かりやすくなる。
boolean
について調べてみると、null: false
の引数も追加して、NOT NULL制約も入れておくべきという意見を多数見かけた。
コンソールで確認してみる。
userの情報にadmin: false
が加わっており、admin?
メソッドにもちゃんと反応してくれる。
toggle!
メソッドは、インスタンスに保存されているbooleanの値を反転させて、データベースに保存する。
このメソッドを利用し、引数を:admin
にすることによって、admin
の値をfalse
からtrue
に変更している。
【Rails】toggleとtoggle!の使い方 - Qiita
Strong Parametersとadmin
Strong Parametersでは、編集してもよい安全な属性だけを更新できるように設定してある。今の状態だと、以下の四つのみが更新可能になっている。
- name
- password
- password_confirmation
もしもこの中にadmin
を加えてしまったらどうなるだろうか。
PATCH
リクエストで、admin: true
という情報を受け取ると、編集可能なadmin
はtrue
に更新されてしまい、非常に危険になる。そのため、絶対に加えないこと。
Railsチュートリアルでは、admin
属性のテストを演習で行った。
destroyアクション
ユーザー一覧のindex
ページで、各ユーザーの削除用リンクを表示する。
if文
を使って、ログイン中のユーザーが管理者のときのみdelete
リンクが表示されるようにする。
先ほど作成した、_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>
if文
で、current_user
が管理者で、かつ削除するuserがcurrent_userではないかどうかを見ている。管理者が自分自身を削除できないようにするためだ。
DELETE
リクエストを発行するリンクは、method: :delete
によって生成されている。
ビューファイルには削除リンクの用意ができたので、destroy
アクションを追加していく。また、このアクションもログインしている必要があるため、logged_in_user
フィルターに追加する。
def destroy User.find(params[:id]).destroy flash[:success] = "User deleted" redirect_to users_url end
今のままでは、DELETE
リクエストを送られてしまうとユーザーが削除されてしまう。
そのため、管理者だけがユーザーを削除できるようにする必要があり、destroy
アクションにアクセス制御を行わなければならない。以下のようにして、befroreアクションを設定する。
before_action :admin_user, only: :destroy private def admin_user redirect_to(root_url) unless current_user.admin? end
ユーザー削除のテスト
ユーザーを削除するといった重要な操作については、動作のテストをしっかり行うべきだと書かれている。そのため、テストで行ったことを書き記しておく。
まずはfixture
ファイルに追加されているユーザーの一人にadmin: true
と書くことで、管理者にする。
作成済みのtest/controllers/users_controller_test.rb
で行う。
destroyアクションを行った時に確認したいことは以下の二つ。
- ログインしていないユーザーであれば、ログイン画面にリダイレクトされる
- ログイン済みで、管理者でない場合はホーム画面にリダイレクトされる
# ログインしていない場合 test "should redirect destroy when not logged in" do # ユーザー数が変化しないかどうかを確認 assert_no_difference 'User.count' do # deleteリクエストを発行 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
次は、管理者でログインしている場合は削除リンクが表示されることを利用して、削除リンクが存在するかどうかを調べるテストも実施する。
また、実際にユーザーが削除できるかどうかもテストをする。
ページネーションを設定した時、ページネーションを含めたUserIndex
のテストを作成したので、その部分に追加する形でテストを書いていく。
test/integration/users_index_test.rb
def setup @admin = users(:adminにしたユーザー) @non_admin = users(:adminではないユーザー) end # ページネーションのテスト部分は省略 test "index as admin including delete links" do 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 # adminユーザーは選択しない unless user == @admin # deleteリンクがあるかを確認 assert_select 'a[href=?], user_path(user), text: 'delete' end end # deleteリクエストでユーザーの数が一つ減るかを確認 assert_difference 'User.count', -1 do delete user_path(@non_admin) end test "index as non-admin" do log_in_as(@non_admin) get users_path # deleteリンクがないことを確認 assert_select 'a', text: 'delete', count: 0 end
Heroku
Railsチュートリアルで作成したものはHerokuにデプロイしているが、サンプルデータを本番データとして作成することもできるようだ。
本番データベースをリセットするには、pg:reset
タスクを使う。
heroku pg:reset DATABASE
マイグレーションをしたい場合は以下のコマンド。
heroku run rails db:migrate heroku run rails db:seed
今日のやらかし
テストでcurrent_user
を指定してテスト失敗
レイアウトにあるリンクに対して統合テストを書くという演習があった。
このリンクが正しく機能しているかどうかテストをしたい。
<li><%= link_to "Profile", current_user %></li> <li><%= link_to "Setting", edit_user_path(current_user) %></li>
setupで@userには正しいユーザーを代入済み。
test do log_in_as(@user) get root_path assert_select "a[href=?]", current_user assert_select "a[href=?]", edit_user_path(current_user) end
このような書き方でいけるかと思ったが、エラーが出て失敗。
NameError: undefined local variable or method `current_user'
メソッドを使わずに書かなければならない。
test do log_in_as(@user) get root_path assert_select "a[href=?]", user_path(@user) assert_select "a[href=?]", edit_user_path(@user) end