これまでのあらすじ
そろそろこのRailsチュートリアルも終盤に差し掛かってきました。思えばいろんなことがありました。あんなことやこんなこといろいろあったなぁ…。
あらすじっていうか感想ですね…。
個人的に前回内容で終わりにしてもいいんじゃないかなって思ってるんですけど続きがあるので第11章やっていきます。前回は管理者権限機能の実装とか、ユーザ周りの削除とか更新とかの機能追加をしてきました。前回無駄にボリューミーだったんですけど、やってることは大したことやってなかったというね……。悲しい。
アカウントの有効化
タイトルからだとなんのこっちゃなんですけど、登録した後にメール飛ばしてそこのURL踏ませてからアカウントとして認識する例のアレの実装のことだそうです。
そんなわけでまたDB周りをいじって有効フラグ的なやつを追加していきます。まずは、とにもかくにも色々生成します。
$ rails generate controller AccountActivations $ rails generate migration add_activation_to_users activation_digest:string activated:boolean activated_at:datetime $ rails db:migrate
編集箇所 | 説明 |
---|---|
config/routes.rb | アカウント有効化のリソース追加 |
app/controllers/account_activations_controller.rb | Editアクションの作成 |
app/models/user.rb | アカウント有効化のコードを追加 |
db/migrate/[timestamp]_add_activation_to_users.rb | マイグレーション設定 |
db/seeds.rb | サンプルユーザの有効化設定 |
test/fixtures/users.yml | fixtureユーザの有効化設定 |
いじるとこがまたごちゃごちゃしてきたのでまた一言メモを残しておきます。
そういえば未だにWindowsで以下コマンドをうまく実行させる方法わかんないんですけど、どうやるんですかね?手動でDB削除するのはなんか違う気がするんだよね……。
$ rails db:migrate:reset $ rails db:seed
これ毎回手動でいじるせいでDBに整合性合わなくなって、それを手動で修正するとかいう頭の悪いことやって時間食ってる……。
# メーラーの作成 $ rails generate mailer UserMailer account_activation password_reset
生成コード | 説明 |
---|---|
app/views/user_mailer/account_activation.text.erb | アカウント有効化テキストメールビュー |
app/views/user_mailer/account_activation.html.erb | アカウント有効化HTMLメールビュー |
app/mailers/application_mailer.rb | Applicationメーラー |
app/mailers/user_mailer.rb | Userメーラー |
test/mailers/user_mailer_test.rb | Userメーラーテスト |
test/mailers/previews/user_mailer_preview.rb | アカウント有効化のプレビュー |
参考
Action Mailer の基礎 | Rails ガイド
これでメール送るテンプレートができるそうな。自動生成されるテストコードを利用すればWeb上で確認できるの便利っすね。このあたりで初めて個人開発でもテスト機能があるって便利だなって思った。
そんでテストはこう。
require 'test_helper' class UserMailerTest < ActionMailer::TestCase test "account_activation" do user = users(:michael) user.activation_token = User.new_token mail = UserMailer.account_activation(user) assert_equal "Account activation", mail.subject assert_equal [user.email], mail.to assert_equal ["noreply@example.com"], mail.from assert_match user.name, mail.body.encoded assert_match user.activation_token, mail.body.encoded assert_match CGI.escape(user.email), mail.body.encoded end # パスワードリセット時の設定 ※残してはいるが本内容と関係していないので特に手を加えていない test "password_reset" do mail = UserMailer.password_reset assert_equal "Password reset", mail.subject assert_equal ["to@example.org"], mail.to assert_equal ["noreply@example.com"], mail.from assert_match "Hi", mail.body.encoded end end
まあテストコードの方は特に言うことがない。メールの設定とかはお好み応じて下記参照
ユーザ登録機能も併せて変更してEditアクションで有効化するようにしてみる
見出しが長くなってしまった……。今のままだとメール飛ばしてアカウント有効化する機能だけで、画面機能との紐づけができないのでそれやっていきます。
とりあえず、authenticated?
メソッドを改良していきます。
# app/models/user.rb class User < ApplicationRecord ### いろいろ略 ### # トークンがダイジェストと一致したらtrueを返す def authenticated?(attribute, token) digest = send("#{attribute}_digest") return false if remember_digest.nil? BCrypt::Password.new(remember_digest).is_password?(remember_token) end end
これで引数が増えたのでauthenticated?
絡みの実装を修正していきます。このチュートリアルだと特になんの言及もないんですけど、コンソール上でgrep
できる機能ほしい。それっぽいことできるメソッドはRubyにあるらしいんだけどね……。
修正箇所 | 説明 |
---|---|
app/helpers/sessions_helper.rb | current_userメソッド |
test/models/user_test.rb | テスト |
できたら以下機能を実装させます。
# app/controllers/account_activations_controller.rb class AccountActivationsController < ApplicationController def edit user = User.find_by(email: params[:email]) if user && !user.activated? && user.authenticated?(:activation, params[:id]) user.activate log_in user flash[:success] = "アカウントが有効化されました" redirect_to user else flash[:danger] = "アクティベーションリンクが無効です" redirect_to root_url end end end
# app/controllers/sessions_controller.rb class SessionsController < ApplicationController ### 略 ### def create @user = User.find_by(email: params[:session][:email].downcase) if @user && @user.authenticate(params[:session][:password]) # ユーザーログイン後にユーザー情報のページにリダイレクトする if @user.activated? log_in @user params[:session][:remember_me] == '1' ? remember(@user) : forget(@user) redirect_back_or @user else message = "アカウントが有効化されていません" message += "メールを確認して有効化してください" flash[:warning] = message redirect_to root_url end else # エラーメッセージを作成する flash.now[:danger] = 'パスワードかメールが無効です' render 'new' end end ### 略 ### end
そんでテストはこう。
# test/integration/users_signup_test.rb require 'test_helper' class UsersSignupTest < ActionDispatch::IntegrationTest def setup ActionMailer::Base.deliveries.clear end test "invalid signup information" do get signup_path assert_no_difference 'User.count' do post users_path, params: { user: { name: "", email: "user@invalid", password: "foo", password_confirmation: "bar" } } end assert_template 'users/new' assert_select 'div#error_explanation' assert_select 'div.field_with_errors' end test "valid signup information with account activation" do get signup_path assert_difference 'User.count', 1 do post users_path, params: { user: { name: "Example User", email: "user@example.com", password: "password", password_confirmation: "password" } } end assert_equal 1, ActionMailer::Base.deliveries.size user = assigns(:user) assert_not user.activated? log_in_as(user) assert_not is_logged_in? get edit_account_activation_path("invalid token", email: user.email) assert_not is_logged_in? get edit_account_activation_path(user.activation_token, email: 'wrong') assert_not is_logged_in? get edit_account_activation_path(user.activation_token, email: user.email) assert user.reload.activated?, user.activated follow_redirect! assert_template 'users/show' assert is_logged_in? end end
そんで演習のリファクタリングはこうと思われ。
# app/models/user.rb class User < ApplicationRecord attr_accessor :remember_token, :activation_token before_save :downcase_email before_create :create_activation_digest ### 略 ### # update_columnsを使用し問い合わせ回数を減らす # アカウントを有効にする def activate update_columns(activated: true, activated_at: Time.zone.now) end # 有効化用のメールを送信する def send_activation_email UserMailer.account_activation(self).deliver_now end private # メールアドレスをすべて小文字にする def downcase_email self.email = email.downcase end # 有効化トークンとダイジェストを作成および代入する def create_activation_digest self.activation_token = User.new_token self.activation_digest = User.digest(activation_token) end end
class UsersController < ApplicationController ### 略 ### # 有効なユーザーだけを表示する def index @users = User.where(activated: true).paginate(page: params[:page]) end def show @user = User.find(params[:id]) redirect_to root_url and return unless @user.activated? end ### 略 ### end
結合テストは…、activated: false
のnon_active
つくってこんな感じで。ユーザリストに表示されてたらごめんなさいって感じで雑に……。
test "有効化されていないものを表示しない" do log_in_as(@user) assert_not @non_active.activated? get user_path(@non_active) assert_redirected_to root_url end
第十一章の感想
とまあ、そんな感じの内容です。今回は削れるところは削ってって方針でまとめたけどもまあ長いね。機能的にはメール関連のことやってるのが新たな学びとなります。
そういうわけで次回へ続く。