Railsチュートリアル 第12章

12.1 PasswordResets リソース

12.1.1

演習1

省略.

演習2

メールにはフルのURLを記載しなくてはなりませんからね.

12.1.2

演習1

APIRailsガイド等を見てみましたが, よくわかりませんでした. 今回の場合は,

という理由しか思い浮かびませんでした.

12.1.3

演習1

UserMailer.password_reserに対し, 以下のエラーが表示されました.

wrong number of arguments (given 1, expected 0)

11章をやっていない場合はまた異なるエラーが出るんでしょうね.

演習2

たしかに演習1で入力したメールアドレスに対応するユーザーには, reset_sent_at属性に値が存在しました.

12.2 パスワード再設定のメール送信

12.2.1

演習1, 2, 3

省略.

12.2.2

演習1

問題なくクリアしました.

演習2

省略.

12.3 パスワードを再設定する

12.3.1

hiddenタグを利用することによって, updateアクションからもメールアドレスがparams[:email]という形で参照できるようになりました. editアクションとupdateアクションにおけるメールアドレスの参照の仕方が統一されたため, before actionをつくることができます.

演習1

問題なく表示されました.

演習2

すでに空っぽのupdateアクションを作っていたので,

No template found for PasswordResetsController#update

というエラーが発生しました.

12.3.2

演習1, 2

省略.

12.3.3

演習1

省略.

演習2

該当箇所は以下のように記述しました.

assert_nil user.reload.reset_digest

演習3

省略.

演習4

updateアクションを以下のように変更しました.

# app/controllers/password_resets_controller.rb

def update
    if params[:user][:password].empty?
        @user.errors.add(:password, :blank)
        render 'edit'
    elsif @user.update_attributes(user_params)
        log_in(@user)
        @user.update_attribute(:reset_digest, nil) # 追加
        flash[:success] = 'Password has been reset'
        redirect_to @user
    else
        render 'edit'
    end
end

12.4 本番環境でのメール送信

この節はやっていません.

Railsチュートリアル 第11章

11.1 AccountActivations リソース

11.1.1

演習1

省略.

演習2

メールにはフルのURLが記載されていないとアクセスできませんからね.

11.1.2

演習1

省略.

演習2

インスタンスを生成するのは面倒ですが, before_createが機能しているかを確かめましょう. create_activation_digestを呼び出した際のログは一部省略しています.

>> user = User.create(
>*   name: "Console Test",
>*   email: "console@test.com",
>*   password: "password",
>*   password_confirmation: "password")

>> user.create_activation_digest
NoMethodError (private method `create_activation_digest' called for #<User:0x00007fbb70839d60>)

>> user.activation_digest
=> "$2a$10$olzQ9tgFiKu/mrHwP/D5nO8XVyiz1TJ9dSVKO1.lvkc9EAdnSL6cK"

はじめbefore_createの機能を勘違いしていました. Rubyとしてのオブジェクトを作成する前に実行されるものだと思っていました. だから上の演習もUser.newを実行して確認しようとしていました.

しかしここでいうcreateは, データベース操作CRUDのうちのひとつのことだったんですね. だからUser.newではactivation_tokenが作成されませんでした.

11.2 アカウント有効化のメール送信

11.2.1

演習1

以下のようになりました.

>> CGI.escape("Don't panic!")
=> "Don%27t+panic%21"

11.2.2

config/environments/development.rbを編集したあとの再起動は以下のコマンドで行いました.

$ docker-compose restart

ちなみに再起動する前にメールプレビューを見てみましたが, エラーが出ました.

演習1

Date欄には以下の内容が表示されました.

Mon, 02 Jul 2018 04:59:25 +0000

Localeを変更しても特に変化はありませんでしたね.

11.2.3

演習1, 2

省略.

11.2.4

演習1

Railsサーバーが起動しているコンテナの標準入出力をローカルマシンのターミナルにアタッチします.

$ docker attach <container id or name>

この状態で新規ユーザー登録をすると, メール送信のログを見ることができます.

終わったら

Ctrl+P, Ctrl+Q

を押してデタッチをしましょう. 間違ってCtrl+D等でコンテナを抜け, コンテナが落ちてしまった場合はこちらを参考にしてください.

演習2

省略.

11.3 アカウントを有効化する

11.3.1

演習1, 2

こんな感じです.

>> user = User.create(
>*   name: "Activation Test",
>*   email: "activation@test.com",
>*   password: "password",
>*   password_confirmation: "password")

>> token = user.activation_token
=> "Pr4fTMDEgwnsh4rTUSsQiA"
>> user.authenticated?(:activation, token)
=> true

11.3.2

演習1, 2

省略.

11.3.3

演習1, 2

簡単なので省略.

演習3

アクティベートされていないユーザーをfixture内に作成しました.

# test/fixtures/users.yml

non_activated:
  name: Non Activated
  email: non_active@example.com
  password_digest: <%= User.digest('password') %>

統合テスト用の新しいクラスを作成すべきなのか, 不勉強なため判断がつきませんでした. すくなくともusers/indexページには関連したテストなので, users_index_test.rbに書くことにしました.

# test/integration/users_index_test.rb

def setup
    @admin = users(:michael)
    @non_admin = users(:archer)
    @non_activated = users(:non_activated)
end

test "should not show non-activated user" do
    log_in_as(@admin)
    get users_path
    assert_select 'a[href=?]', user_path(@non_activated), count: 0

    get user_path(@non_activated)
    assert_redirected_to root_url
end

11.4 本番環境でのメール送信

この節はやっていません.

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

省略.

Railsチュートリアル 第9章

9.1 Remember me 機能

9.1.1

演習1

問題なく動作しました.

演習2

もともとリスト9.4の書き方をしていました. リスト9.5の書き方は省略します.

9.1.2

疑問1 current_userメソッドの定義

第8章では一時セッションにユーザーIDを保存していましたが, 本章ではユーザー情報を永続的に保存するために, クッキーを用いています. そのため, 第8章で定義したcurrent_userメソッドを修正しています. それがこちらです.

def current_user
    if (user_id = session[:user_id])
        @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
        user = User.find_by(id: user_id) 
        if user && user.authenticated?(cookies[:remember_token])
            log_in user
            @current_user = user
        end
    end
end

はじめはsessioncookiesの評価がどうしてこの順番なのかがわかりませんでした.

コードを見た通り, sessionが空かどうかをチェックし, 空であった場合はcookiesが空かどうかのチェックに移っています. この評価の順番が正当化されるということは, 「sessionは空だがcookiesは空ではない」という状況がありえることになります. それはブラウザを閉じたあとのことですね. sessionは短命なのでブラウザを閉じれは消えてしまいます. しかしcookiesは残ったままです. これでこの順番に評価をすることが意味を持つわけですね.

また後に実装するように, ユーザーはRemember me機能を使わないという選択をとれるようになります. したがってユーザーによってはcookiesには値がない場合もあるわけです. それならば必ず利用されるsessionをはじめに評価するほうが合理的ですね.

疑問2 どうしてログアウトできないのか

演習の直前に, テストスイートが失敗することが述べられています. その理由として, ユーザーがログアウトできないからだと書いてあります. 今までに定義したヘルパーメソッド等を思い出しながら, これについて詳しく見ていきましょう.

実際に統合テストを実行すると失敗します. 失敗した箇所を以下に示します.

# test/integration/users_login_test.rb
# login_with_valid_information_followed_by_logout
# ログアウトの処理以降のみを抜粋

delete logout_path
assert_not is_logged_in?
assert_redirected_to root_url
follow_redirect!
assert_select "a[href=?]", login_path   #ここで失敗
assert_select "a[href=?]", logout_path, count: 0
assert_select "a[href=?]", user_path(@user), count: 0

以下テストで実行される流れを書いていきます.

logout_pathdeleteリクエストが飛びます. これによりSessionsControllerdestroyアクションが実行されます.

# SessionsController

def destroy
    log_out
    redirect_to root_url
end

ログアウト処理がなされ, root_urlにリダイレクトしていますね. このログアウト処理はなにをしているかというと, sessionからユーザーIDを消去し, インスタンス変数の@current_userを初期化しています.

# app/helpers/sessions_helper.rb

def log_out
    session.delete(:user_id)
    @current_user = nil
end

テストの続きへ進みます.

テストヘルパーメソッドのis_logged_in?によって, ログインした状態かどうかをチェックしています.

def is_logged_in?
    !session[:user_id].nil?
end

log_outメソッドによりsessionの内容は消去しているので, 当然このアサーションはクリアします. 次です.

destroyアクションの定義内に, リダイレクトするように書かれていますから, redirected_toアサーションもクリアします. 問題は次です.

さて問題はselectアサーションです. これが失敗する理由は本文にもあるようにログアウトできていないからです. つまりログインしてしまっているからです. ちゃんとsessionを消去したのに, 復活してしまっているんですね. その答えがヘッダー内の次の一行にあります.

# app/views/layouts/_header.html.erb

<% if logged_in? %>

ログインしているかどうかによって, ヘッダーの内容を変化させるためのif文です. ではヘルパーメソッドのlogged_in?の定義を見てみましょう.

# app/helpers/sessions_helper.rb

def logged_in?
    !current_user.nil?
end

current_usernilかどうかで, ログインしているかどうかを判断しています. さてcurrent_userメソッドはどんな定義でしたっけ...? 疑問1でも取り上げたように, 以下のような定義でしたね.

def current_user
    if (user_id = session[:user_id])
        @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
        user = User.find_by(id: user_id) 
        if user && user.authenticated?(cookies[:remember_token])
            log_in user
            @current_user = user
        end
    end
end

そろそろ核心に近づいてきました. current_userメソッドでは, sessionが空であった場合はcookiesの中身を見ます. ログアウトしてsession情報を消去しtも, cookiesの中身は残ったままですよね. ということはそのデータをもとにしてユーザーの検索が行われます. そしてauthenticated?が通った場合, log_inメソッドが実行されてしまいます. ここが, session情報の復活点でした. cookie情報が残った状態でヘッダーのレンダリングを行えば, 自然とログインしてしまうようになっていたのです.

演習1, 2

省略.

9.1.3

これでログアウトができますね.

演習1

省略.

9.1.4

細かいnilの扱いは難しいですね...

演習1, 2, 3

省略.

9.2 [Remenber me] チェックボックス

省略.

9.3 [Remember me] のテスト

9.3.1

演習1

sessions_controller.rbcreate内のuserインスタンス変数@userに変えます. するとassignsメソッドでインスタンス変数を参照できるようになるので, より詳細にテストを書くことができますね.

# test/integration/users_login_test.rb

test "login with remembering" do
    log_in_as(@user, remember_me: '1')
    assert_equal cookies['remember_token'] , assigns(:user).remember_token
end

9.3.2

省略.

Railsチュートリアル 第8章

8.1 セッション

8.1.1

演習1

省略.

演習2

routes.rbに記述したルーティングのみが表示されました.

8.1.2

演習1

routes.rbにおける設定

post '/login', to: 'sessions#create'

により実現されていますね.

8.1.3

演習1

省略.

8.1.4

Flashメッセージについて今ひとつよくわからなかったので, Railsガイドを参照しました.

flashはセッションの中の特殊な部分であり、リクエストごとにクリアされます。この特徴から、flashは「直後のリクエスト」でのみ参照可能になります。

これを踏まえて, Flashメッセージが残ってしまう理由を説明しましょう.

ログインボタンを押すことにより, /loginに対しPOSTリクエストが送られます. このとき変数flashにエラーメッセージが格納されます.

これと同時にnewページがレンダリングされますが, これは同一のPOSTリクエストの中で行われます.

上の引用文からもわかるように, 次のリクエストでもまだ変数flashにエラーメッセージは格納されたままです. だからHomeページに移動してもフラッシュメッセージが表示されたままなのです.

次にHelpページなどにアクセスし, もう一度HTTPリクエストを送ることにより, やっとflashの中身がクリアされます.

8.1.5

演習1

flash.nowがうまく機能していることがブラウザ上でも確認できます.

8.2 ログイン

Applicationコントローラの定義内に, 追加した覚えのないメソッドがある...どこかで見落としたのでしょうか.

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
    protect_from_forgery with: :exception # 見知らぬ行
    include SessionHelper
end

8.2.1

演習1

Sxi%2FKPGHw...のような暗号化された値で保存されていますね.

演習2

有効期限の欄にはセッションと書かれています.

8.2.2

演習1

たしかにnilが返ってきます.

irb(main):003:0> User.find_by(id: 2)
  User Load (0.9ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1
=> nil

演習2

以下のようになりました. 例外を発生させないことと, 2回目にデータベースへの問い合わせが無いことが確認できますね.

>> session = {}
=> {}
>> session[:user_id] = nil
=> nil

>> @current_user ||= User.find_by(id: session[:user_id])
   (0.6ms)  SET NAMES utf8,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
  User Load (0.3ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` IS NULL LIMIT 1
=> nil

>> session[:user_id] = User.first.id
  User Load (0.5ms)  SELECT  `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1
=> 1

>> @current_user ||= User.find_by(id: session[:user_id])
  User Load (1.2ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2018-06-28 05:35:56", updated_at: "2018-06-28 05:35:56", password_digest: "$2a$10$i8HBze1p.5miIytb9ahZjefM27b8EXVMSMjV/B0za7t...">

>> @current_user ||= User.find_by(id: session[:user_id])
=> #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2018-06-28 05:35:56", updated_at: "2018-06-28 05:35:56", password_digest: "$2a$10$i8HBze1p.5miIytb9ahZjefM27b8EXVMSMjV/B0za7t...">

8.2.3

Bootstrapのドロップダウンがうまく機能しません. 何行かコードを追加すると動くという記事もありましたが, 浅い理解でコードを書き換えるのも嫌なので後回しにします.

と思ったらただのタイポでした. ではBootstrapが動かないと報告している記事はなんだったんでしょう.

演習1, 2

省略.

8.2.4

演習1, 2

省略.

8.2.5

演習1, 2

省略.

8.3 ログアウト

演習1

省略.

演習2

ログアウトと同時にcookieの値が変化していることがわかります.

Railsチュートリアル 第7章

7.1 ユーザーを表示する

7.1.1

演習1

以下の通りでした.

--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
  controller: static_pages
  action: about
permitted: false

演習2

全く同じものが表示されました.

irb(main):002:0> puts user.attributes.to_yaml
---
id: 4
name: Michael Jackson
email: mhartl@example.com
created_at: !ruby/object:ActiveSupport::TimeWithZone
  utc: &1 2018-06-25 09:33:51.000000000 Z
  zone: &2 !ruby/object:ActiveSupport::TimeZone
    name: Etc/UTC
  time: *1
updated_at: !ruby/object:ActiveSupport::TimeWithZone
  utc: &3 2018-06-25 09:41:15.000000000 Z
  zone: *2
  time: *3
password_digest: "$2a$10$tM4ebAaSFIzfhqzi6.Lo/Ofs6/TfKpnP7LrgkCRV/z02U.CjwKubu"
=> nil

7.1.2

演習1, 2

こんな感じでしょうかね.

# app/views/users/show.html.erb
<%= @user.name %>, <%= @user.email %>,
<%= @user.created_at %>, <%= @user.updated_at %><br>

<%= Time.now %>

7.1.3

Dockerで環境を構築している場合は, Byebugを利用するためにひと工夫必要なようです. 詳しくはこちらに記載しました.

users_controller.rbdebuggerを追記したら, 以下のコマンドでコンテナの標準入出力とローカルマシンのターミナルをアタッチします.

docker attach <Rails' container name>

この状態で/users/1にアクセスすると, Byebugのプロンプトがターミナルに表示されます.

[1, 9] in /myapp/app/controllers/users_controller.rb
   1: class UsersController < ApplicationController
   2:   def show
   3:       @user = User.find(params[:id])
   4:       byebug
=> 5:    end
   6:   
   7:   def new
   8:   end
   9: end
(byebug)

paramsの内容をYAML形式で表示してみましょう.

(byebug)puts params.to_yaml
--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
  controller: users
  action: show
  id: '1'
permitted: false
nil

yメソッドも試してみます.

 (byebug) y params
*** NoMethodError Exception: undefined method `y' for #<UsersController:0x00007fe159010210>

Byebugではyメソッドは使えないみたいですね.

演習2

newアクションではインスタンスメソッドの@userは定義されていませんね.

7.1.4

演習1, 2

省略.

演習3

リスト7.12と7.13を見比べると, コードの量が異なりますね. ハッシュを引数にとる場合は, ハッシュの中から値を取り出すコードを書く必要があります. しかしキーワード引数であれば, それは不要です.

他にも, メソッドが予期していない引数を受け取った場合にも挙動が異なります. ハッシュ引数の場合, 以下のような誤った引数を指定してもエラーが発生しません.

grabatar_for @user, size: 100, shape: :square

ハッシュを受け取っている以上, 間違った引数と認識されないわけですね. しかしキーワード引数の場合, 上記のような引数を指定するとエラーが発生します. 定義していないキーワードが存在するからですね.

7.2 ユーザー登録フォーム

7.2.1

演習1

:name:nomeと書き換えユーザー登録ページにアクセスすると, 以下のようなエラーが表示されました.

undefined method `nome' for #<User:0x00007fe154bddef8>

演習2

Vimの置換コマンドを使ってffoobarに書き換えましょう.

:%s/f\./foobar\.

ファイル全体のf.foobar.に置換しています. これだけではブロック引数の|f|の部分は置換できないので, そこは手動で書き換えましょう.

以下のようにしてしまうと無駄なところまで置換されるので注意ですね.

:%s/f/foobar

この状態でユーザー登録ページにアクセスしても, 問題なく表示されます. では何が問題なのでしょうか...? わかりませんでした. でもあまりよくないとのことですから, 戻しておきました. 戻すときは以下のコマンドで良いですね.

:%s/foobar/f

7.2.2

演習1

いちいちHTMLタグを書くのなんて面倒ですもんね.

7.3 ユーザー登録失敗

7.3.1

Rails側ではシンボル, HTML側では文字列というのが少しややこしいですね.

7.3.2

演習1

省略.

7.3.3

演習1

省略.

演習2

入力データの検証に失敗した場合, createコントローラによって, newページがレンダリングされます. あくまでアクセスしているURLはcreateに対応する/usersということでしょうかね.

7.3.4

演習1

こちらを参考にしました.

assert_select 'div#error_explanation'
assert_select 'div.field_with_errors'
assert_select 'li', "Name can't be blank"
assert_select 'li', "Email is invalid"
assert_select 'li', "Password is too short (minimum is 6 characters)"
assert_select 'li', "Password confirmation doesn't match Password"

演習2

何が問題になっているかを整理しましょう.

config/routes.rbに以下のルーティングを設定しました.

post '/signup', to: 'users#create'

これによって, /signupへのPOSTリクエストに, createアクションが対応するようになりました.

しかしテストコードでPOSTリクエストをしているURLは/usersのままです.

assert_no_difference 'User.count' do
    post users_path, params: #略
end

それなのにテストをパスしまっているのはどうしてだろうというのが, この演習の問いです.

答えとしては, routes.rbに上記の設定をしたとしても, /usersへのPOSTリクエストとcreateアクションの対応は消えたわけではないとうことでしょうね.

演習3

以下のように変更し, テストにパスすることを確認しました.

assert_no_difference 'User.count' do
    post signup_path, params: #略
end

演習4

正しいアクションが指定されているかを確認するテストを書き加え, テストにパスすることを確認しました.

assert_select 'form[action="/signup"]'

7.4 ユーザー登録成功

7.4.1

演習1, 2

省略.

7.4.2

演習1, 2

省略.

7.4.3

演習1, 2

省略.

7.4.4

演習1

これまでのように, HTMLタグの有無を確認するテストをはじめは思いつきました.

assert_select 'div.alert-success'

flashが空でないかを確認するのであれば, 以下のように書けます.

assert_not flash.empty?

この書き方でテストがパスすることが確認できました. しかしこれだと演習2の変更がうまくいっているかどうかのテストはできませんね.

演習2, 3

省略.

演習4

データの保存に失敗した場合はリダイレクトが行われません. したがってテンプレートファイルのassertionでこのバグを検知することができます.

7.5 プロのデプロイ

省略.

Docker環境でByebugを使う方法

概要

Railsチュートリアルの第7章でByebugを使っています. Docker環境でByebugを利用するためには, 追加手順が必要なので書き残しておきます.

本当ははじめから全てが整った環境を構築できれば申し分ないですが, 途中からでも以下の手順を実行すれば問題ありません.

手順

以前書いたこちらの記事の通りに環境が出来上がっている状態からスタートします.

docker-compose.ymlに, 以下の設定を加えます.

# docker-compose.yml
services:
  web:
    stdin_open: true
    tty: true

これら2つの設定により, コンテナ内の標準入出力をローカルマシンのターミナルにアタッチする準備が整います.

さてComposeファイルを修正したので, それを反映させなくてはなりません. 先ほども述べたように, すでに環境が構築し終わった後にこの変更を加えています. この場合は以下のコマンドを実行すればOKです.

$ docker-compose up -d

公式ドキュメントを見て, コマンドの説明を見てみましょう.

If there are existing containers for a service, and the service’s configuration or image was changed after the container’s creation, docker-compose up picks up the changes by stopping and recreating the containers (preserving mounted volumes).

service's configurationへの変更は, コンテナを再構築することにより適用されるということですね.

コンテナが起動した状態でもいいの?

上で引用した説明分に書かれているように, すでにコンテナを起動している状態でも上のコマンドを実行することができます.

docker-compose restartではないの?

これについては公式ドキュメントに書いてあります.

If you make changes to your docker-compose.yml configuration these changes are not reflected after running this command.

このコマンドではComposeファイルの変更は反映されません.

docker-compose buildからやり直す必要はないの?

これも公式ドキュメントに書いてあります.

If you change a service’s Dockerfile or the contents of its build directory, run docker-compose build to rebuild it.

このコマンドを使うのは, DockerfileGemfileに変更があった場合です.

Byebugの使い方

上の手順を実行することにより, Byebugを使う準備が整いました. ここからはByebugの使い方です. 以下のコマンドを実行し, コンテナが起動した状態にあるとします.

$ docker-compose up -d

次のコマンドで, Railsが動いているコンテナのIDもしくはコンテナ名を調べます.

$ docker ps

あとは今調べたコンテナの標準入出力を, ローカルマシンのターミナルにアタッチするコマンドを実行します.

$ docker attach <container name or ID>

この状態でWebアプリケーションにアクセスすると, Byebugの出力がターミナルに表示されます.

デタッチする

コンテナの標準入出力を, ローカルマシンのターミナルにアタッチする方法がわかりました. 大事なのはここからです. デタッチを正しく行わないと面倒なことになります.

デバッグが終了し, コンテナの標準入出力をデタッチしたい場合は以下を実行してください.

Ctrl+P, Ctrl+Q

つまりCtrlキーを押しっぱにしたままpqと押せばいいわけですね. もしCtrl+Cを押してしまった場合は面倒なことになる可能性があります. そのときは こちらの記事を参照してください.