Railsチュートリアル 第10章

10.1 ユーザーを表示する

10.1.1

演習1

省略.

演習2

new.html.erbedit.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'

のように書けば, パーシャル内では変数userUserオブジェクトが格納されることになります. つまりパーシャル内の変数名を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

省略.