Search code examples
ruby-on-railsdevise

Multiple user models with Ruby On Rails and devise to have separate registration routes but one common login route


First, I've searched intensely with Google and Yahoo and I've found several replies on topics like mine, but they all don't really cover what I need to know.

I've got several user models in my app, for now it's Customers, Designers, Retailers and it seems there are yet more to come. They all have different data stored in their tables and several areas on the site they're allowed to or not. So I figured to go the devise+CanCan way and to try my luck with polymorphic associations, so I got the following models setup:

class User < AR
  belongs_to :loginable, :polymorphic => true
end

class Customer < AR
  has_one :user, :as => :loginable
end

class Designer < AR
  has_one :user, :as => :loginable
end

class Retailer < AR
  has_one :user, :as => :loginable
end

For the registration I've got customized views for each different User type and my routes are setup like this:

devise_for :customers, :class_name => 'User'
devise_for :designers, :class_name => 'User'
devise_for :retailers, :class_name => 'User'

For now the registrations controller is left as standard (which is "devise/registrations"), but I figured, since I got different data to store in different models I'd have to customize this behaviour as well!?

But with this setup I got helpers like customer_signed_in? and designer_signed_in?, but what I'd really need is a general helper like user_signed_in? for the areas on the site that are accessible to all users, no matter which user type.

I'd also like a routes helper like new_user_session_path instead of the several new_*type*_session_path and so on. In fact all I need to be different is the registration process...

So I was wondering IF THIS IS THE WAY TO GO for this problem? Or is there a better/easier/less must-customize solution for this?


Solution

  • 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.