Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

setting Devise after_sign_in_path_for with multiple models

I have some basic experience with Devise in a previous application that had only one Devise model. However, I am rewriting it all from scratch to have expanded functionality, and in the second iteration I think it best to have two separate user models (patient_user and staff_user).

I am aware of CanCan and Rolify, and will be using those for one of the models but not the other.

My problem is with setting the after_sign_in_path_for and redirecting each model to a different "home screen".

I have set each model to a separate after_sign_up_path and that works beautifully.

class RegistrationsController < Devise::RegistrationsController
  protected

  # Creating separate after_sign_up_paths for patient_user and staff_user

  def after_sign_up_path_for(patient_user)
    flash[:notice] = 'Welcome! You have signed up successfully.'
    privacy_agreement_path
  end

  # Add an after_sign_up path for staff_user
  def after_sign_up_path_for(staff_user)
    flash[:notice] = 'Welcome! You have signed up successfully.'
    dashboard_path
  end

end

Apparently after_sign_in_path_for should be defined in Application Controller though rather than in Sessions Controller.

Stack Overflow question clarifying this difference

Here's my best attempt:

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  def after_sign_in_path_for(resource)
    case resource
    when patient_user
      privacy_agreement_path  
    when staff_user
      dashboard_path
    end
  end

end

which gives error:

undefined local variable or method `patient_user' for #<Devise::SessionsController:0x00000109a40e48>

If I capitalise the case select conditions then it seems to recognise the variable but I get a completely different error:

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  def after_sign_in_path_for(resource)
    case resource
    when Patient_user
      privacy_agreement_path  
    when Staff_user
      dashboard_path
    end
  end

end

which gives error

  Circular dependency detected while autoloading constant Patient_user (RuntimeError)
  ./app/controllers/application_controller.rb:11:in `after_sign_in_path_for'

I have tried a lot of Googling and looked at various other Devise and Circular Dependency problems but have not been able to find a work around, I guess I'm not good enough at Devise to know what I'm doing.

The one other thing I tried was making patient_user and staff_user separate after_sign_in_path_for calls in Application controller

#application_controller.rb
  def after_sign_in_path_for(patient_user)
     privacy_agreement_path  
  end

  def after_sign_in_path_for(staff_user)
     dashboard_path  
  end

This works for staff_user, but going to /patient_users/sign_in and giving a valid username and password instead redirects to dashboard_path (not privacy_agreement_path).

The problem seems to be focussed around the use of "resource" and the conditional statement that is intended to redirect "patient_user" accounts to "privacy_agreement_path" and "staff_user" accounts to "dashboard_path". This works well for after_sign_up_path in RegistrationsController, but not in after_sign_in_path in ApplicationController.

Other files

#routes.rb
devise_for :patient_users, :controllers => { :registrations => :registrations }
devise_for :staff_users, :controllers => { :registrations => :registrations }

——

#config/initializers/devise.rb
  # https://stackoverflow.com/questions/8320398/second-devise-model-not-using-generated-views
  # Workaround for having multiple Devise models, used the second answer
  config.scoped_views = true

Any help would be much appreciated.

EDIT:

I tried Vapire's solution:

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  def after_sign_in_path_for(resource)
    # check for the class of the object to determine what type it is
    case resource.class
    when PatientUser
      privacy_agreement_path  
    when StaffUser
      dashboard_path
    end
  end
end

but this started up the error:

      undefined method `staff_user_url' for #<Devise::SessionsController:0x000001047a0198> (NoMethodError)

With a bit of Googling I found this discussion on Devise github, with a total lack of consensus on whether this is some kind of bug or just a poor implementation.

I followed the suggested solution though, which was to update routes.rb

#routes.rb
  devise_for :patient_users, :controllers => { :registrations => :registrations }
  devise_for :staff_users, :controllers => { :registrations => :registrations }

  resources :patient_users #added as bugfix
  resources :staff_users #added as bug fix

This gave a new error:

      uninitialized constant StaffUsersController (ActionController::RoutingError)

So I created a new Controller file:

#controllers/staff_users_controller.rb
class StaffUsersController < ApplicationController

end

which gave error

  The action 'show' could not be found for StaffUsersController (AbstractController::ActionNotFound)

So I added that to the controller file

#controllers/staff_users_controller.rb
class StaffUsersController < ApplicationController

  def show
  end

end

Of course that prompted this error:

  Missing template staff_users/show, application/show with {:locale=>[:en], :formats=>[:html], :handlers=>[:erb, :builder, :raw, :ruby, :jbuilder, :coffee]}. Searched in:

So I added that file too (just a blank file at app/views/staff_users.html.erb)

which then works, but redirects to the wrong page /staff_users/1

So I modified the controller again

#controllers/staff_users_controller.rb
class StaffUsersController < ApplicationController

  def show
    redirect_to dashboard_path
  end

end

And then everything works. This seems like an enormously overcomplicated solution though, had to do the same thing for PatientUsers, and I've picked up a lot of spare resource routes that I don't need either.

EDIT 2:

Debugger information as requested by Mandeep.

[7, 16] in /Users/Me/Code/medapp/app/controllers/application_controller.rb
   7    # https://github.com/plataformatec/devise/wiki/How-To%3A-Redirect-to-a-specific-page-on-successful-sign-in-and-sign-out
   8    # redirect successfully signed in users to the dashboard
   9    def after_sign_in_path_for(resource)
   10      debugger
   11      # check for the class of the object to determine what type it is
=> 12      case resource.class
   13      when PatientUser
   14        privacy_agreement_path  
   15      when StaffUser
   16        dashboard_path
(rdb:2) resource.show
*** NoMethodError Exception: undefined method `show' for #<PatientUser:0x0000010171c1c0>

(rdb:2) @resource = resource
#<PatientUser id: 2, email: "[email protected]", encrypted_password: "$2a$10$qY0jBEC8UZHD883ryq69BevPo5oxV.9LPDM8K44gXqcD...", reset_password_token: nil, reset_password_sent_at: nil, remember_created_at: nil, sign_in_count: 16, current_sign_in_at: "2014-07-03 08:46:07", last_sign_in_at: "2014-07-03 08:45:06", current_sign_in_ip: "127.0.0.1", last_sign_in_ip: "127.0.0.1", created_at: "2014-07-03 03:05:01", updated_at: "2014-07-03 08:46:07">

Seems exactly as expected to me, should indicate that resource.class would work fine without any huge workarounds, but obviously not.

Solution

So just using an If conditional instead of a case conditional fixed the whole thing, no need for any of that other stuff. I have no idea why this is, but it's an adequate solution.

#registrations_controller.rb
class RegistrationsController < Devise::RegistrationsController
  protected

  # BUGFIX
  # https://stackoverflow.com/questions/19451881/devise-after-sign-in-path-for-works-but-not-the-other-ones
  # Creating separate after_sign_up_paths for patient_user and staff_user
  def after_sign_up_path_for(resource)
    # check for the class of the object to determine what type it is
    if resource.class == PatientUser
      privacy_agreement_path
    elsif resource.class == StaffUser
      dashboard_path
    end 
  end

end

AND

#application_controllers.rb
class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  def after_sign_in_path_for(resource)
    # check for the class of the object to determine what type it is
    if resource.class == PatientUser
      privacy_agreement_path
    elsif resource.class == StaffUser
      dashboard_path
    end 
  end

end

Thanks to Mandeep and Vapire for their help!!

like image 650
Jeremy E Avatar asked Jul 03 '14 04:07

Jeremy E


1 Answers

Given that your both user models are called PatientUser and StaffUser You should do this:

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  def after_sign_in_path_for(resource)
    # check for the class of the object to determine what type it is
    case resource.class
    when PatientUser
      privacy_agreement_path  
    when StaffUser
      dashboard_path
    end
  end
end

To elaborate a bit on your other tries:

After Devise has signed in the user for you it simply makes one call of after_sign_in_path(resource) to your application controller. Whether it invokes the standard implementation provided by Devise itself or a custom implementation is a non-issue for Devise. It leaves the distinction of what to do fully to you if you implement it yourself. Hence it's your responsibility to check what type is coming as the resource param if you want to react to it.

# application_controller.rb

# you implement the method once
def after_sign_in_path_for(patient_user)
  privacy_agreement_path  
end

# you implement it twice which means
# this overrides the above method, so a call to after_sign_in_path will always
# result in calling this method, no matter what type of user it is
def after_sign_in_path_for(staff_user)
  dashboard_path  
end

Just because you name your parameter a certain way it doesn't give information on what type of parameter it is. That's not how OO programming is designed.

like image 117
Vapire Avatar answered Nov 01 '22 07:11

Vapire