Railsチュートリアル 第10章
10.1 ユーザーを表示する
10.1.1
演習1
省略.
演習2
new.html.erb
とedit.html.etb
のフォームはほとんど同じ形をしているから, パーシャルでまとめてしまおうということですね. ほとんど同じとはいっても異なる点が2箇所あります. それがボタンのテキストと, フォームのアクション属性です. これらを考慮しながらパーシャルを以下のように作りました.
# app/views/users/_form.html.erb <%= form_for(@user, url: yield(:form_action)) do |f| %> <%= render 'shared/error_messages' %> # 気になった点その1 <%= 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 yield(:button_text), class: 'btn btn-primary' %> <% end %>
# app/views/users/new.html.erb <% provide(:title, "Sign up") %> <% provide(:button_text, 'Create my account') %> <% provide(:form_action, signup_path) %> <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(:button_text, 'Save changes') %> <% provide(:form_action, user_path) %> # 気になった点その2 <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>
ただしいくつか気になった点があったのでそれらを掘り下げていくことにします.
気になった点その1
気になった箇所はコード内にコメントしておきました. この行の内容は, チュートリアルの見本コードとは異なります. なぜかというと不要ではないかと感じたからです. まずチュートリアルのコードを提示しましょう.
<%= render 'shared/error_messages', object: @user%>
render
メソッドにハッシュが渡されているのがわかりますね. これが必要かどうかを議論するには, render
メソッドの使い方について理解しなければなりません. 詳しくはAPI)を見ていただきたいのですが, ここでは上のコードにフォーカスして説明します.
render
メソッドには, パーシャルに変数を渡す方法が用意されています. つまりテンプレート内で使用可能な変数を, パーシャル内の変数に割り当てることができます. その方法のうちのひとつが, object:
という記法です. Rubyではすべてがオブジェクトですから, :object
というキーが使われているんだと思います.
見ての通り@user
というUser
オブジェクトをパーシャルに渡していることがわかります. ではパーシャル内ではどのような変数名でこのオブジェクトにアクセスできるのでしょうか.
たとえば
object: @user, as: 'user'
のように書けば, パーシャル内では変数user
にUser
オブジェクトが格納されることになります. つまりパーシャル内の変数名をas:
によって指定できるわけですね.
しかしチュートリアルではこの指定が無いのでデフォルトの変数名が指定されます. それはパーシャル名です. ここで言えばerror_messages
ですね.
render
メソッドの説明が済んだので結論です. チュートリアルのコードでは, パーシャル_error_messages.html.erb
内において, オブジェクト@user
が変数error_messages
に格納されるようになっています. しかし変数error_messages
は使われていません. そのかわり直接インスタンス変数@user
を参照しています. したがってobject: @user
は不要であると考えられます.
気になった点その2
これも気になった箇所はコード内にコメントしておきました. この行ははじめ以下のように書きました.
<% provide(:form_action, user_path(@user)) %>
表7.1を見ても, このような記法がなされています. しかし同じ演習問題の解答を載せているブログ等を見てみると,
<% provide(:form_action, user_path) %>
のように書かれています. User
オブジェクトがなくても正しいURLになるのか不安でした.
このコードでよい理由はおそらく, パーシャルを適用させたテンプレートがこのようになるからでしょうね.
form_for(@user, url: user_path)
form_for
メソッドの第一引数に@user
を渡している時点で, ユーザーID等の情報がフォームに伝わっています. だからuser_path
に@user
を渡さなくとも, 正しいURLを生成できるということなのでしょう.
10.1.2
省略.
10.1.3
演習1
以下の一行を追加しました.
# test/integration/users_edit_test.rb assert_select "div.alert", "The form contains 4 errors."
10.1.4
省略.
10.2 認可
10.2.1
beforeフィルターを次のように定義しています.
def logged_in_user unless logged_in? flash[:danger] = "Please log in." redirect_to login_url end end
login_path
ではなく, login_url
としているのがポイントですね. これについては第5章に書かれています.
なお、Railsチュートリアルでは一般的な規約に従い、基本的には
_path
書式を使い、リダイレクトの場合のみ_url
書式を使うようにします。これはHTTPの標準としては、リダイレクトのときに完全なURLが要求されるためです。演習1
beforeフィルターが全コントローラに適用された場合は, SignupページやLoginページにアクセスできなくなりますから, 多くのテストが失敗しますね.
10.2.2
演習1
調べてみると,
curl
コマンドでPATCH
メソッドのHTTPリクエストを送れるようですね. ということは, フォーム画面=edit
アクションを経由することなく, いきなりupdate
アクションにデータを送信できるということです. だからupdate
アクションに対してもbeforeフィルターが必要ということですね.
演習2
ブラウザで確認しやすいのはedit
アクションのほうですね.
10.2.3
フレンドリーフォワーディングのテストは過剰な気がします.
- 編集画面にアクセス
- ログイン画面にリダイレクト
- ログイン
- さっきの編集画面にリダイレクト
という流れをチェックするためのテストなので, データが正しく更新されたかどうかをチェックする部分は不要なのではないでしょうか. 更新がうまくいくかのテストはすでに書いているわけですしね. もちろん手厚いテストを書いておけばいいのはわかりますが, なくてはならないのかが気になります.
演習1
はじめはsuccessful edit with friendly forwarding
テストの最後に以下を追加することを考えました.
delete logout_path
get login_path
assert_nil session[:forwarding_url]
つまり一旦ログアウトして, またログインするときにはデフォルトページにリダイレクトされるということを確認しています. しかし本文を見返してみると, この演習について以下のように書かれています.
session.delete(:forwarding_url)
という行を通して転送用のURLを削除している点にも注意してください。これをやっておかないと、次回ログインしたときに保護されたページに転送されてしまい、ブラウザを閉じるまでこれが繰り返されてしまいます (このコードのテストは10.2.3.1の演習に回します)。
というわけで, ちゃんとsession
の内容がデリートされているかをチェックすればいいんですね. チュートリアルをこなしているブログを参考にすると, 以下のようなテストがよさそうです. session
の中身をログインの前後で確認しています.
test "successful edit with friendly forwarding" do get edit_user_path(@user) assert_equal edit_user_url(@user), session[:forwarding_url] #追加 log_in_as(@user) assert_nil session[:forwarding_url] #追加 assert_redirected_to edit_user_url(@user) name = "Foo Bar" email = "foo@bar.com" patch user_path(@user), params: { user: { name: name, email: email }} assert_not flash.empty? assert_redirected_to @user @user.reload assert_equal name, @user.name assert_equal email, @user.email end
演習2
session[:forwarding_url]
とrequest.get?
はそれぞれ期待通りの値が返ってきました.
(byebug) session[:forwarding_url] "http://localhost/users/1/edit" (byebug) request.get? true
せっかくだから他のことも調べておこうと思ったら...あれ?
(byebug) request.original_url "http://localhost/login"
session[:forwarding_url]
の内容と異なりますね. つまりログインページにリダイレクトした際にRequest
オブジェクトが更新されたということです. APIを参考にこのRequest
オブジェクトを少し調べてみましょう.
(byebug) request.class ActionDispatch::Request (byebug) request.controller_class SessionsController (byebug) request.request_parameters {}
これだけやればこの演習はよいでしょう.
10.3 すべてのユーザーを表示する
10.3.1
演習1
以下のように書いてみました.
def setup @user = users(:michael) end test "layout links when logged in" do log_in_as(@user) get root_path assert_select "a[href=?]", root_path, count: 2 assert_select "a[href=?]", help_path assert_select "a[href=?]", about_path assert_select "a[href=?]", contact_path assert_select "a[href=?]", user_path(@user) assert_select "a[href=?]", edit_user_path(@user) assert_select "a[href=?]", logout_path end
10.3.2
rails db
コマンドに時間は全くかかりませんでした.
演習1
Example Userでログインした状態でusers/2/edit
へアクセスすると, トップページにリダイレクトされました.
10.3.3
演習1
省略.
演習2
どちらもUsers::ActiveRecord_Relation
クラスでした.
10.3.4
演習1
Redにならないぞと思ったら, コメントアウトを#
でやってました. 正しくコメントアウトしたところ, テストは失敗しました.
演習2
<div class="pagination">
が2つあることを確認するために, count: 2
を書き加えました.
test "index including pagination" do log_in_as(@user) get users_path assert_template 'users/index' assert_select 'div.pagination', count: 2 # 追加 User.paginate(page: 1).each do |user| assert_select 'a[href=?]', user_path(user), text: user.name end end
10.3.5
演習1
省略.
10.4 ユーザーを削除する
10.4.1
演習1
以下のように実装しました.
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, admin: true }} assert_not @other_user.reload.admin? end
10.4.2
演習1
以下のコマンドにより, Railsサーバーが稼働しているコンテナの標準入出力を手元のターミナルにアタッチします.
docker attach <container id or name>
この状態でユーザーを削除すると, 以下のようなログが表示されました.
Started DELETE "/users/6" for 172.18.0.1 at 2018-07-01 07:45:23 +0000 Cannot render console from 172.18.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 Processing by UsersController#destroy as HTML Parameters: {"authenticity_token"=>"tBX07b8P1vb9hze5S0SicpXXW67+E235kSq1EkOgpDF4AXJuJmwVI98KZmGquu8Eq8T42/5HkjSBLSor8YarBg==", "id"=>"6"} User Load (0.6ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 ↳ app/helpers/sessions_helper.rb:18 User Load (0.5ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 6 LIMIT 1 ↳ app/controllers/users_controller.rb:44 (0.3ms) BEGIN ↳ app/controllers/users_controller.rb:44 User Destroy (0.5ms) DELETE FROM `users` WHERE `users`.`id` = 6 ↳ app/controllers/users_controller.rb:44 (1.2ms) COMMIT ↳ app/controllers/users_controller.rb:44 Redirected to http://localhost/users Completed 302 Found in 15ms (ActiveRecord: 3.0ms) Started GET "/users" for 172.18.0.1 at 2018-07-01 07:45:23 +0000 Cannot render console from 172.18.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 Processing by UsersController#index as HTML User Load (0.4ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 ↳ app/helpers/sessions_helper.rb:18 Rendering users/index.html.erb within layouts/application (0.7ms) SELECT COUNT(*) FROM `users` ↳ app/views/users/index.html.erb:4 User Load (0.4ms) SELECT `users`.* FROM `users` LIMIT 30 OFFSET 0 ↳ app/views/users/index.html.erb:7 Rendered collection of users/_user.html.erb [30 times] (4.8ms) Rendered users/index.html.erb within layouts/application (22.6ms) Rendered layouts/_rails_default.html.erb (107.4ms) Rendered layouts/_shim.html.erb (0.3ms) Rendered layouts/_header.html.erb (1.1ms) Rendered layouts/_footer.html.erb (0.4ms) Completed 200 OK in 194ms (Views: 180.8ms | ActiveRecord: 1.6ms)
10.4.3
演習1
省略.