コミュ障だから明日が僕らをよんだって返事もろくにしなかった

何かを創る人に憧れたからブログをはじめたんだと思うよ

社会のボトムズがRailsに手を出す #11

これまでのあらすじ

そろそろこのRailsチュートリアルも終盤に差し掛かってきました。思えばいろんなことがありました。あんなことやこんなこといろいろあったなぁ…。

前回記事
inujini.hatenablog.com


あらすじっていうか感想ですね…。

個人的に前回内容で終わりにしてもいいんじゃないかなって思ってるんですけど続きがあるので第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上で確認できるの便利っすね。このあたりで初めて個人開発でもテスト機能があるって便利だなって思った。
f:id:andron:20190120173055p:plain

そんでテストはこう。

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

まあテストコードの方は特に言うことがない。メールの設定とかはお好み応じて下記参照

参考
Railsアプリを設定する | Rails ガイド


ユーザ登録機能も併せて変更して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 テスト


できたら以下機能を実装させます。

f:id:andron:20190120230125p:plainf:id:andron:20190120230129p:plain

# 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: falsenon_activeつくってこんな感じで。ユーザリストに表示されてたらごめんなさいって感じで雑に……。

  test "有効化されていないものを表示しない" do
    log_in_as(@user) 
    assert_not @non_active.activated?
    get user_path(@non_active)
    assert_redirected_to root_url
  end



第十一章の感想

とまあ、そんな感じの内容です。今回は削れるところは削ってって方針でまとめたけどもまあ長いね。機能的にはメール関連のことやってるのが新たな学びとなります。

そういうわけで次回へ続く。