はじめに
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章 アカウントの有効化
新規登録の途中に、アカウントを有効化するステップを挟む。
- 新規登録
- 有効化トークンやダイジェストを関連付け
- 有効化トークンを含めたリンクをユーザー院メールを送信
- ユーザーがリンクをクリックすると、有効化する
アカウントを有効化する流れは、ユーザーの記憶と似ている。 ログイン/記憶トークン/アカウントの有効化/パスワードの再設定で似ている点は以下の通り。
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アクションとなる。
そのため、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テーブルを以下のように定義を変更する。
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での開発にもだんだん慣れてきたので、このままサクサク進めたいと思います。