複数のメールアドレスを持つユーザーモデルをDeviseでさくさくっと作ろうと思ってたら思ったより手間取ったのでメモ
DeviseのインストールとUserモデルの作成
コマンド
$ rails new demo --skip-bundle
$ cd demo
$ echo "gem 'devise'" >> Gemfile
$ rails g devise:install
config/environments/development.rb
Rails.application.configure do
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#↓追加
config.action_mailer.default_url_options = { host: 'localhost:3000' }
end
Userモデルの作成
コマンド
$ rails g devise User
Emailモデルの作成
コマンド
$ rails g model Email user:belong_to address:strings
Usersテーブルの編集
db/migrate/20xxxxxxxxxxxx_devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[5.0]
def change
create_table :users do |t|
## Database authenticatable
#↓コメントアウトor削除
#t.string :email, null: false, default: ""
#↓追加
t.belongs_to :main_email, null: true
#↑ここでハマった、emailカラムはDeviseがメソッドオーバーライドしてるらしく
#'t.belong_to :email' としてしまうと保存ができない
#後、'null: false'にしてしまうとデッドロックが発生し保存できない
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
end
#↓コメントアウトor削除
#add_index :users, :email, unique: true
end
end
Emailsテーブルの編集
db/migrate/20xxxxxxxxxxxx_create_emails.rb
class CreateEmails < ActiveRecord::Migration[5.0]
def change
create_table :emails do |t|
#↓行末に', null: false'を追加
t.belongs_to :user, null: false
t.string :address
t.timestamps
end
#↓追加
add_index :emails, :address, unique: true
end
end
Userモデルの編集
app/model/user.rb
class User < ApplicationRecord
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#↓以下追加
belongs_to :main_email, class_name: Email, optional: true
accepts_nested_attributes_for :main_email
has_many :emails, dependent: :destroy, index_errors: true
accepts_nested_attributes_for :emails
attr_accessor :email
after_save :set_main_email
after_save :add_main_email_to_emails
#↓deviseにemailのチェックをさせないようにする
def email_required?
false
end
def email_changed?
false
end
protected
#↓deviseのユーザー取得メソッドをオーバーライド
def self.find_first_by_auth_conditions(warden_conditions)
conditions = warden_conditions.dup
if email = conditions.delete(:email)
#↓emailをemailsに持つユーザーを返す
where(conditions).joins(:emails).find_by('emails.address': email)
#↓main_emailでしかログインさせたくない場合はこれ
#where(conditions).joins(:main_email).find_by('main_email.address': email)
else
where(conditions).first
end
end
#↓main_emailが設定されていない時にemailsの最初の値を設定する
def set_main_email
return false if self.emails.empty?
if self.main_email.nil?
self.main_email = self.emails.first
self.save
end
end
#↓main_emailが変更された時にemailsに追加
#今回の実装では必要ないが一応
def add_main_email_to_emails
return false if self.main_email.nil?
if self.main_email.changed? && !self.emails.include?(self.main_email)
self.emails << self.main_email
self.save
end
end
end
Emailモデルの編集
app/model/email.rb
class Email < ApplicationRecord
has_one :user
belongs_to :user, optional: true
#↑Rails5からbelong_toカラムは必須になったが保存してからでないと
#IDが定まらず保存できないので'optional: true'を指定
end
ApplicationControllerの編集
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
#↓以下追加
before_action :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters
#Devise4での書き方 古いバージョンでは
#'devise_parameter_sanitizer.for'を使う
devise_parameter_sanitizer.permit(:sign_up) do |u|
u.permit([emails_attributes: [:address]], :password, :password_confirmation, :remember_me)
end
devise_parameter_sanitizer.permit(:sign_in) do
|u| u.permit([emails_attributes: [:address]], :password, :remember_me)
end
devise_parameter_sanitizer.permit(:account_update) do
|u| u.permit([emails_attributes: [:address]], :password, :password_confirmation, :current_password)
end
end
end
Viewsの編集
コマンド
$ rails g devise:views user
config/initializers/devise.rb
#↓223行目付近 コメントアウトのまま放置or削除
# config.scoped_views = false
config.scoped_views = true
#↑追加
app/views/users/registrations/*.html.erb
<h2>Sign up</h2>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= devise_error_messages! %>
#↓追加
<% @user.emails.new if @user.emails.empty? %>
<%= f.fields_for :emails, @user.emails || @user.emails.new do |e| %>
<div class="field">
<%= e.label :email %><br />
<%= e.email_field :address %>
</div>
<% end %>
#↓削除
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true %>
</div>
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
これでおしまい。