この記事の基本的な方針
deviseで実装したログイン機能は特にカスタマイズしなければ、1ページでEmailとパスワードの2項目を入力して登録完了です。
これを2ページに拡張し、1ページ目にEmailとパスワードに加えニックネームを、2ページ目でケータイ番号の入力を求めます。
この記事は、以下の「登録画面」「ログイン画面」「TOP画面」の3画面の簡単なアプリを元に拡張していきます。
【TOP画面(ログイン前)】 【TOP画面(ログイン後)】
【登録画面】
【ログイン画面】
手を動かしながら読みたいようでしたら、以下でこの3画面アプリを手に入れてください。
$ git clone -b 超最低限のRailsアプリ(messageコントローラVer) https://github.com/annaPanda8170/minimum_rails_application.git
$ bundle install
$ bundle exec rake db:create
$ bundle exec rake db:migrate
これ自体の作り方はこちら。
最終的に以下のように1ページ目にニックネームの項目が加わり、ケータイ番号の登録をする2ページ目が新たに作られます。
想定する読み手
既に一度Railsアプリをチュートリアルやスクール等で作ったことがある方を想定しております。
Mac使用で、パソコンの環境構築は完了していることが前提です。
具体的なコーディング手順
完成品GitHub(masterではなく一つのブランチなので注意して下さい)
①取り急ぎ1ページ目のみでニックネームカラムを登録項目として増やす
まず1ページのままでニックネーム入力項目を追加してみましょう。
通常、最初にテーブルを作成するときはrails g model <モデル名>
としたときに一緒にマイグレーションファイルも作られますが、今回はすでにあるテーブルにカラムを増やすだけなのでマイグレーションファイルのみを作ります。
$ rails g migration <クラス名>
を打ちます。
クラス名はなんでもいいです。rails g migration Aaa
でも問題ないですが、rails g migration AddColumn<テーブル名>
とするのが一般的です。今回はrails g migration AddColumnUsers
としましょう。
そして、作られたマイグレーションファイルを編集します。
class AddColumnUsers < ActiveRecord::Migration[5.2]
def change
add_column :users, :nickname, :string
end
end
カラムを加えるadd_column
メソッドを使います。第一引数がテーブル名、第二引数がカラム名、第三引数がデータ型です。「ユーザテーブル
にニックネームカラム
を文字列型
で加えます」ってことですね。極めて直感的です。余談ですが、コードを読むときは悶々とコードのまま黙読で理解しようとせずに、きっちりと日本語(自然言語)に直して読んでみることをオススメいたします。
そして
$ rails db:migrate
して、テーブルは完成です。一応データベースを見に行ってカラムが増えているかを確認しましょう。
続いてコントローラで、追加したカラムを受け入れられるようにします。ここはapp/controllers/messages_controller.rb
ではなく、app/controllers/application_controller.rb
を編集します。
class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:<追加したカラム名>])
end
end
難しいですが、それぞれのメソッドを完全に理解できなくでも大丈夫です。deviseのGitHubにそうしろと書いてあるのでそれに従います。掃除機を正しく使うのに掃除機を作れるようになる必要はありません。説明書をきちんと読みましょう。
今回はdevise_parameter_sanitizer.permit(:sign_up, keys: [:nickname])
ですね。
今回はnicknameなしでは登録できないようにしようと思うので、モデルを編集します。以下を追記します。
〜省略〜
validates :nickname ,presence: true
〜省略〜
あとはビューにニックネームを入力する欄を加えれば完了ですね。
あれ?ビューファイルはどこにありますか?
ありません。deviseをデフォルトのまま使うにはビューファイルとコントローラファイルの編集が必要ないので見えないところに隠されています。これを編集するには表に出させる必要があります。
$ rails g devise:views
すると、隠れていたビューファイル達が表に出てきます。当たり前ですが、ただ表に出てきただけなので編集しなければ何も変わりません。
以下を追記します。
<div class="field">
<%= f.label :nickname %><br />
<%= f.text_field :nickname %>
</div>
autofocus: true
に関しては適切な場所に一箇所置きます。詳細は省きます。
これで登録してみます。
大丈夫そうですね。
②2ページ目用モデル
2ページ目で入力されるケータイ番号をデータベースに保存するには、usersテーブルにカラムを追加するのではなく新たにテーブルを作ります。新たにテーブルを作るには、
$ rails g model cellphone
して、userテーブルに従属するので、
class CreateCellphones < ActiveRecord::Migration[5.2]
def change
create_table :cellphones do |t|
t.integer :cellphone
t.references :user
t.timestamps
end
end
end
と編集して、
$ rails db:migrate
です。慣れたもんです。
ここからモデルにバリデーションとアソシエーションを記述します。
まずapp/models/cellphone.rb
にvalidates :cellphone ,presence: true
を加えます。バリデーションです。これは簡単ですね。
続いて、アソシエーションです。cellphaneモデルとuserモデルを同時に見てください。
class Cellphone < ApplicationRecord
validates :cellphone ,presence: true
belongs_to :user, optional: true
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 :name ,presence: true
has_one :cellphone
end
belongs_to
メソッドとhas_one
メソッドが使われています。テーブル同士の関係が1対1の場合に使われます。1対1でどちらにどちらのメソッドを使うかというと、外部キーを置いていない側にbelongs_toを置きます。今回は後から作ったcellphoneの側のテーブルにuser_idカラムがあります。だから、cellphoneテーブルがuserテーブルに従属する(belongs)というイメージですね。そして逆側ににhas_oneを置くことになります。
optional: true
が付いていますが、これはウィザード形式を実装するには必須です。(コントローラでCellphoneモデルのみでインスタンスを生成する場面があるからのようです)
③2ページ目用コントローラ
隠れていたビューファイルとコントローラファイルのうち前者は①で表に出していますが、ここでは後者のコントローラファイルを表に出して編集していく必要があります。 表に出してみましょう。
$ rails g devise:controllers <コントローラのディレクトリ名>
コントローラのディレクトリ名はなんでも大丈夫ですが、通常モデル名の複数形なのでrails g devise:controllers users
にします。
さてここで話を整理しましょう。普通、登録画面に関するバックグラウンドの流れは、コントローラのnewアクションを通り、newのビューが表示され、そこのformにクライアントが情報が入力してsubmitが押されると、createアクションに来て、バリデーションを通過すればデータベースに登録するというものであります。図示するとこんな感じです。
newアクション → 登録画面 → createアクション → データベース
ここで2ページ目の登録画面を挟まなくてはならないので、
①newアクション → ①登録画面 → ①createと②newのアクション → ②登録画面 → ②createアクション → データベース
となります。
この①createと②newのアクション
と②createアクション
が難しいです。丁寧に行きましょう。
①createと②newのアクション
は、registrations_controller.rb
にもとからあるcreateアクション(コメントアウトされています)を使います。②createアクション
は新たに適当にアクションを作ります。
まず①createと②newのアクション
についてです。ここは①登録画面と②登録画面の橋渡しです。①登録画面の情報を受け取って変数(session変数という特別な変数)に格納して②登録画面に遷移します。
まず、最終的なcreateアクションの全体を掲載します。
# frozen_string_literal: true
class Users::RegistrationsController < Devise::RegistrationsController
〜省略〜
# POST /resource
def create
user = User.new(sign_up_params)
unless user.valid?
render :new and return
end
session[:registration] = {user: user.attributes}
session[:registration][:user][:password] = params[:user][:password]
@cellphone = user.build_cellphone
render :new_cellphone
end
〜省略〜
end
これを構築してゆきます。
まず、表に出したapp/controllers/users/registrations_controller.rb
のコメントアウトしているcreateアクションを復活させ、superを消してbinding.pry
をかませてみましょう。
# frozen_string_literal: true
class Users::RegistrationsController < Devise::RegistrationsController
〜省略〜
# POST /resource
def create
binding.pry
end
〜省略〜
end
こんな感じです。http://localhost:3000/users/sign_up
にアクセスして適当に値を埋めてSign up
ボタンを押すと停止するので、ターミナルを見ます。
まず①登録画面で入力された情報は
> sign_up_params
で取れます。私の場合、
=> {"email"=>"aaa@aaa", "password"=>"123123123", "password_confirmation"=>"123123123", "name"=>"annaPanda"}
こんな感じです。sign_up_params
とはdeviseが準備しているメソッドでしょう。
さてこのsign_up_params
を代入してUserモデルのインスタンスを生成してみましょう。
> aaa = User.new(sign_up_params)
=> #<User id: nil, email: "aaa@aaa", created_at: nil, updated_at: nil, nickname: "annaPanda">
これでバリデーションを通過できるかどうかはvalid?
メソッドを使います。
> aaa.valid?
User Exists (0.8ms) SELECT 1 AS one FROM `users` WHERE `users`.`email` = BINARY 'aaa@aaa' LIMIT 1
↳ (pry):3
=> true
trueって出ました。OKなようです。
続いて、②登録画面に引き継ぐべき値はattributes
メソッドで取れます。
> aaa.attributes
=> {"id"=>nil,
"email"=>"aaa@aaa",
"encrypted_password"=>"$2a$11$W/lFietoiCkM6Lj6bsuY2eTsvGSg7nakpLCisng73tpv5bp.8OGFu",
"reset_password_token"=>nil,
"reset_password_sent_at"=>nil,
"remember_created_at"=>nil,
"created_at"=>nil,
"updated_at"=>nil,
"nickname"=>"annaPanda"}
パスワードがencrypted_password
で暗号化されていますね。ですが引き継ぐ段階では元のパスワードが必要なのでそれを引き出します。params変数から、
> params[:user][:password]
=> "123123123"
このように取れるのはわかりますね。
さてここでsession変数を使います。普通変数などの値は、ブラウザが読み込まれるたびに一度クリアになります。これは"ステートレス"というHTTP通信の性質です。これに抗って値を保持できるのがsession変数です。
格納するsessionに付随する[:registration]や[:user]や[:password]の名前はなんでもOKですが今回はこうします。
あとはcellphone用のインスタンスを生成できればOKです。これにはnewメソッド出なく特別なbuild_<テーブル名>
メソッドを使います。
> aaa.build_cellphone
=> #<Cellphone:0x00007fc4c5d1db20 id: nil, cellphone: nil, user_id: nil, created_at: nil, updated_at: nil>
これらの情報を元にcreateアクションを構築すると上のようになるわけです。
再掲します。
# frozen_string_literal: true
class Users::RegistrationsController < Devise::RegistrationsController
〜省略〜
# POST /resource
def create
user = User.new(sign_up_params)
unless user.valid?
render :new and return
end
session[:registration] = {user: user.attributes}
session[:registration][:user][:password] = params[:user][:password]
@cellphone = user.build_cellphone
render :new_cellphone
end
〜省略〜
end
そしてこのコントローラが使用されるにはルーティングを変更する必要があります。
Rails.application.routes.draw do
get 'messages/index'
devise_for :users, controllers: {
registrations: 'users/registrations',
}
devise_scope :user do
get 'cellphones', to: 'users/registrations#new_cellphone'
post 'cellphones', to: 'users/registrations#create_cellphone'
end
root "messages#index"
end
devise_for :users
の先に加えられているのがcreateアクションへのルーティングです。これがないと上で書いたcreateアクションが使われません。
2ページ目用はdevise_scope
メソッドを使います。その先は普通にURIを設定しているだけですね。
②登録画面
のビューは以下です。ファイル自体もないので作ります。
<%= form_for @cellphone do |f| %>
<%= render "devise/shared/error_messages", resource: @cellphone %>
<div class="field">
<%= f.label :cellphone %><br />
<%= f.text_field :cellphone %>
</div>
<div class="actions">
<%= f.submit "Sign up" %>
</div>
<% end %>
<%= render "devise/shared/links" %>
続いて、②createアクション
です。名前はcreate_cellphoneです。
# frozen_string_literal: true
class Users::RegistrationsController < Devise::RegistrationsController
〜省略〜
def create_cellphone
@user = User.new(session[:registration]["user"])
@cellphone = Cellphone.new(cellphone_params)
unless @cellphone.valid?
flash.now[:alert] = @cellphone.errors.full_messages
render :new_cellphone and return
end
@user.build_cellphone(@cellphone.attributes)
@user.save
sign_in(:user, @user)
redirect_to root_path
end
protected
def cellphone_params
params.require(:cellphone).permit(:cellphone)
end
〜省略〜
end
最終的にこうなります。これを構築してゆきます。
createアクションと同じようにbinding.pry
をかませて見てみましょう。
# frozen_string_literal: true
class Users::RegistrationsController < Devise::RegistrationsController
〜省略〜
def create_cellphone
binding.pry
end
〜省略〜
end
②createアクション
のアクション名はcreate_<テーブル名>
でないとならないみたいですね。
まず、session変数に格納されている値を引き出してみましょう。
> session[:registration][:user]
=> nil
あれれ?なんででしょう。仕方ないので[:user]を抜いて見てみましょう。
> session[:registration]
=> {"user"=>
{"id"=>nil,
"email"=>"bbb@bbb",
"encrypted_password"=>"$2a$11$Gp6J1spbcfu4EiS6EfqJsuGjZV1GB9LZXpWTzTBNDTY0GBmsIDDgm",
"reset_password_token"=>nil,
"reset_password_sent_at"=>nil,
"remember_created_at"=>nil,
"created_at"=>nil,
"updated_at"=>nil,
"nickname"=>"annaPanda",
"password"=>"321321321"}}
取れました。"user"
になっているみたいなんでそれに変えてみましょう。
> session[:registration]["user"]
=> {"id"=>nil,
"email"=>"bbb@bbb",
"encrypted_password"=>"$2a$11$Gp6J1spbcfu4EiS6EfqJsuGjZV1GB9LZXpWTzTBNDTY0GBmsIDDgm",
"reset_password_token"=>nil,
"reset_password_sent_at"=>nil,
"remember_created_at"=>nil,
"created_at"=>nil,
"updated_at"=>nil,
"nickname"=>"annaPanda",
"password"=>"321321321"}
OKです。
次に②登録画面で入力されたケータイ番号を引き出してみましょう。
通常のストロングパラメータと同じですね。
> params.require(:cellphone).permit(:cellphone)
=> <ActionController::Parameters {"cellphone"=>"09011111111"} permitted: true>
さてコントローラ全体はこんな感じになります。
# frozen_string_literal: true
class Users::RegistrationsController < Devise::RegistrationsController
〜省略〜
def create
user = User.new(sign_up_params)
unless user.valid?
render :new and return
end
session[:registration] = {user: user.attributes}
session[:registration][:user][:password] = params[:user][:password]
@cellphone = user.build_cellphone
render :new_cellphone
end
def create_cellphone
@user = User.new(session[:registration]["user"])
@cellphone = Cellphone.new(cellphone_params)
unless @cellphone.valid?
flash.now[:alert] = @cellphone.errors.full_messages
render :new_cellphone and return
end
@user.build_cellphone(@cellphone.attributes)
@user.save
sign_in(:user, @user)
redirect_to root_path
end
protected
def cellphone_params
params.require(:cellphone).permit(:cellphone)
end
〜省略〜
end
これで完成です。
まだケータイ番号用に桁数を制限などはしていません。余裕があったら挑戦してみてください。