27

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

Yarin
  • 173,523
  • 149
  • 402
  • 512
  • 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. – aceofspades Jun 19 '13 at 18:53
  • @aceofspades- Could you expand this into an answer? I've not used .class_eval before, would like to see what you have in mind.. – Yarin Jun 20 '13 at 14:34

7 Answers7

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
aceofspades
  • 7,568
  • 1
  • 35
  • 48
  • 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! – Yarin Jun 20 '13 at 22:03
  • 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". – jrhorn424 Sep 09 '13 at 05:23
  • Using plain ruby in the partial helped me: `
    ">`
    – jrhorn424 Sep 09 '13 at 06:06
  • 1
    @aceofspades 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)` – Dan2552 Oct 10 '13 at 16:33
  • @Dan2552 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. – aceofspades Oct 11 '13 at 19:00
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

ARO
  • 239
  • 2
  • 5
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.

RedXVII
  • 867
  • 6
  • 12
  • thanks- can you provide some comments around the `def self.included klass` method, explaining just what is happening? I've never seen that before – Yarin Jun 20 '13 at 14:36
  • @RedXVII- Thanks for the solution and the explanation- while this did actually work, I'm gonna go with aceofspades simpler solution. Thanks- – Yarin Jun 20 '13 at 22:05
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)

Laura Popa
  • 456
  • 5
  • 6
  • 1
    Thanks- though this is a different solution to the example problem, my question is specifically about how to monkey patch gems. – Yarin Jun 19 '13 at 12:44
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
Borski
  • 11
  • 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 #` – Yarin Jun 19 '13 at 12:41
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
userbard
  • 36
  • 3
  • 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? – dani24 Dec 29 '22 at 13:59
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.

Yarin
  • 173,523
  • 149
  • 402
  • 512
vinicius gati
  • 421
  • 5
  • 16
  • 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:Class` – Yarin Jun 16 '13 at 14:55