紙一重の積み重ね

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

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

はじめに

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

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

動作環境

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

11章 アカウントの有効化

新規登録の途中に、アカウントを有効化するステップを挟む。

  • 新規登録
  • 有効化トークンやダイジェストを関連付け
  • 有効化トークンを含めたリンクをユーザー院メールを送信
  • ユーザーがリンクをクリックすると、有効化する

アカウントを有効化する流れは、ユーザーの記憶と似ている。 ログイン/記憶トークン/アカウントの有効化/パスワードの再設定で似ている点は以下の通り。

image

11.1 AccountActivationsリソース

本章での学び

  • セッション機能を使用して、アカウントの有効化という作業を「リソース」としてモデル化する。
  • 有効化トークンや、有効化ダイジェストをUsersテーブルに追加する。
  • 有効化用リンクにアクセスして、有効化ステータスに更新するのは、PATCHリクエスト+editアクションではなく、GETリクエスト+editアクションとなる。
    • RESTのルールと一部異なる点に注意。
    • これは、有効化リンクをクリックした際に、ブラウザからGETリクエストが送信されるため。

11.1.1 AccountActivationsコントローラ

本章での学び

【controller】AccountActivationsリソースの作成

rails generate controller AccountActivationsで自動作成する。

yokoyan:~/workspace/sample_app (account-activation) $ rails generate controller AccountActivations
Running via Spring preloader in process 2786
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/account_activations_controller.rb
      invoke  erb
      create    app/views/account_activations
      invoke  test_unit
      create    test/controllers/account_activations_controller_test.rb
      invoke  helper
      create    app/helpers/account_activations_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/account_activations.coffee
      invoke    scss
      create      app/assets/stylesheets/account_activations.scss

【routes】editアクションの名前付きルートの修正

先述したとおり、有効化用リンクにアクセスして、有効化ステータスに更新するのは、PATCHリクエスト+editアクションではなく、GETリクエスト+editアクションとなる。

image

そのため、editアクションの名前付きルートを変更する。

  resources :account_activations , only: [:edit]

演習1

現時点でテストスイートを実行すると greenになることを確認してみましょう。

現時点ではgreenになることを確認。

yokoyan:~/workspace/sample_app (account-activation) $ rails test
Running via Spring preloader in process 3434
Started with run options --seed 45995

  42/42: [============================================================================================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.22669s
42 tests, 179 assertions, 0 failures, 0 errors, 0 skips

演習2

表 11.2の名前付きルートでは、pathではなくurlを使うように記してあります。なぜでしょうか? 考えてみましょう。ヒント: 私達はこれからメールで名前付きルートを使います。

ユーザー有効化の際に、メール内に有効化URLを表示するため。 URLクリック時に、GETリクエストを発行して、editアクションでユーザーの状態を有効化に更新する。

11.1.2 AccountActivationのデータモデル

本章での学び

有効化メールで使用する、一意の有効化トークンを作成する。 平文ではなく、ハッシュ化しておく。悪意のあるユーザーがDBにアクセスし、有効化トークンを盗み出して、本来のユーザーが使う前にそのトークンを使ってログインしてしまうことを防ぐ。

【DB】Usersテーブルに有効化ダイジェスト、有効化ステータス、有効化日時を追加する

Usersテーブルを以下のように定義を変更する。

image

rails generate migrationでマイグレーションファイルを作成する。

yokoyan:~/workspace/sample_app (account-activation) $ rails generate migration add_activation_to_users activation_digest:string activated:boolean activated_at:datetime
Running via Spring preloader in process 3732
Expected string default value for '--jbuilder'; got true (boolean)
      invoke  active_record
      create    db/migrate/20170618221238_add_activation_to_users.rb

activatedに、dafault:falseを追加する。

    add_column :users, :activated, :boolean, dafault: false

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

yokoyan:~/workspace/sample_app (account-activation) $ rails db:migrate
== 20170618221238 AddActivationToUsers: migrating =============================
-- add_column(:users, :activation_digest, :string)
   -> 0.0052s
-- add_column(:users, :activated, :boolean, {:dafault=>false})
   -> 0.0004s
-- add_column(:users, :activated_at, :datetime)
   -> 0.0003s
== 20170618221238 AddActivationToUsers: migrated (0.0061s) ====================

【model】activationトークンのコールバック

有効化トークンと、有効化ダイジェストは、ユーザー登録のために必ず必要になるため、Userオブジェクトが作成される前に生成しておく必要がある。

before_createコールバックを使用して実現する。 before_createは、Userオブジェクトが生成された時だけに呼び出される。 (before_saveの場合、Userオブジェクトの登録や更新時に呼び出される)

また、有効化トークンはDBに保存しない仮の値となるため、処理内で取り扱うためにattr_accessor:activation_tokenとして追加しておく。

まとめると下記の通り。

  • 有効化トークンをattr_accessorに追加
  • 有効化トークンと有効化ダイジェストの代入処理をprivateで定義する
  • before_createで、有効化トークンと有効化ダイジェストの代入処理の呼び出す
class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token
  before_create :create_activation_digest
・・・略・・・
  private
  
    # 有効化トークンと有効化ダイジェストを作成、代入する
    def create_activation_digest
      self.activation_token = User.new_token
      self.activation_digest = User.digest(activation_token)
    end

また、メールアドレスを小文字に変換している、before_saveコールバックの部分をリファクタリングする。 具体的には、ブロックを渡すのではなく、メソッド参照にする。

class User < ApplicationRecord
  before_save :downcase_email
・・・略・・・
  private
    # メールアドレスをすべて小文字にする
    def downcase_email
      self.email = email.downcase
    end

【seeds】サンプルユーザの有効化

DBで登録するサンプルユーザーを有効化しておく。 activated: trueと、activated_at: Time.zone.nowを追加する。

User.create!( name:   "Example User",
              email:  "example@railstutorial.org",
              password: "foobar",
              password_confirmation: "foobar",
              admin: true,
              activated: true,
              activated_at: Time.zone.now)

99.times do |n|
  name = Faker::Name.name
  email = "example-#{n+1}@railstutorial.org"
  password = "password"
  User.create!( name:   name,
                email:  email,
                password: password,
                password_confirmation: password,
                activated: true,
                activated_at: Time.zone.now)
end

Time.zone.nowは、UTCで時刻を返すようだ。 日本とは9時間ずれていますね。

yokoyan:~/workspace/sample_app (account-activation) $ rails console
Running via Spring preloader in process 1721
Loading development environment (Rails 5.0.0.1)
>> Time.zone.now
=> Tue, 20 Jun 2017 04:02:45 UTC +00:00

【fixture】テストユーザーの有効化

テスト時に使用するユーザーを有効化しておく。

michael:
  ・・・省略・・・
  activated: true
  activated_at: <%= Time.zone.now %>

【DB】データベースの初期化と、サンプルデータの登録

db:migrate:restで初期化。

yokoyan:~/workspace/sample_app (account-activation) $ rails db:migrate:reset
Dropped database 'db/development.sqlite3'
Dropped database 'db/test.sqlite3'
Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'
== 20170417215343 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0014s
== 20170417215343 CreateUsers: migrated (0.0014s) =============================

== 20170423125906 AddIndexToUsersEmail: migrating =============================
-- add_index(:users, :email, {:unique=>true})
   -> 0.0014s
== 20170423125906 AddIndexToUsersEmail: migrated (0.0015s) ====================

== 20170424041610 AddPasswordDigestToUsers: migrating =========================
-- add_column(:users, :password_digest, :string)
   -> 0.0011s
== 20170424041610 AddPasswordDigestToUsers: migrated (0.0013s) ================

== 20170514215307 AddRememberDigestToUsers: migrating =========================
-- add_column(:users, :remember_digest, :string)
   -> 0.0007s
== 20170514215307 AddRememberDigestToUsers: migrated (0.0012s) ================

== 20170614215124 AddAdminToUsers: migrating ==================================
-- add_column(:users, :admin, :boolean, {:default=>false})
   -> 0.0008s
== 20170614215124 AddAdminToUsers: migrated (0.0013s) =========================

== 20170618221238 AddActivationToUsers: migrating =============================
-- add_column(:users, :activation_digest, :string)
   -> 0.0006s
-- add_column(:users, :activated, :boolean, {:dafault=>false})
   -> 0.0004s
-- add_column(:users, :activated_at, :datetime)
   -> 0.0005s
== 20170618221238 AddActivationToUsers: migrated (0.0017s) ====================

db:seedでサンプルデータを登録する。

yokoyan:~/workspace/sample_app (account-activation) $ rails db:seed

演習1

本項での変更を加えた後、テストスイートが green のままになっていることを確認してみましょう。

テストコードがgreenになることを確認。

yokoyan:~/workspace/sample_app (account-activation) $ rails test
Running via Spring preloader in process 1776
Started with run options --seed 16844

  42/42: [=================================================================================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.39674s
42 tests, 179 assertions, 0 failures, 0 errors, 0 skips

演習2

コンソールからUserクラスのインスタンスを生成し、そのオブジェクトからcreate_activation_digestメソッドを呼び出そうとすると (Privateメソッドなので) NoMethodErrorが発生することを確認してみましょう。また、そのUserオブジェクトからダイジェストの値も確認してみましょう。

NoMethodErrorが発生することを確認。

yokoyan:~/workspace/sample_app (account-activation) $ rails console
Running via Spring preloader in process 1878
Loading development environment (Rails 5.0.0.1)
>> user = User.first
  User Load (0.3ms)  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-20 04:07:12", 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">
>> user.create_activation_digest
NoMethodError: private method `create_activation_digest' called for #<User:0x000000036e6ce0>
Did you mean?  created_at_change
               created_at_previous_change
               created_at
               created_at_was
               created_at_changed?
               created_at_will_change!
               restore_activation_digest!
        from /usr/local/rvm/gems/ruby-2.3.0/gems/activemodel-5.0.0.1/lib/active_model/attribute_methods.rb:430:in `method_missing'
・・・以下略・・・

ダイジェストの値を確認。 なお、seedでサンプルデータを登録しているため、firstのユーザ(micheal)のactivatedはtrueになっている。

?> user.activation_digest
=> "$2a$10$ufdL0hGRXuEm9biMP3H0deF99cC.BpPrrobGbyL4FQ/QOr0USsdKy"
>> 
?> user.activated?
=> true
>> 

演習3

リスト 6.34で、メールアドレスの小文字化にはemail.downcase!という (代入せずに済む) メソッドがあることを知りました。このメソッドを使って、リスト 11.3のdowncase_emailメソッドを改良してみてください。また、うまく変更できれば、テストスイートは成功したままになっていることも確認してみてください。

downcase_emailメソッドを改良する。

  private
    # メールアドレスをすべて小文字にする
    def downcase_email
      # self.email = email.downcase
      email.downcase!
    end

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

yokoyan:~/workspace/sample_app (account-activation) $ rails test
Running via Spring preloader in process 1915
Started with run options --seed 12498

  42/42: [=================================================================================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.71963s
42 tests, 179 assertions, 0 failures, 0 errors, 0 skips

おわりに

remember_tokenの仕組みを応用して、activate_tokenの仕組みを実装することができました。 仕組みを簡単に流用できるのもオブジェクト指向ならではの強みですね。(別にこれはRubyに限ったことではありませんが) Railsでの開発にもだんだん慣れてきたので、このままサクサク進めたいと思います。