LoginSignup
33

More than 3 years have passed since last update.

[HowTo]deviseを用いたウィザード形式でユーザー新規登録機能を実装

Last updated at Posted at 2020-01-26

某フリマサイトのコピーサイトをチーム開発しており、deviseを用いたウィザード形式でのユーザー新規登録機能を実装しましたが、大分苦戦しました。。。
ということで、備忘録もかねて以下にまとめます!
*ページ内、クレジットカード登録のページございますが、今回はpay.jpなどのGemは使用しておりません。単純に情報を登録するだけにしております。
以下の記事にてPay.jpを使用してますので、興味ある方はご参照ください。
https://qiita.com/Tatsu88/items/23fe4b83d0ff8d78709d

本記事該当Github: https://github.com/Tatsu88-Tokyo/freemarket_sample_60ce

ウィザード形式とは?

そもそもウィザード形式とは、なんぞや?というところから説明します。
ウィザード形式とは、サイト利用者に一つずつ質問や設定項目を提示し、対話的に処理を進める操作方式のことを指します。
イメージとしては、以下のような感じです。
d103dd1497df787e7afc015db8e1b3cb.png

ウィザード形式でのユーザー新規登録機能を実装する時の概要

まず、会員登録の画面でユーザー情報を入力させ、それをsessionに保持させておきます。
次に住所情報を登録する画面で住所情報入力してもらい、それをまたsessionに保持させておきます。
そして次の支払い方法を登録する画面で支払い方法を入力してもらい、最後のステップでsessionに保持していたユーザー情報と、それに関連する住所情報・支払いをテーブルに保存します。

この時注意する必要があるのは、ウィザードの各ページごとにテーブルを分ける必要があるということです。ウィザードが切り替わるときに、都度バリデーションのチェックを行うからです。
例えば2ページ目で記入する住所情報のカラムが、adressesテーブルではなく、usersテーブルに存在すると、1ページ目から2ページ目に切り替わるときに住所情報が入力されていないのでバリデーションに引っかかってしまいます。

該当テーブル(モデル)

今回の実装で使用したテーブルは以下のようになっております。(該当箇所のみ抜き出してます)

Column Type Options
nickname string null: false
email string null: false, unique:true
password string null: false
first_name string null: false
last_name string null: false
first_name_kana string null: false
last_name_kana string null: false
birthday integer null: false

Association
- has_one: address
- has_one : credit_cards

Column Type Options
user_id references null: false, foreign_key: true
card_company string null: false
card_number string null: false
card_year integer null: false
card_month integer null: false
card_pass integer null: false

Association
- belongs_to : user

Column Type Options
user_id references null: false, foreign_key: true
postal_code varcar(7) null: false
prefecture integer null: false
city string null: false
address string null: false
apartment string

Association
- belongs_to : user

実装の流れ

今回の実装の流れは以下のようになっております。
- gemインストール
- 各モデルの準備
- view編集
- controller編集
- route編集

gemインストール

まず、今回の機能実装にあたり"devise"を使用するので、gemをインストールします。

gemfile
gem 'devise'
ターミナル
$ bundle install

#アプリケーション内でdeviseを使えるようにするため、下記のコマンドを実行します。
$ rails g devise:install

各モデルの準備

それでは、今回使用するモデルの準備をしていきます。
今回、userモデルはdeviseを使用して登録されるため、devise管理下のモデルを作成します。

ターミナル
#User(devise管理下)作成
$ rails g devise user

#マイグレーションファイル編集後、migrateを忘れずに。
$ rails db:migrate

これでマイグレーションファイルが出来上がりますので、上記テーブルを元にマイグレーションファイルを編集します。
*emailとpasswordはdeviseが元々持っているので、それ以外を追記します。

application_controller編集

追加したカラムがある場合は、以下のようにapplication_controllerを編集します。

app/controllers/application_controller.rb
#例(nicknameだけ、カラム追加した場合)
  protected
    def configure_permitted_parameters
      devise_parameter_sanitizer.permit(:sign_up, keys: [:nickname])
    end

モデル編集

作成したモデルを編集してvalidationなどを必要に応じてかけましょう。

app/models/user.rb
#例(nicknameだけ、カラム追加した場合)
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  validates :nickname ,presence: true
end

*その他モデルについては、通常通り「$ rails g model モデル名」で作成し、
マイグレーションファイルなどを編集しましょう。

以下、各モデルの記述です。
ポイント
- creditcardモデルとaddressモデルはenumを使用しています。
- creditcardモデルとaddressモデルはoptional: trueを記述してます。
*この記述がないと、creditcardモデルとaddressモデル内のカラム(user_id)に対して、エラーが発生するので、必ず記述しましょう。

models/creditcard
class Creditcard < ApplicationRecord
  belongs_to :user, optional: true
  validates :card_number, :card_company, :card_year, :card_month,:card_pass, presence: true

  enum card_company:{
    VISA:1,Mastercard:2,セゾンカード:3,JCB:4,アメリカンエキスプレス:5,ダイナーズ:6,ディスカバー:7
  }
end
models/address
class Address < ApplicationRecord
  belongs_to :user, optional: true
  validates :postal_code, :prefecture, :city, :address, presence: true

  enum prefecture:{
    北海道:1,青森県:2,岩手県:3,宮城県:4,秋田県:5,山形県:6,福島県:7,
    茨城県:8,栃木県:9,群馬県:10,埼玉県:11,千葉県:12,東京都:13,神奈川県:14,
    新潟県:15,富山県:16,石川県:17,福井県:18,山梨県:19,長野県:20,
    岐阜県:21,静岡県:22,愛知県:23,三重県:24,
    滋賀県:25,京都府:26,大阪府:27,兵庫県:28,奈良県:29,和歌山県:30,
    鳥取県:31,島根県:32,岡山県:33,広島県:34,山口県:35,
    徳島県:36,香川県:37,愛媛県:38,高知県:39,
    福岡県:40,佐賀県:41,長崎県:42,熊本県:43,大分県:44,宮崎県:45,鹿児島県:46,沖縄県:47
  }
end
model/user
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  validates :nickname, presence: true
  has_one :address
  has_one :creditcard
end

viewの編集(user)

それでは、各viewを作成しましょう。
今回はrailsのverが古いのでform_forを使用しております。

完成イメージ1e6f07b58919c11ee9f7ab262ba0cf5b.png
6d28ee1c1e57038bf074ce08cad0eeff.png

viewファイルは以下のように作成しています。

view
.single-container
  %header.single-header
    %h1.single-header__logo
      =link_to image_tag("fmarket_logo_red.svg",width:"134",height:"36"), root_path  
    %nav.single-header__progress
      %ol
        %li.single-header__progress__text--active{ id: "first" }
          会員情報
          .single-header__progress__round--red
        %li.single-header__progress__text
          お届け先住所入力
          .single-header__progress__round     
        %li.single-header__progress__text
          支払い方法
          .single-header__progress__round    
        %li.single-header__progress__text{ id: "end" }
          完了
          .single-header__progress__round
.single-main__container__form__frame
  = form_for(@user, url: user_registration_path) do |f|
    = render "devise/shared/error_messages", resource: @user
    .form-group
      = f.label :ニックネーム
      %span.form-group__require 必須
      = f.text_field :nickname, {placeholder: "例) メルカリ太郎",class:'form-group__input'}
    .form-group
      = f.label :メールアドレス
      %span.form-group__require 必須
      = f.email_field :email, {autofocus: true, autocomplete: "email", placeholder: "PC・携帯どちらでも可",class:'form-group__input'}
    .form-group
      = f.label :パスワード
      %span.form-group__require 必須
      = f.password_field :password,{autocomplete: "new-password",placeholder: "7文字以上の半角英数字",class:'form-group__input',id:"password"}
      %p.form-group__info ※ 英字と数字の両方を含めて設定してください
      .form-password-revelation-toggle
        .checkbox-default
          %input#reveal_password{type: "checkbox",class:"icon-check"}
          %label{for: "reveal_password"} パスワードを表示する
        .form-password-revelation-revealed-password-container
          %span.form-password-revelation-revealed-password
    .form-group
      %label.form-group-text-title 本人確認
      %p.form-group__info 安心・安全にご利用いただくために、お客さまの本人情報の登録にご協力ください。他のお客さまに公開されることはありません。
    .form-group
      = f.label :"お名前(全角)"
      %span.form-group__require 必須
    = f.text_field :last_name, {placeholder:"例) 山田",class:'form-group__input--half'}
    = f.text_field :first_name, {placeholder:"例) 彩",class:'form-group__input--half'}
    .form-group
      = f.label :"お名前カナ(全角)"
      %span.form-group__require 必須
    = f.text_field :last_name_kana, {placeholder:"例) ヤマダ",class:'form-group__input--half'}
    = f.text_field :first_name_kana, {placeholder:"例) アヤ",class:'form-group__input--half'}
    .form-group
      = f.label :"生年月日"
      %span.form-group__require 必須
      %br
      .birthday-select-wrap
        != sprintf(f.date_select(:birthday, prefix:'birthday',with_css_classes:'XXXXX', prompt:"--",use_month_numbers:true, start_year:Time.now.year, end_year:1900, date_separator:'%s'),'年','月')+'日'
      .clearfix
    .form-group
      %p.form-group__text--center
        「次へ進む」のボタンを押すことにより、
        = link_to "利用規約", "#" , target:"_blank"
        に同意したものとみなします
      = f.submit '次へ', class: "btn-default btn-red", url: "address_path"
  = render "registration_footer"

viewの編集(address)

d43f2201d8164b186cc41b855da68991.png

view
.single-container
  %header.single-header
    %h1.single-header__logo
      = link_to "#" do
        =image_tag("fmarket_logo_red.svg")
    %nav.single-header__progress
      %ol
        %li.single-header__progress__text{ id: "first" }
          会員情報
          .single-header__progress__round--red
        %li.single-header__progress__text--active
          お届け先住所入力
          .single-header__progress__round--red
        %li.single-header__progress__text
          支払い方法
          .single-header__progress__round
        %li.single-header__progress__text{ id: "end" }
          完了
          .single-header__progress__round
  %main.single-main
    %section.single-main__container
      %h2.single-main__container__title
        住所入力
      .single-main__container__form__frame
        =form_for(@address, url: addresses_path, method: :post) do |f|
          =render "devise/shared/error_messages", resource: @address
          .form-group
            = f.label :郵便番号
            %span.form-group__require 必須
            = f.text_field :postal_code,{autofocus: true, placeholder: "例)123-4567", class: 'form-group__input'}
          .form-group
            = f.label :都道府県
            %span.form-group__require 必須
            = f.select :prefecture, Address.prefectures.keys, {}, {class: 'form-group__input'}
          .form-group
            = f.label :市町村
            %span.form-group__require 必須
            = f.text_field :city, autofocus: true, placeholder: "例)札幌市", class: 'form-group__input'
          .form-group
            = f.label :番地
            %span.form-group__require 必須
            = f.text_field :address, autofocus: true, placeholder: "例)青葉1-1-1", class: 'form-group__input'
          .form-group
            = f.label :建物名
            %span.form-group__optional 任意
            = f.text_field :apartment, autofocus: true, placeholder: "例)柳ビル103", class: 'form-group__input'
          .form-group
            = f.submit '次へ', class: "btn-default btn-red", url: "creditcard_path"
  = render "registration_footer"

viewの編集(creditcard)

612316d11d112f7fb4f670ef83a440ec.png

view
.single-container
  %header.single-header
    %h1.single-header__logo
      = link_to root_path do
        =image_tag("fmarket_logo_red.svg")
    %nav.single-header__progress
      %ol
        %li.single-header__progress__text{ id: "first" }
          会員情報
          .single-header__progress__round--red
        %li.single-header__progress__text
          お届け先住所入力
          .single-header__progress__round--red
        %li.single-header__progress__text--active
          支払い方法
          .single-header__progress__round--red
            .single-header__progress__round--red-long{ id: "long" }
        %li.single-header__progress__text{ id: "end" }
          完了
          .single-header__progress__round
  %main.single-main
    %section.single-main__container
      %h2.single-main__container__title
        支払い方法

.single-main__container__form
  .single-main__container__form__frame
    = form_for(@creditcard, url: creditcards_path,method: :post) do |f|
      = render "devise/shared/error_messages", resource: @creditcard
      .form-group
        = f.label :カード番号
        %span.form-group__require 必須
        = f.text_field :card_number, {placeholder: "半角数字のみ", class: "form-group__input"}
      .form-group
        = f.label :カード会社
        %span.form-group__require 必須
        = f.select :card_company, Creditcard.card_companies.keys, {}, {class: 'form-group__input'}
        %ul.signup__card--list
          %li.icon--visa
            = image_tag("visa.svg", id:"icon--visa")
          %li.icon--master
            = image_tag("master-card.svg", id:"icon--master")
          %li.icon--saison
            = image_tag("saison-card.svg", id:"icon--saison")
          %li.icon--jcb
            = image_tag("jcb.svg", id:"icon--jcb")
          %li.icon--americanexpress
            = image_tag("american_express.svg", id:"icon--americanexpress")
          %li.icon--diners
            = image_tag("dinersclub.svg", id:"icon--diners")
          %li.icon--discover
            = image_tag("discover.svg", id:"icon--discover")
      .form-group
        = f.label :有効期限
        %span.form-group__require 必須
        %br
        = f.select :card_year, options_for_select(["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]), {}, {class: "form-group__input--half"}
        = f.label :, class: "form-group__card--year-and-month"
        = f.select :card_month, options_for_select(["20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30"]), {}, {class: "form-group__input--half"}
        = f.label :, class: "form-group__card--year-and-month"
      .form-group
        = f.label :セキュリティコード, class: "label"
        %span.form-group__require 必須
        = f.text_field :card_pass, placeholder: "カード背面4桁もしくは3桁の番", class: "form-group__input"

      .form-group__add
        .form-group__add--question ?
        %p.form-group__text--right--blue
          カード裏面の番号とは?
      .form-group
        = f.submit "登録する", class: "btn-default btn-red", url: "creditcards_path", method: :post
  = render "/registration/registration_footer"

  f.submit "Sign up"

controllerの準備+編集

今回は、devise管理下のuserモデルを使用しているため、controllerもdeviseの管理下に作成します。

ターミナル
$ rails g devise:controllers users

作成が完了しましたら、該当のコントローラーを編集します。

controller編集(user登録)

まずはuserの登録ができるようにコントローラを編集します。
ファイル内に元々記述されているコメントアウトされている箇所は、すでにDevise::RegistrationsControllerで定義されているものです。
コメントアウト部分は外して上書きすることができます(メソッドのオーバーライド)。
また、superはスーパークラス(今回であればDevise)のメソッドを呼び出しています。
*スーパークラスについての詳細は、まずはこちらのRuby公式ドキュメントを確認しましょう。
https://docs.ruby-lang.org/ja/2.5.0/doc/spec=2fcall.html#super

それでは、まずnewアクションを編集します。

app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController

#以下、superクラスの呼び出しを削除して、User.newで新規インスタンスを生成することもできますが、最初にdeviseとUserモデルが紐づくように設定してあるので、superで呼び出してもdeviseは同様の操作を行ってくれます。

 def new
    super
 end

続いて、createアクションを編集します。
ポイントは以下の通りです。
- 1ページ目で入力した情報のバリデーションチェック
- 1ページで入力した情報をsessionに保持させること
*passwordはuser.attributeに入っていないので、個別で入れてあげます。
*session["devise.regist_data"]の"devise.regist_data"、こちらは基本的には変数ですが、
"devise.regist_data"はdeviseが落ちたときに自動的にセッションを切る機能を持ってます。
- 次の住所情報登録で使用するインスタンスを生成、当該ページへ遷移すること

app/controllers/users/registrations_controller.rb
 def create
    @user = User.new(sign_up_params)
    unless @user.valid?
      flash.now[:alert] = @user.errors.full_messages
      render :new and return
    end
    session["devise.regist_data"] = {user: @user.attributes}
  #この記述でもOKです。 session["devise.regist_data"] [user]= @user.attributes
    session["devise.regist_data"][:user]["password"] = params[:user][:password]
    @address = @user.build_address
    render :new_address
 end

controller編集(address登録)

アドレスの登録は基本的にuserと同様ですが、userの記述でaddressのインスタンス変数は作成しているので、
createアクションのみ記述します。
また、protected内に、引数の設定をしましょう。
- buildメソッド:
今回、以下のようにbuildメソッドを使用しています。
これは、親モデルに属する子モデルのインスタンスを新たに生成したい場合に使うメソッドとなっており、
このメソッドを使用することで、外部キーに値が入った状態でインスタンスが生成できます。
(親モデルと子モデルは、アソシエーション設定あり)
今回は以下のようにaddressをuserの子モデルとして、使用するために、以下のように記述しており、
addressにはuser_idのカラムに数字が与えられます。
例) @address = @user.build_address

app/controllers/users/registrations_controller.rb
  def create_address
    @user = User.new(session["devise.regist_data"]["user"])
    @address = Address.new(address_params)
    unless @address.valid?
      flash.now[:alert] = @address.errors.full_messages
      render :new_address and return
    end
    @user.build_address(@address.attributes)
    session["address"] = @address.attributes
    @creditcard = @user.build_creditcard
    render :new_credit_card
  end

  protected

  def address_params
    params.require(:address).permit(:address,:postal_code, :prefecture, :city,:apartment)
  end

controller編集(creditcard登録)

creditcardの登録に関しても、addressの記述でcreditcardのインスタンス変数は作成しているので、
createアクションのみ記述します。
ポイントは以下の通りです。
- バリデーションチェック
- バリデーションチェックが完了した情報と、sessionで保持していた情報とあわせ、ユーザー情報として保存すること
ログインをすること
- sessionを削除すること

app/controllers/users/registrations_controller.rb
  def create_creditcard
    @user = User.new(session["devise.regist_data"]["user"])
    @address = Address.new(session["address"])
    @creditcard = Creditcard.new(creditcard_params)
    unless @creditcard.valid?
      flash.now[:alert] = @creditcard.errors.full_messages
      render :new_credit_card and return
    end
    @user.build_address(@address.attributes)
    @user.build_creditcard(@creditcard.attributes)
    @user.save
    sign_in(:user, @user)
  end

routeの編集

view、controllerの準備ができましたので、routeの編集をします。
この編集は2つのポイントがあります。
1.devise userモデルにrouteを設定
2.devise_scopeを使用し、Deviseで複数のモデルを扱うことができるようにする

1.devise userモデルにrouteを設定

devise管理下のusersコントローラを作成がしましたが、記述を変更しないと、現状deviseのコントローラーが呼ばれてしまっています。
そこで、以下のように記述をして、ルーティングを設定しましょう。

route.rb
Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations',
}
end

2.devise_scopeを使用し、Deviseで複数のモデルを扱うことができるようにする

今回複数モデル(addressとcreditcard)を使用しているため、devise_scopeを使用し、各モデルのルートを設定しましょう。

route.rb
Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations',
    }
  devise_scope :user do
    get 'addresses', to: 'users/registrations#new_address'
    post 'addresses', to: 'users/registrations#create_address'
    get 'creditcards', to: 'users/registrations#new_creditcard'
    post 'creditcards', to: 'users/registrations#create_creditcard'
  end
end

これで今回使用するルーティングの設定が完了しました。

以上でdeviseを用いたウィザード形式でユーザー新規登録機能が実装されております。

最後までご覧いただき、ありがとうございました!
今後も学習した事項に関してQiitaに投稿していきますので、よろしくお願いします!
記述に何か誤りなどございましたら、お手数ですが、ご連絡いただけますと幸いです。

devise_parameter_sanitizerメソッドとはなにか
https://aliceblog1616.com/devise_parameter_sanitizer%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%A8%E3%81%AF%EF%BC%9F/

rails devise完全入門!結局deviseって何ができるの?
https://www.sejuku.net/blog/13378

buildメソッドについて
https://qiita.com/tsuchinoko_run/items/d671ea840bc0bfa90186

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33