Ruby on Rails チュートリアルで30歳までに人生を変える(第10章)
こんにちは。opiyoです。
今回は、第10章をやっていきます。
第10章はユーザー登録以外の「表示」「編集」「削除」の方法です。
railsチュートリアル10章の学んだこと
- target="_blank"が使われていますが、これを使うとリンク先を新しいタブ (またはウィンドウ) で開くようになる
<a href="http://gravatar.com/emails" target="_blank">change</a>
- form_forの引数にインスタンス変数を使うと中身がある場合はそれをRailsが勝手に表示する
- :allow_nil → trueならば、nilの検証はスキップ。つまり空文字(' ')はキャッチする!
- beforeフィルターはコントローラ内のすべてのアクションに適用されるので、ここでは適切な:onlyオプション (ハッシュ) を渡すことで、:editと:updateアクションだけにこのフィルタが適用される
- フレンドリーフォワーディング ... リダイレクト先は、ユーザーが開こうとしていたページにしてあげること
will_paginate
とbootstrap-will_paginate gem
を使えばページネーションが作れる- will_paginateメソッドをviewに追加して、paginateメソッドでデータを取ってくるだけですと?
# view <%= will_paginate %> # controller def index @users = User.paginate(page: params[:page]) end
-統合テスト = integration_test - $ rails generate integration_test users_index - toggleメソッド使うと反転できる?
$ rails console --sandbox >> user = User.first >> user.admin? => false >> user.toggle!(:admin) => true >> user.admin? => true
railsチュートリアル10章の演習解説
10.1.1 演習 編集フォーム
10.1.1.1
<問題>先ほど触れたように、target="_blank"で新しいページを開くときには、セキュリティ上の小さな問題があります。それは、リンク先のサイトがHTMLドキュメントのwindowオブジェクトを扱えてしまう、という点です。具体的には、フィッシング (Phising) サイトのような、悪意のあるコンテンツを導入させられてしまう可能性があります。Gravatarのような著名なサイトではこのような事態は起こらないと思いますが、念のため、このセキュリティ上のリスクも排除しておきましょう。対処方法は、リンク用のaタグのrel (relationship) 属性に、"noopener"と設定するだけです。早速、リスト 10.2で使ったGravatarの編集ページへのリンクにこの設定をしてみましょう。
<回答>
# app/views/users/edit.html.erb <div class="gravatar_edit"> <%= gravatar_for @user %> <a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a> </div>
10.1.1.2
<問題>リスト 10.5のパーシャルを使って、new.html.erbビュー (リスト 10.6) とedit.html.erbビュー (リスト 10.7) をリファクタリングしてみましょう (コードの重複を取り除いてみましょう)。ヒント: 3.4.3で使ったprovideメソッドを使うと、重複を取り除けます3。(関連するリスト 7.27の演習課題を既に解いている場合、この演習課題をうまく解けない可能性があります。うまく解けない場合は、既存のコードのどこに差異があるのか考えながらこの課題に取り組んでみましょう。例えば筆者であれば、リスト 10.5のテクニックをリスト 10.6に適用してみたり、リスト 10.7のテクニックをリスト 10.5に適用してみたりするでしょう。)
<回答>
# app/views/users/_form.html.erb <%= form_for(@user) do |f| %> <%= render 'shared/error_messages', object: @user %> <%= f.label :name %> <%= f.text_field :name, class: 'form-control' %> <%= f.label :email %> <%= f.text_field :email, class: 'form-control' %> <%= f.label :passwore %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation, "Confirmation" %> <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit yield(:btn_text), class: "btn btn-primary" %> <% end %>
# app/views/users/new.html.erb <% provide(:title, 'Sign up') %> <% provide(:btn_text, 'Create my account') %> <h1><h1>Sign up</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= render 'form' %> </div> </div>
# app/views/users/edit.html.erb <% provide(:title, 'Edit user') %> <% provide(:btn_text, 'Save changes') %> <h1>Update your profile</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= render 'form' %> <div class="gravatar_edit"> <%= gravatar_for @user %> <a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a> </div> </div> </div>
10.1.2 演習 編集の失敗
10.1.2.1
<問題>編集フォームから有効でないユーザー名やメールアドレス、パスワードを使って送信した場合、編集に失敗することを確認してみましょう。
<回答>
The form contains 2 errors. Email is invalid Password is too short (minimum is 6 characters) ["Email is invalid", "Password is too short (minimum is 6 characters)"]
エラーメッセージが表示されることを確認。
10.1.3 演習 編集失敗時のテスト
10.1.3.1
<問題>リスト 10.9のテストに1行追加し、正しい数のエラーメッセージが表示されているかテストしてみてましょう。ヒント: 表 5.2で紹介したassert_selectを使ってalertクラスのdivタグを探しだし、「The form contains 4 errors.」というテキストを精査してみましょう。
<回答>
# test/integration/users_edit_test.rb test "unsuccessful edit" do log_in_as(@user) 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' assert_select "div", "The form contains 4 errors." # この行を追加した! end
各パラメータがvalidateに引っかかるので、4 errorsのメッセージが表示されることをチェック!
10.1.4 演習 TDDで編集を成功させる
10.1.4.1
<問題>実際に編集が成功するかどうか、有効な情報を送信して確かめてみましょう。
<回答> - 問題なく更新できる。 - パスワード欄が空白で更新できる。
10.1.4.2
<問題>もしGravatarと紐付いていない適当なメールアドレス (foobar@example.comなど) に変更した場合、プロフィール画像はどのように表示されるでしょうか? 実際に編集フォームからメールアドレスを変更して、確認してみてましょう。
<回答>
10.2.1 演習 ユーザーにログインを要求する
10.2.1.1
<問題>デフォルトのbeforeフィルターは、すべてのアクションに対して制限を加えます。今回のケースだと、ログインページやユーザー登録ページにも制限の範囲が及んでしまうはずです (結果としてテストも失敗するはずです)。リスト 10.15のonly:オプションをコメントアウトしてみて、テストスイートがそのエラーを検知できるかどうか (テストが失敗するかどうか) 確かめてみましょう。
<回答> 問題なし
10.2.2 演習 正しいユーザーを要求する
10.2.2.1
<問題>何故editアクションとupdateアクションを両方とも保護する必要があるのでしょうか? 考えてみてください。
<回答> 自身データが修正されてしまうことを保護するため。
10.2.2.2
<問題>上記のアクションのうち、どちらがブラウザで簡単にテストできるアクションでしょうか?
<回答>
editアクション。
10.2.3 演習 フレンドリーフォワーディング
10.2.3.1
<問題>フレンドリーフォワーディングで、最初に渡されたURLにのみ確実に転送されていることを確認するテストを作成してみましょう。続けて、ログインを行った後、転送先のURLはデフォルト (プロフィール画面) に戻る必要もありますので、これもテストで確認してみてください。ヒント: リスト 10.29のsession[:forwarding_url]が正しい値かどうかを確認するテストを追加してみましょう。
<回答>
# test/integration/users_edit_test.rb test "successful edit with friendly forwarding" do get edit_user_path(@user) assert session[:forwarding_url] # sessionに値が入っていることをチェック assert_equal edit_user_url(@user), session[forwarding_url] # 正しい値かチェックする log_in_as(@user) assert_redirected_to edit_user_url(@user) # 今まではログイン後はログインユーザーのプロフィール画面に遷移してたけど、記憶したURLに遷移するようになったので、ログイン前にアクセスしたユーザー編集画面に遷移してる。 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
10.2.3.2
<問題>7.1.3で紹介したdebuggerメソッドをSessionsコントローラのnewアクションに置いてみましょう。その後、ログアウトして /users/1/edit にアクセスしてみてください (デバッガーが途中で処理を止めるはずです)。ここでコンソールに移り、session[:forwarding_url]の値が正しいかどうか確認してみましょう。また、newアクションにアクセスしたときのrequest.get?の値も確認してみましょう (デバッガーを使っていると、ときどき予期せぬ箇所でターミナルが止まったり、おかしい挙動を見せたりします。熟練の開発者になった気になって (コラム 1.1)、落ち着いて対処してみましょう)。
<回答>
[1, 10] in /Users/taku/rails/railstutorial/railstutorial_10/app/controllers/sessions_controller.rb 1: class SessionsController < ApplicationController 2: def new 3: debugger => 4: end 5: 6: def create 7: user = User.find_by(email: params[:session][:email].downcase) 8: if user && user.authenticate(params[:session][:password]) 9: # ユーザーログイン後にユーザー情報のページにリダイレクトする 10: log_in user (byebug) session[:forwarding_url] "http://localhost:3000/users/1/edit" (byebug) request.get? true
10.3.1 演習 すべてのユーザーを表示する
10.3.1.1
<問題>レイアウトにあるすべてのリンクに対して統合テストを書いてみましょう。ログイン済みユーザーとそうでないユーザーのそれぞれに対して、正しい振る舞いを考えてください。ヒント: log_in_asヘルパーを使ってリスト 5.32にテストを追加してみましょう。
<回答>
# test/integration/site_layout_test.rb require 'test_helper' class SiteLayoutTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end test "layout links" do get root_path assert_template 'static_pages/home' assert_select "a[href=?]", root_path, count: 2 assert_select "a[href=?]", help_path assert_select "a[href=?]", login_path assert_select "a[href=?]", about_path assert_select "a[href=?]", contact_path assert_select "a[href=?]", "http://news.railstutorial.org/" # ログインする log_in_as(@user) get root_path assert_select "a[href=?]", root_path assert_select "a[href=?]", help_path 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
10.3.2 演習 サンプルのユーザー
10.3.2.1
<問題>試しに他人の編集ページにアクセスしてみて、10.2.2で実装したようにリダイレクトされるかどうかを確かめてみましょう。
<回答>
/users/XXXX/edit
にアクセスすると、トップページにリダイレクトされる(XXXXはユーザーID)
10.3.3 演習 ページネーション
10.3.3.1
<問題>Railsコンソールを開き、pageオプションにnilをセットして実行すると、1ページ目のユーザーが取得できることを確認してみましょう。
<回答>
> User.paginate(page: nil) User Load (4.4ms) SELECT "users".* FROM "users" LIMIT ? OFFSET ? [["LIMIT", 30], ["OFFSET", 0]] => [#<User:0x007fd36897ceb0 id: 1, name: "Example User", email: "example@railstutorial.org", created_at: Wed, 23 Aug 2017 00:25:16 UTC +00:00, updated_at: Wed, 23 Aug 2017 00:25:16 UTC +00:00, password_digest: "$2a$10$mjXbcC/DgUxXF.uhaGsZDeueTiLUbdsXsQVso79iYlmi1Nq7hMu0q", remember_digest: nil, admin: true>, #<User:0x007fd36894fcf8
10.3.3.2
<問題>先ほどの演習課題で取得したpaginationオブジェクトは、何クラスでしょうか? また、User.allのクラスとどこが違うでしょうか? 比較してみてください。
<回答>
> paginate_user = User.paginate(page: nil) > paginate_user.class => User::ActiveRecord_Relation > all_user = User.all > all_user.class => User::ActiveRecord_Relation
クラスは一緒だよ!
10.3.4 演習 ユーザー一覧のテスト
10.3.4.1
<問題>試しにリスト 10.45にあるページネーションのリンク (will_paginateの部分) を2つともコメントアウトしてみて、リスト 10.48のテストが redに変わるかどうか確かめてみましょう。
<回答>
FAIL["test_index_as_admin_including_pagination_and_delete_links", UsersIndexTest, 4.353926331999901] test_index_as_admin_including_pagination_and_delete_links#UsersIndexTest (4.35s) Expected at least 1 element matching "div.pagination", found 0.. Expected 0 to be >= 1. test/integration/users_index_test.rb:14:in `block in <class:UsersIndexTest>'
divタグのpaginationクラスが無いからエラーになる
10.3.4.2
<問題>先ほどは2つともコメントアウトしましたが、1つだけコメントアウトした場合、テストが greenのままであることを確認してみましょう。will_paginateのリンクが2つとも存在していることをテストしたい場合は、どのようなテストを追加すれば良いでしょうか? ヒント: 表 5.2を参考にして、数をカウントするテストを追加してみましょう。
<回答>
- 1つだけコメントアウトした場合、テストが greenのままであることを確認
- 2つとも存在していることをテストする
# test/integration/users_index_test.rb assert_select 'div.pagination', count: 2 # count: 2を追加
10.3.5 演習 パーシャルのリファクタリング
10.3.5.1
<問題>リスト 10.52にあるrenderの行をコメントアウトし、テストの結果が redに変わることを確認してみましょう。
<回答>
FAIL["test_index_as_admin_including_pagination_and_delete_links", UsersIndexTest, 1.8776025120005215] test_index_as_admin_including_pagination_and_delete_links#UsersIndexTest (1.88s) Expected at least 1 element matching "a[href="/users/14035331"]", found 0.. Expected 0 to be >= 1. test/integration/users_index_test.rb:17:in `block (2 levels) in <class:UsersIndexTest>' test/integration/users_index_test.rb:16:in `block in <class:UsersIndexTest>'
一覧に表示されるはずのユーザーへのリンクパスが見つから無いのでエラーになります。
10.4.1 演習 管理ユーザー
10.4.1.1
<問題>Web経由でadmin属性を変更できないことを確認してみましょう。具体的には、リスト 10.56に示したように、PATCHを直接ユーザーのURL (/users/:id) に送信するテストを作成してみてください。テストが正しい振る舞いをしているかどうか確信を得るために、まずはadminをuser_paramsメソッド内の許可されたパラメータ一覧に追加するところから始めてみましょう。最初のテストの結果は redになるはずです。
<回答>
# test/controllers/users_controller_test.rb 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, password_confirmation: @other_user.password_confirmation, admin: true } } assert_not @other_user.reload.admin? end
10.4.2 演習 destroyアクション
10.4.2.1
<問題>管理者ユーザーとしてログインし、試しにサンプルユーザを2〜3人削除してみましょう。ユーザーを削除すると、Railsサーバーのログにはどのような情報が表示されるでしょうか?
<回答>
10.4.3 演習 ユーザー削除のテスト
10.4.3.1
<問題>試しにリスト 10.59にある管理者ユーザーのbeforeフィルターをコメントアウトしてみて、テストの結果が redに変わることを確認してみましょう。
<回答>
FAIL["test_should_redirect_destroy_when_logged_in_as_a_non-admin", UsersControllerTest, 4.160433043001831] test_should_redirect_destroy_when_logged_in_as_a_non-admin#UsersControllerTest (4.16s) "User.count" didn't change by 0. Expected: 34 Actual: 33 test/controllers/users_controller_test.rb:56:in `block in <class:UsersControllerTest>'
画面からだとdeleteリンクがでないから、多分これありえないけど攻撃される時はきっとテストみたいな感じで直接やられるのだろう。 それをテストでしっかりブロックできてることを確認できるのは良いね。 で、patchとかdeleteメソッドってどうやってやるんだろうか?
railsチュートリアル10章でつまずいた
$ rails test Running via Spring preloader in process 13479 Started with run options --seed 32097 ERROR["test_index_as_non-admin", UsersIndexTest, 1.2183937259997037] test_index_as_non-admin#UsersIndexTest (1.22s) NoMethodError: NoMethodError: undefined method `paginate' for #<Class:0x007fc03bee53b0> app/controllers/users_controller.rb:7:in `index' test/integration/users_index_test.rb:29:in `block in <class:UsersIndexTest>' ERROR["test_index_as_admin_including_pagination_and_delete_links", UsersIndexTest, 1.2559454040001583] test_index_as_admin_including_pagination_and_delete_links#UsersIndexTest (1.26s) NoMethodError: NoMethodError: undefined method `paginate' for #<Class:0x007fc03bee53b0> app/controllers/users_controller.rb:7:in `index' test/integration/users_index_test.rb:12:in `block in <class:UsersIndexTest>' 37/37: [==========================================================================================================================================================] 100% Time: 00:00:01, Time: 00:00:01 Finished in 1.95285s 37 tests, 93 assertions, 0 failures, 2 errors, 0 skips rh0257:railstutorial_10 taku$ spring stop # これやったら治った! Spring stopped. rh0257:railstutorial_10 taku$ rails test Running via Spring preloader in process 13604 Started with run options --seed 8584 37/37: [==========================================================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02 Finished in 2.85845s 37 tests, 156 assertions, 0 failures, 0 errors, 0 skips
くそー動かしてみて問題無いのなら、一先ずほっとくのもありだね。 忘れた頃に治ってるかも。