Monkey patching Devise (or any Rails gem)
Asked Answered
H

7

29

I'm using the Devise authentication gem in my Rails project, and I want to change the keys it's using in flash alerts. (Devise uses :notice and :alert flash keys, but I want to change them to :success and :error so that I can display nice green/red boxes with Bootstrap.)

So I want to be able to somehow override the set_flash_message method in DeviseController.

Here's the new method:

def set_flash_message(key, kind, options = {})

  if key == 'alert'
    key = 'error'
  elsif key == 'notice'
    key = 'success'
  end

  message = find_message(kind, options)
  flash[key] = message if message.present?

end

But I just don't know where to put it.


UPDATE:

Based on an answer I created a config/initializers/overrides.rb file with the following code:

class DeviseController
    def set_flash_message(key, kind, options = {})
       if key == 'alert'
          key = 'error'
       elsif key == 'notice'
          key = 'success'
       end
       message = find_message(kind, options)
       flash[key] = message if message.present?
    end
end

But this causes an error on every Devise action:

Routing Error: undefined method 'prepend_before_filter' for Devise::SessionsController:Class

Hyperplane answered 14/6, 2013 at 23:23 Comment(2)
You might want to require the file where DeviseController is declared. I usually would use DeviseController.class_eval instead of reopening the class anyway to be sure it's already been declared.Divisive
@aceofspades- Could you expand this into an answer? I've not used .class_eval before, would like to see what you have in mind..Hyperplane
D
62

If you try to reopen a class, it's the same syntax as declaring a new class:

class DeviseController
end

If this code is executed before the real class declaration, it inherits from Object instead of extending the class declared by Devise. Instead I try to use the following

DeviseController.class_eval do
  # Your new methods here
end

This way, you'll get an error if DeviseController has not been declared. As a result, you'll probably end up with

require 'devise/app/controllers/devise_controller'

DeviseController.class_eval do
  # Your new methods here
end
Divisive answered 20/6, 2013 at 17:49 Comment(5)
YES, finally this is working. Interestingly, all I had to do was use DeviseController.class_eval do- Didn't need to require the devise_controller, it just worked. So... I'll roll with it- Thanks!Hyperplane
Oddly enough, I can't get this to work using ruby 2.0 and rails 4. @redxvii's more complicated answer should also work, but doesn't. Failure alerts still have the class "alert" instead of "error".Beautify
Using plain ruby in the partial helped me: <div class="alert alert-<%= name == :notice ? "success" : "error" %>">Beautify
@Divisive I feel like I'm stalking you today, you were all over all the Rubymotion gems I was reading over. This isn't working for me, I'm getting require': cannot load such file -- devise/app/controllers/devise_controller (LoadError)Congress
@Congress Just have to make sure it can find the controller class definition, per standard ruby. You might try hardcoding the entire path just to get it working, or reference the loaded gem directory.Divisive
D
16

Using Rails 4 @aceofspades answer didn't work for me.

I kept getting require': cannot load such file -- devise/app/controllers/devise_controller (LoadError)

Instead of screwing around with load order of initializers I used the to_prepare event hook without a require statement. It ensures that the monkey patching happens before the first request. This effect is similar to after_initialize hook, but ensures that monkey patching is reapplied in development mode after a reload (in prod mode the result is identical).

Rails.application.config.to_prepare do
  DeviseController.class_eval do
    # Your new methods here
  end
end

N.B. the rails documentation on to_prepare is still incorrect: See this Github issue

Denaturalize answered 21/10, 2014 at 8:46 Comment(0)
B
4

In your initializer file :

module DeviseControllerFlashMessage
  # This method is called when this mixin is included
  def self.included klass
    # klass here is our DeviseController

    klass.class_eval do
      remove_method :set_flash_message
    end
  end

  protected 
  def set_flash_message(key, kind, options = {})
    if key == 'alert'
      key = 'error'
    elsif key == 'notice'
      key = 'success'
    end
    message = find_message(kind, options)
    flash[key] = message if message.present?
  end
end

DeviseController.send(:include, DeviseControllerFlashMessage)

This is pretty brutal but will do what you want. The mixin will delete the previous set_flash_message method forcing the subclasses to fall back to the mixin method.

Edit: self.included is called when the mixin is included in a class. The klass parameter is the Class to which the mixin has been included. In this case, klass is DeviseController, and we call remove_method on it.

Buhr answered 20/6, 2013 at 9:51 Comment(2)
thanks- can you provide some comments around the def self.included klass method, explaining just what is happening? I've never seen that beforeHyperplane
@RedXVII- Thanks for the solution and the explanation- while this did actually work, I'm gonna go with aceofspades simpler solution. Thanks-Hyperplane
T
3

What about adding in the override initializer and alias for the attributes of the flash hash, like this:

class ActionDispatch::Flash::FlashHash
  alias_attribute :success, :notice
  alias_attribute :error, :alert
end

This should allow your application to read flash[:notice] or flash[:success](flash.notice and flash.success)

Tracheitis answered 18/6, 2013 at 17:14 Comment(1)
Thanks- though this is a different solution to the example problem, my question is specifically about how to monkey patch gems.Hyperplane
G
1

You need to overwrite DeviseController while keeping around its superclass, in your initializer.

Something like:

class DeviseController < Devise.parent_controller.constantize
    def set_flash_message(key, kind, options = {})
       if key == 'alert'
           key = 'error'
       elsif key == 'notice'
           key = 'success'
       end
       message = find_message(kind, options)
       flash[key] = message if message.present?
    end
end
Griffen answered 18/6, 2013 at 20:46 Comment(1)
Using your code I still get an error on all Devise actions, though a different one: NameError in Devise::SessionsController#new undefined local variable or method 'require_no_authentication' for #<Devise::SessionsController:0x00000102907e20>Hyperplane
L
1

I know this is an old thread but this might still be helpful. You should be able to require the file from the gem directory using the engine called_from path.

  require File.expand_path('../../app/helpers/devise_helper',Devise::Engine.called_from)
  require File.expand_path('../../app/controllers/devise_controller',Devise::Engine.called_from)

  DeviseController.class_eval do
    # Your new methods here
  end
Lengthways answered 13/12, 2017 at 23:2 Comment(1)
I used this solution without requiring the devise_helper and it worked well BUT somehow if I update the file (by adding a new line for instance) the server returns an error saying that it could find the devise_controller. The workaround to make it work again is to restart the server. Do anyone know how to avoid that behavior?Crayon
C
0

This is the kind of thing that you will want to put on initialize rails folder, because it's a custom config for this application in particular, second you should use like so:

class DeviseController
    def set_flash_message(key, kind, options = {})
       if key == 'alert'
          key = 'error'
       elsif key == 'notice'
          key = 'success'
       end
       message = find_message(kind, options)
       flash[key] = message if message.present?
    end
end

then you should get the expected behavior. hope it helps since i dont tested, of not pls give a feedback and i will help you try something diferent.

Ceroplastics answered 15/6, 2013 at 1:24 Comment(1)
I added the code above to a file called config/initializers/overrides.rb, but now my app throws an error: Routing Error: undefined method 'prepend_before_filter' for Devise::SessionsController:ClassHyperplane

© 2022 - 2024 — McMap. All rights reserved.