devise and multiple "user" models
Asked Answered
O

4

77

I'm using rails 3.2 and devise 2.0 and I'm quite new to Rails.

Requirements

I'd like to achieve the following:

  • have 2 or more "user" models, eg. Member, Customer, Admin
  • all models share some required fields (eg. email and password)
  • each model may have some unique fields (eg. company for Customer only)
  • some fields may be shared but not have the same validation (eg. name is required for Customer but optional for Member)
  • all fields must be filled during the registration process, so the forms are different
  • the login form should be unique

Possible solutions

I googled and searched StackOverflow for quite a long time, but nothing seems right to me (I'm a Java guy, sorry :) and now I'm quite confused. Two solutions came up:

Single devise user

That's the most frequent answer. Just create the default devise User and create relations between Member-->User and Customer-->User. My concern here is how can I achieve a customized registration process for each model? I tried different things but all ended as a mess!

Multiple devise users

This solves the custom registration process, and seems right to me, but the unique login form is a blocker. I found an answer on SO (Devise - login from two model) which suggests to override Devise::Models::Authenticatable.find_for_authentication(conditions). That seems complicated (?) and since I'm new to rails, I'd like to know if that could work?

Thanks for your advice!

Olympe answered 27/2, 2012 at 21:50 Comment(0)
O
18

I found a way to go and I'm quite happy with it so far. I'll describe it here for others.

I went with the single "user" class. My problem was to achieve a customized registration process for each pseudo model.

model/user.rb:

class User < ActiveRecord::Base
  devise :confirmable,
       :database_authenticatable,
       :lockable,
       :recoverable,
       :registerable,
       :rememberable,
       :timeoutable,
       :trackable,
       :validatable

  # Setup accessible (or protected) attributes for your model
  attr_accessible :email, :password, :password_confirmation, :remember_me, :role

  as_enum :role, [:administrator, :client, :member]
  validates_as_enum :role
  ## Rails 4+ for the above two lines
  # enum role: [:administrator, :client, :member]

end

Then I adapted http://railscasts.com/episodes/217-multistep-forms and http://pastie.org/1084054 to have two registration paths with an overridden controller:

config/routes.rb:

get  'users/sign_up'   => 'users/registrations#new',        :as => 'new_user_registration'

get  'clients/sign_up' => 'users/registrations#new_client', :as => 'new_client_registration'
post 'clients/sign_up' => 'users/registrations#create',     :as => 'client_registration'

get  'members/sign_up' => 'users/registrations#new_member', :as => 'new_member_registration'
post 'members/sign_up' => 'users/registrations#create',     :as => 'member_registration'

controllers/users/registrations_controller.rb:

I created a wizard class which knows the fields to validate at each step

class Users::RegistrationsController < Devise::RegistrationsController

    # GET /resource/sign_up
    def new
        session[:user] ||= { }
        @user = build_resource(session[:user])
        @wizard = ClientRegistrationWizard.new(current_step)

        respond_with @user
    end

    # GET /clients/sign_up
    def new_client
        session[:user] ||= { }
        session[:user]['role'] = :client
        @user = build_resource(session[:user])
        @wizard = ClientRegistrationWizard.new(current_step)

        render 'new_client'
    end

    # GET /members/sign_up
    def new_member
      # same
    end

    # POST /clients/sign_up
    # POST /members/sign_up
    def create
        session[:user].deep_merge!(params[:user]) if params[:user]
        @user = build_resource(session[:user])
        @wizard = ClientRegistrationWizard.new(current_step)

        if params[:previous_button]
            @wizard.previous
        elsif @user.valid?(@wizard)
            if @wizard.last_step?
                @user.save if @user.valid?
            else
                @wizard.next
            end
        end

        session[:registration_current_step] = @wizard.current_step

        if @user.new_record?
            clean_up_passwords @user
            render 'new_client'
        else
            #session[:registration_current_step] = nil
            session[:user_params] = nil

            if @user.active_for_authentication?
                set_flash_message :notice, :signed_up if is_navigational_format?
                sign_in(:user, @user)
                respond_with @user, :location => after_sign_up_path_for(@user)
            else
                set_flash_message :notice, :"signed_up_but_#{@user.inactive_message}" if is_navigational_format?
                expire_session_data_after_sign_in!
                respond_with @user, :location => after_inactive_sign_up_path_for(@user)
            end
        end

    end

    private

    def current_step
        if params[:wizard] && params[:wizard][:current_step]
            return params[:wizard][:current_step]
        end
        return session[:registration_current_step]
    end

end

and my views are:

  • new.rb
  • new_client.rb including a partial according to the wizard step:
    • _new_client_1.rb
    • _new_client_2.rb
  • new_member.rb including a partial according to the wizard step:
    • _new_member_1.rb
    • _new_member_2.rb
Olympe answered 2/3, 2012 at 20:51 Comment(0)
H
74

Welcome aboard Java guy =), I hope you'll enjoy the Rails world. Simply, to solve your issue you have 2 solutions:

  1. For each user create a table in the database and corresponding model.
  2. Create a single table in the database and for each user type create a model. This is called single table inheritance (STI).

Which one to choose? It depends on the common attributes of the roles. If they are almost common (for example all have a name, email, mobile, ...) and a few attributes are different, I highly recommend the STI solution.

How to do the STI? 1. Simply create the the devise user model and table using the command rails generate devise User 2. Add a column named type with string datatype to the user table in the database using a migration. 3. For each user type create a model (for example rails g model admin) 4. Make the Admin class inherits from user model

class Admin < User
end

That's it you are done =) ... Yupeee

To create an admin run the command Admin.create(...) where the dots is the admin attributes for example the email, name, ...

I think this question could help you too

Heretical answered 27/2, 2012 at 22:7 Comment(4)
i do, thanks :) I've been quite successful for a newcomer until now... in fact, Customer have a lot of attributes and Member only a few. Is your solution a "single user" one or a "multiple user" one, that's not clear to me. If that's the later, do I need config.scoped_views = true?Olympe
Do they have many attributes in common? if so then I highly recommend the STI solution, if no then for each user type (Customer and Member in your case) create a table in the database and a corresponding model. Any way, you could choose the solution that fits your problem.Heretical
As far as I understood from you, it's the single user one (single user table in the database that stores all the users members and customers)Heretical
Thanks! Side note: I think rails g model also creates the migration files for that model. We're not creating a new table for admin in this case, so I think you just need to create a file called admin.rb in the model folder as opposed to using the rails generator.Mathre
M
28

I'm in similar shoes as you, after trying all sorts of approaches I went with a single User model, which would belong to polymorphic roles. This seems like the simplest way to achieve single-login.

The User model would contain the information specific to log-in only.

The Role model would store fields specific to each role, as well as other associations specific to the role.

New registrations would be customized for each user type (roles) via individual controllers, and then building nested attributes for the User.

class User < ActiveRecord::Base
    #... devise code ...
    belongs_to :role, :polymorphic => true
end

class Member < ActiveRecord::Base
    attr_accessible :name, :tel, :city  #etc etc....
    attr_accessible :user_attributes #this is needed for nested attributes assignment

    #model specific associations like  
    has_many :resumes

    has_one :user, :as => :role, dependent: :destroy
    accepts_nested_attributes_for :user
end 

Routes -- just regular stuff for the Member model.

resources :members
#maybe make a new path for New signups, but for now its new_member_path

Controller -- you have to build_user for nested attributes

#controllers/members_controller.rb
def new
    @member = Member.new
    @member.build_user
end

def create
    #... standard controller stuff
end

views/members/new.html.erb

<h2>Sign up for new members!</h2>
<%= simple_form_for @member do |f| %>

    # user fields
    <%= f.fields_for :user do |u| %>
      <%= u.input :email, :required => true, :autofocus => true %>
      <%= u.input :password, :required => true %>
      <%= u.input :password_confirmation, :required => true %>
    <% end %>

    # member fields
    <%= f.input :name %>
    <%= f.input :tel %>
    <%= f.input :city %>

    <%= f.button :submit, "Sign up" %>
<% end %>

I would like to point out that there is NO NEED to reach for nested_form gem; since the requirement is that User can only belong_to one type of Role.

Mythopoeic answered 14/8, 2012 at 16:28 Comment(3)
I ve used this similar approach in my project but after signup, it doesn't maintain the user session, so it always show up the sign in page which it shouldn't. Do you have any idea about it?Dreeda
From a database point of view is more appropriate to have has_one in the User model and belongs_to in the Member model, so that you can avoid the polymorphic relation. Is there any reason why you did the opposite?Freberg
@Freberg legit question, but polymorphic only applies to belongs_to. There are other solutions regarding "reverse polymorphic" but seems out of scope.Mythopoeic
O
18

I found a way to go and I'm quite happy with it so far. I'll describe it here for others.

I went with the single "user" class. My problem was to achieve a customized registration process for each pseudo model.

model/user.rb:

class User < ActiveRecord::Base
  devise :confirmable,
       :database_authenticatable,
       :lockable,
       :recoverable,
       :registerable,
       :rememberable,
       :timeoutable,
       :trackable,
       :validatable

  # Setup accessible (or protected) attributes for your model
  attr_accessible :email, :password, :password_confirmation, :remember_me, :role

  as_enum :role, [:administrator, :client, :member]
  validates_as_enum :role
  ## Rails 4+ for the above two lines
  # enum role: [:administrator, :client, :member]

end

Then I adapted http://railscasts.com/episodes/217-multistep-forms and http://pastie.org/1084054 to have two registration paths with an overridden controller:

config/routes.rb:

get  'users/sign_up'   => 'users/registrations#new',        :as => 'new_user_registration'

get  'clients/sign_up' => 'users/registrations#new_client', :as => 'new_client_registration'
post 'clients/sign_up' => 'users/registrations#create',     :as => 'client_registration'

get  'members/sign_up' => 'users/registrations#new_member', :as => 'new_member_registration'
post 'members/sign_up' => 'users/registrations#create',     :as => 'member_registration'

controllers/users/registrations_controller.rb:

I created a wizard class which knows the fields to validate at each step

class Users::RegistrationsController < Devise::RegistrationsController

    # GET /resource/sign_up
    def new
        session[:user] ||= { }
        @user = build_resource(session[:user])
        @wizard = ClientRegistrationWizard.new(current_step)

        respond_with @user
    end

    # GET /clients/sign_up
    def new_client
        session[:user] ||= { }
        session[:user]['role'] = :client
        @user = build_resource(session[:user])
        @wizard = ClientRegistrationWizard.new(current_step)

        render 'new_client'
    end

    # GET /members/sign_up
    def new_member
      # same
    end

    # POST /clients/sign_up
    # POST /members/sign_up
    def create
        session[:user].deep_merge!(params[:user]) if params[:user]
        @user = build_resource(session[:user])
        @wizard = ClientRegistrationWizard.new(current_step)

        if params[:previous_button]
            @wizard.previous
        elsif @user.valid?(@wizard)
            if @wizard.last_step?
                @user.save if @user.valid?
            else
                @wizard.next
            end
        end

        session[:registration_current_step] = @wizard.current_step

        if @user.new_record?
            clean_up_passwords @user
            render 'new_client'
        else
            #session[:registration_current_step] = nil
            session[:user_params] = nil

            if @user.active_for_authentication?
                set_flash_message :notice, :signed_up if is_navigational_format?
                sign_in(:user, @user)
                respond_with @user, :location => after_sign_up_path_for(@user)
            else
                set_flash_message :notice, :"signed_up_but_#{@user.inactive_message}" if is_navigational_format?
                expire_session_data_after_sign_in!
                respond_with @user, :location => after_inactive_sign_up_path_for(@user)
            end
        end

    end

    private

    def current_step
        if params[:wizard] && params[:wizard][:current_step]
            return params[:wizard][:current_step]
        end
        return session[:registration_current_step]
    end

end

and my views are:

  • new.rb
  • new_client.rb including a partial according to the wizard step:
    • _new_client_1.rb
    • _new_client_2.rb
  • new_member.rb including a partial according to the wizard step:
    • _new_member_1.rb
    • _new_member_2.rb
Olympe answered 2/3, 2012 at 20:51 Comment(0)
P
6

So what's wrong? Just run rails g devise:views [model_name], customize each registration forms and in config/initializer/devise.rb just put config.scoped_views = true.

Puberty answered 27/2, 2012 at 22:1 Comment(5)
thanks but AFAIK that doesn't solve the single login form, does it?Olympe
sorry, I just read "After doing so, you will be able to have views based on the role like "users/sessions/new" and "admins/sessions/new". If no view is found within the scope, Devise will use the default view at "devise/sessions/new". You can also use the generator to generate scoped views:" so that should work...Olympe
I tried to go that way. I now have routes for members and customers, some views are shared, some are not. But the single login form issue remains... How can a unique form be used to authenticate a member OR a customer ???Olympe
Have you turned on scoped_views option in config/initializers/devise.rb? It is disableb by default to improve performance.Puberty
@c_inconnu found this link gist.github.com/jeremyw/5319386 to share a login form for multiple modelsEgalitarian

© 2022 - 2024 — McMap. All rights reserved.