Multiple user models with Ruby On Rails and devise to have separate registration routes but one common login route
Okay, so I worked it through and came to the following solution.
I needed to costumize devise a little bit, but it's not that complicated.
The User model
# user.rb
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
attr_accessible :email, :password, :password_confirmation, :remember_me
belongs_to :rolable, :polymorphic => true
end
The Customer model
# customer.rb
class Customer < ActiveRecord::Base
has_one :user, :as => :rolable
end
The Designer model
# designer.rb
class Designer < ActiveRecord::Base
has_one :user, :as => :rolable
end
So the User model has a simple polymorphic association, defining if it's a Customer or a Designer.
The next thing I had to do was to generate the devise views with rails g devise:views
to be part of my application. Since I only needed the registration to be customized I kept the app/views/devise/registrations
folder only and removed the rest.
Then I customized the registrations view for new registrations, which can be found in app/views/devise/registrations/new.html.erb
after you generated them.
<h2>Sign up</h2>
<%
# customized code begin
params[:user][:user_type] ||= 'customer'
if ["customer", "designer"].include? params[:user][:user_type].downcase
child_class_name = params[:user][:user_type].downcase.camelize
user_type = params[:user][:user_type].downcase
else
child_class_name = "Customer"
user_type = "customer"
end
resource.rolable = child_class_name.constantize.new if resource.rolable.nil?
# customized code end
%>
<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f| %>
<%= my_devise_error_messages! # customized code %>
<div><%= f.label :email %><br />
<%= f.email_field :email %></div>
<div><%= f.label :password %><br />
<%= f.password_field :password %></div>
<div><%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation %></div>
<% # customized code begin %>
<%= fields_for resource.rolable do |rf| %>
<% render :partial => "#{child_class_name.underscore}_fields", :locals => { :f => rf } %>
<% end %>
<%= hidden_field :user, :user_type, :value => user_type %>
<% # customized code end %>
<div><%= f.submit "Sign up" %></div>
<% end %>
<%= render :partial => "devise/shared/links" %>
For each User type I created a separate partial with the custom fields for that specific User type, i.e. Designer --> _designer_fields.html
<div><%= f.label :label_name %><br />
<%= f.text_field :label_name %></div>
Then I setup the routes for devise to use the custom controller on registrations
devise_for :users, :controllers => { :registrations => 'UserRegistrations' }
Then I generated a controller to handle the customized registration process, copied the original source code from the create
method in the Devise::RegistrationsController
and modified it to work my way (don't forget to move your view files to the appropriate folder, in my case app/views/user_registrations
class UserRegistrationsController < Devise::RegistrationsController
def create
build_resource
# customized code begin
# crate a new child instance depending on the given user type
child_class = params[:user][:user_type].camelize.constantize
resource.rolable = child_class.new(params[child_class.to_s.underscore.to_sym])
# first check if child instance is valid
# cause if so and the parent instance is valid as well
# it's all being saved at once
valid = resource.valid?
valid = resource.rolable.valid? && valid
# customized code end
if valid && resource.save # customized code
if resource.active_for_authentication?
set_flash_message :notice, :signed_up if is_navigational_format?
sign_in(resource_name, resource)
respond_with resource, :location => redirect_location(resource_name, resource)
else
set_flash_message :notice, :inactive_signed_up, :reason => inactive_reason(resource) if is_navigational_format?
expire_session_data_after_sign_in!
respond_with resource, :location => after_inactive_sign_up_path_for(resource)
end
else
clean_up_passwords(resource)
respond_with_navigational(resource) { render_with_scope :new }
end
end
end
What this all basically does is that the controller determines which user type must be created according to the user_type
parameter that's delivered to the controller's create
method by the hidden field in the view which uses the parameter by a simple GET-param in the URL.
For example:
If you go to /users/sign_up?user[user_type]=designer
you can create a Designer.
If you go to /users/sign_up?user[user_type]=customer
you can create a Customer.
The my_devise_error_messages!
method is a helper method which also handles validation errors in the associative model, based on the original devise_error_messages!
method
module ApplicationHelper
def my_devise_error_messages!
return "" if resource.errors.empty? && resource.rolable.errors.empty?
messages = rolable_messages = ""
if !resource.errors.empty?
messages = resource.errors.full_messages.map { |msg| content_tag(:li, msg) }.join
end
if !resource.rolable.errors.empty?
rolable_messages = resource.rolable.errors.full_messages.map { |msg| content_tag(:li, msg) }.join
end
messages = messages + rolable_messages
sentence = I18n.t("errors.messages.not_saved",
:count => resource.errors.count + resource.rolable.errors.count,
:resource => resource.class.model_name.human.downcase)
html = <<-HTML
<div id="error_explanation">
<h2>#{sentence}</h2>
<ul>#{messages}</ul>
</div>
HTML
html.html_safe
end
end
UPDATE:
To be able to support routes like /designer/sign_up
and /customer/sign_up
you can do the following in your routes file:
# routes.rb
match 'designer/sign_up' => 'user_registrations#new', :user => { :user_type => 'designer' }
match 'customer/sign_up' => 'user_registrations#new', :user => { :user_type => 'customer' }
Any parameter that's not used in the routes syntax internally gets passed to the params hash. So :user
gets passed to the params hash.
So... that's it. With a little tweeking here and there I got it working in a quite general way, that's easily extensible with many other User models sharing a common User table.
Hope someone finds it useful.
I didn't manage to find any way of commenting for the accepted answer, so I'm just gonna write here.
There are a couple of things that don't work exactly as the accepted answer states, probably because it is out of date.
Anyway, some of the things that I had to work out myself:
- For the UserRegistrationsController,
render_with_scope
doesn't exist any more, just userender :new
The first line in the create function, again in the UserRegistrationsController doesn't work as stated. Just try using
# Getting the user type that is send through a hidden field in the registration form. user_type = params[:user][:user_type] # Deleting the user_type from the params hash, won't work without this. params[:user].delete(:user_type) # Building the user, I assume. build_resource
instead of simply build_resource
. Some mass-assignment error was coming up when unchanged.
- If you want to have all the user information in Devise's current_user method, make these modifications:
class ApplicationController < ActionController::Base
protect_from_forgery
# Overriding the Devise current_user method
alias_method :devise_current_user, :current_user
def current_user
# It will now return either a Company or a Customer, instead of the plain User.
super.rolable
end
end
I was following the above instructions and found out some gaps and that instructions were just out of date when I was implementing it.
So after struggling with it the whole day, let me share with you what worked for me - and hopefully it will save you few hours of sweat and tears
First of all, if you are not that familiar with RoR polymorphism, please go over this guide: http://astockwell.com/blog/2014/03/polymorphic-associations-in-rails-4-devise/ After following it you will have devise and user users models installed and you will be able to start working.
After that please follow Vapire's great tutorial for generating the views with all the partails.
What I found most frustrating was that dut to using the latest version of Devise (3.5.1), RegistrationController refused to work. Here is the code that will make it work again:
def create meta_type = params[:user][:meta_type] meta_type_params = params[:user][meta_type] params[:user].delete(:meta_type) params[:user].delete(meta_type) build_resource(sign_up_params) child_class = meta_type.camelize.constantize child_class.new(params[child_class.to_s.underscore.to_sym]) resource.meta = child_class.new(meta_type_params) # first check if child intance is valid # cause if so and the parent instance is valid as well # it's all being saved at once valid = resource.valid? valid = resource.meta.valid? && valid # customized code end if valid && resource.save # customized code yield resource if block_given? if resource.persisted? if resource.active_for_authentication? set_flash_message :notice, :signed_up if is_flashing_format? sign_up(resource_name, resource) respond_with resource, location: after_sign_up_path_for(resource) else set_flash_message :notice, :"signed_up_but_#{resource.inactive_message}" if is_flashing_format? expire_data_after_sign_in! respond_with resource, location: after_inactive_sign_up_path_for(resource) end else clean_up_passwords resource set_minimum_password_length respond_with resource end end end
and also add these overrides so that the redirections will work fine:
protected def after_sign_up_path_for(resource) after_sign_in_path_for(resource) end def after_update_path_for(resource) case resource when :user, User resource.meta? ? another_path : root_path else super end end
In order that devise flash messages will keep working you'll need to update
config/locales/devise.en.yml
instead of the overridden RegistraionsControlloer by UserRegistraionsControlloer all you'll need to do is add this new section:user_registrations: signed_up: 'Welcome! You have signed up successfully.'
Hope that will save you guys few hours.