紙一重の積み重ね

35歳のエンジニアがなれる最高の自分を目指して、学んだことをこつこつ情報発信するブログです。

【12章】Ruby on Railsチュートリアル演習まとめ&解答例【12.1 PasswordResetsリソース】

はじめに

Ruby on Rails チュートリアル実例を使ってRailsを学ぼう 第4版の 12章 12.1 PasswordResetsリソースの演習まとめ&回答例です。

個人の解答例なので、誤りがあればご指摘ください。

動作環境

  • cloud9
  • ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-linux]
  • Rails 5.0.0.1

12章 パスワードの再設定

パスワードを忘れた際のリマインダー機能を作る。

  • ログインフォームに「forget password」リンクを追加
  • 「forget password」フォームを追加
    • メールアドレスを入力
    • メールを送信
    • パスワード再設定用リンクを記載
  • パスワード再設定フォームを追加
    • パスワード再設定

12.1.1 PasswordResetsコントローラ

本章での学び

【controller】パスワード再設定用のコントローラを作成する

rails generate controllerで生成する。

  • コントローラ名は、PasswordResets
  • newアクションを生成する
  • editアクションを生成する
  • テストは生成しない
yokoyan:~/workspace/sample_app (password-reset) $ rails generate controller PasswordResets new edit --no--test-framework
Running via Spring preloader in process 1737
Expected string default value for '--jbuilder'; got true (boolean)
Expected string default value for '--helper'; got true (boolean)
Expected string default value for '--assets'; got true (boolean)
      create  app/controllers/password_resets_controller.rb
       route  get 'password_resets/edit'
       route  get 'password_resets/new'
      invoke  erb
      create    app/views/password_resets
      create    app/views/password_resets/new.html.erb
      create    app/views/password_resets/edit.html.erb
      invoke  test_unit
      create    test/controllers/password_resets_controller_test.rb
      invoke  helper
      create    app/helpers/password_resets_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/password_resets.coffee
      invoke    scss
      create      app/assets/stylesheets/password_resets.scss

【routes】パスワード再設定用リソースを追加する

routesファイルに以下を追加する。

  • リマインダー画面
    • 画面描画
      • newアクション
    • パスワード再設定
      • createアクション
  • パスワード再設定画面
    • 画面描画
      • editアクション
    • パスワード更新
      • updateアクション
Rails.application.routes.draw do
  # 省略
  resources :password_resets, only: [:new, :create, :edit, :update]
end

RESTfulルーティングは下記の通り。

yokoyan:~/workspace/sample_app (password-reset) $ rails routes
                 Prefix Verb   URI Pattern                             Controller#Action
    password_resets_new GET    /password_resets/new(.:format)          password_resets#new
   password_resets_edit GET    /password_resets/edit(.:format)         password_resets#edit
        password_resets POST   /password_resets(.:format)              password_resets#create
     new_password_reset GET    /password_resets/new(.:format)          password_resets#new
    edit_password_reset GET    /password_resets/:id/edit(.:format)     password_resets#edit
         password_reset PATCH  /password_resets/:id(.:format)          password_resets#update
                        PUT    /password_resets/:id(.:format)          password_resets#update

【view】パスワード再設定画面へのリンクを追加

link_toメソッドでパスワード設定画面へのリンクを追加する。

      <%= f.label :password %>
      <%= link_to "(forgot password)", new_password_reset_path %>
      <%= f.password_field :password, class: 'form-control' %>

演習1

この時点で、テストスイートが greenになっていることを確認してみましょう。

テスト結果がgreenになることを確認。

yokoyan:~/workspace/sample_app (password-reset) $ rails test
Running via Spring preloader in process 1715
Started with run options --seed 24253

  46/46: [============================================================================================================================================================================================] 100% Time: 00:00:06, Time: 00:00:06

Finished in 6.48079s
46 tests, 198 assertions, 0 failures, 0 errors, 0 skips

演習2

表 12.1の名前付きルートでは、pathではなくurlを使うように記してあります。なぜでしょうか? 考えてみましょう。ヒント: アカウント有効化で行った演習 (11.1.1.1) と同じ理由です。

ブラウザのパスではなく、メール内のパスワード再設定URLからアクセスするため。

image.png

12.1.2 新しいパスワードの設定

本章での学び

【model】Userモデルの修正

以下、2つの属性をUserモデルに追加する。

  • reset_digest
    • パスワード再設定ダイジェスト
  • reset_sent_at
    • パスワード再設定用リンクの期限切れにするための、パスワード再設定メールの送信時間

image.png

【model】Userモデルのマイグレーション

Usersテーブルにカラムを追加するマイグレーションファイルを生成する。

yokoyan:~/workspace/sample_app (password-reset) $ rails generate migration add_reset_to_users reset_digest:string reset_sent_at:datetime
Running via Spring preloader in process 1842
Expected string default value for '--jbuilder'; got true (boolean)
      invoke  active_record
      create    db/migrate/20170703042006_add_reset_to_users.rb

DBのマイグレーションを実行する。

yokoyan:~/workspace/sample_app (password-reset) $ rails db:migrate
== 20170703042006 AddResetToUsers: migrating ==================================
-- add_column(:users, :reset_digest, :string)
   -> 0.0052s
-- add_column(:users, :reset_sent_at, :datetime)
   -> 0.0005s
== 20170703042006 AddResetToUsers: migrated (0.0058s) =========================

【view】パスワード再設定画面を作成する

form_forの呼び出しには、Active Recordを使用するのではなく、:password_resetsを使用する。

<% provide(:title, "Forgot password") %>
<h1>Forgot password</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(:password_reset, url: password_resets_path) do |f| %>
      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.submit "Submit", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>

演習1

リスト 12.4のform_forメソッドでは、なぜ@password_resetではなく:password_resetを使っているのでしょうか? 考えてみてください。

理由は以下の通り。

  • password_resetモデルが存在せず、@userのようなインスタンス変数に相当するものがないため、シンボル:password_resetを使用している。
    • form_for@userを指定すると、/usersに対するPOSTであると勝手に解釈するが、シンボルを使用する場合、form_forでは、具体的なリソース名とURLを指定しなければならない

form_for(:password_reset)の場合

<%= form_for(:password_resets, url: password_resets_path) do |f| %>

form_for(@user)の場合

<%= form_for(@user, url: yield(:url)) do |f| %>

12.1.3 createアクションでパスワード再設定

本章での学び

【controller】処理の概要

  • フォームから送信する
    • 送信内容が有効だった場合
      • メールアドレスをキーにしてユーザーをDBから見つける
      • パスワード再設定用トークンと、送信時のタイムスタンプでDBの属性を更新する
      • ルートURLにリダイレクトする
      • フラッシュメッセージを表示する
    • 送信内容が無効だった場合
      • newページを表示
      • flash.nowメッセージを表示する(レンダリングが終わっているページで特別にフラッシュメッセージを表示することができる。また、その後のリクエストが発生した時に消滅する)

【model】パスワード再設定用メソッドを追加する

class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token, :reset_token
  ・・・略・・・
  # パスワード再設定の属性を追加する
  def create_reest_digest
    self.reset_token = User.new_token
    update_attribute(:reset_digest, User.digest(reset_token))
    update_attribute(:reset_sent_at, Time.zone.now)
  end

  # パスワード再設定のメールを送信する
  def send_password_reset_email
    UserMailer.password_reset(self).deliver_now
  end

【controller】パスワード再設定用のcreateアクションを作成

  def create
    @user = User.find_by(email: params[:password_resets][:email].downcase)
    if @user
      @user.create_reest_digest
      @user.send_password_reset_email
      flash[:info] = "Email sent with password reset instructions"
      redirect_to root_url
    else
      flash.now[:danger] = "Email address not found"
      render 'new'
    end
  end

演習1

試しに有効なメールアドレスをフォームから送信してみましょう (図 12.6)。どんなエラーメッセージが表示されたでしょうか?

存在するメールアドレスを入力すると、Email address not foundと表示されてしまう。

image

演習2

コンソールに移り、先ほどの演習課題で送信した結果、(エラーと表示されてはいるものの) 該当するuserオブジェクトにはreset_digestとreset_sent_atがあることを確認してみましょう。また、それぞれの値はどのようになっていますか?

どちらも値がnilになっている。

yokoyan:~/workspace/sample_app (password-reset) $ rails console --sandbox
Running via Spring preloader in process 2185
Loading development environment in sandbox (Rails 5.0.0.1)
Any modifications you make will be rolled back on exit
>> user = User.first
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2017-06-20 04:07:12", updated_at: "2017-06-28 21:27:49", password_digest: "$2a$10$No5Z7APSsWojYME0Cm1POerj89GopLI23E2dN/9gyvt...", remember_digest: nil, admin: true, activation_digest: "$2a$10$ufdL0hGRXuEm9biMP3H0deF99cC.BpPrrobGbyL4FQ/...", activated: true, activated_at: "2017-06-20 04:07:12", reset_digest: nil, reset_sent_at: nil>
>> 
?> user.reset_digest
=> nil
>> 
?> user.reset_sent_at
=> nil

おわりに

11章と似た内容であるため、サクサク進めることができました。 DBのモデルを使わなくても、リソースは定義できることを学びました。 まだメール送信ができないため、次項で完成させます。