某フリマサイトのコピーサイトをチーム開発しており、deviseを用いたウィザード形式でのユーザー新規登録機能を実装しましたが、大分苦戦しました。。。
ということで、備忘録もかねて以下にまとめます!
*ページ内、クレジットカード登録のページございますが、今回はpay.jpなどのGemは使用しておりません。単純に情報を登録するだけにしております。
以下の記事にてPay.jpを使用してますので、興味ある方はご参照ください。
https://qiita.com/Tatsu88/items/23fe4b83d0ff8d78709d
本記事該当Github: https://github.com/Tatsu88-Tokyo/freemarket_sample_60ce
#ウィザード形式とは?
そもそもウィザード形式とは、なんぞや?というところから説明します。
ウィザード形式とは、サイト利用者に一つずつ質問や設定項目を提示し、対話的に処理を進める操作方式のことを指します。
イメージとしては、以下のような感じです。
#ウィザード形式でのユーザー新規登録機能を実装する時の概要
まず、会員登録の画面でユーザー情報を入力させ、それをsessionに保持させておきます。
次に住所情報を登録する画面で住所情報入力してもらい、それをまたsessionに保持させておきます。
そして次の支払い方法を登録する画面で支払い方法を入力してもらい、最後のステップでsessionに保持していたユーザー情報と、それに関連する住所情報・支払いをテーブルに保存します。
この時注意する必要があるのは、ウィザードの各ページごとにテーブルを分ける必要があるということです。ウィザードが切り替わるときに、都度バリデーションのチェックを行うからです。
例えば2ページ目で記入する住所情報のカラムが、adressesテーブルではなく、usersテーブルに存在すると、1ページ目から2ページ目に切り替わるときに住所情報が入力されていないのでバリデーションに引っかかってしまいます。
#該当テーブル(モデル)
今回の実装で使用したテーブルは以下のようになっております。(該当箇所のみ抜き出してます)
Column | Type | Options |
---|---|---|
nickname | string | null: false |
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をインストールします。
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を編集します。
#例(nicknameだけ、カラム追加した場合)
protected
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:nickname])
end
##モデル編集
作成したモデルを編集してvalidationなどを必要に応じてかけましょう。
#例(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)に対して、エラーが発生するので、必ず記述しましょう。
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
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
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を使用しております。
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"
.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"
.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アクションを編集します。
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が落ちたときに自動的にセッションを切る機能を持ってます。 - 次の住所情報登録で使用するインスタンスを生成、当該ページへ遷移すること
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
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を削除すること
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のコントローラーが呼ばれてしまっています。
そこで、以下のように記述をして、ルーティングを設定しましょう。
Rails.application.routes.draw do
devise_for :users, controllers: {
registrations: 'users/registrations',
}
end
###2.devise_scopeを使用し、Deviseで複数のモデルを扱うことができるようにする
今回複数モデル(addressとcreditcard)を使用しているため、devise_scopeを使用し、各モデルのルートを設定しましょう。
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