Search code examples
ruby-on-railsrubymetaprogrammingmonkeypatching

How to correctly prepend module in a class?


I have got a AccountController class, this controller class is in the application kernel. I dont want to make changes into kernel, therefore Im going to Monkey Patch it. Controller has method named successful_authentication, which I've rewritten. Inside the new method (in my module) this code calls new method named load_favourite_or_index.

I've read that alias_method_chain is deprecated now and should not be used. I'm trying to prepend my module before AccountController. But nothing happens, I guess my prepending code is not correct, please could you help me? Here's my code.

# frozen_string_literal: true

module RedmineKapDesign
    module Patches
        module AccountControllerPatch
            def self.prepended(base) # :nodoc:
                class << base
                    prepend InstanceMethods
                end
            end

            module InstanceMethods
                def successful_authentication(user)
                    logger.info "Successful authentication for '#{user.login}' from #{request.remote_ip} at #{Time.now.utc}"
                    # Valid user
                    self.logged_user = user
                    logger.info "Setting.autologin? = #{Setting.autologin?}, params[:autologin] = #{params[:autologin]}"
                    # generate a key and set cookie if autologin
                    if params[:autologin] && Setting.autologin?
                        set_autologin_cookie(user)
                    end
                    call_hook(:controller_account_success_authentication_after, {:user => user})
                    load_favourite_page_or_index
                    #redirect_back_or_default my_page_path
                end
                def load_favourite_page_or_index
                    user = User.current
                    favourite_page_field = CustomField.where(name: ["Favourite page", "favourite page", "favorite page", "Favourite page", "Любимая страница", "любимая страница", "Избранная страница", "избранная страница"]).first
                    page_url = user.custom_values.where(custom_field_id: favourite_page_field.id).first.value
                    if page_url.empty?
                        redirect_back_or_default my_page_path
                    else
                        redirect_to(page_url)
                    end
                end
                def self.hello
                    puts "Hello"
                end

            end
        end
    end
end

#unless AccountController.included_modules.include?(RedmineKapDesign::Patches::AccountControllerPatch)
#    AccountController.send(:prepend, RedmineKapDesign::Patches::AccountControllerPatch)
#end
AccountController.singleton_class.prepend(RedmineKapDesign::Patches::AccountControllerPatch)


Solution

  • You don't need to separate out the instance methods into a seperate module. You can just place them in AccountControllerPatch and prepend whatever class you are monkeypatching with it.

    module RedmineKapDesign
      module Patches
        module AccountControllerPatch
          def successful_authentication(user)
            logger.info "Successful authentication for '#{user.login}' from #{request.remote_ip} at #{Time.now.utc}"
            # Valid user
            self.logged_user = user
            logger.info "Setting.autologin? = #{Setting.autologin?}, params[:autologin] = #{params[:autologin]}"
            # generate a key and set cookie if autologin
            if params[:autologin] && Setting.autologin?
              set_autologin_cookie(user)
            end
            call_hook(:controller_account_success_authentication_after, {:user => user})
            load_favourite_page_or_index
            #redirect_back_or_default my_page_path
          end
          def load_favourite_page_or_index
            user = User.current
            favourite_page_field = CustomField.where(name: ["Favourite page", "favourite page", "favorite page", "Favourite page", "Любимая страница", "любимая страница", "Избранная страница", "избранная страница"]).first
            page_url = user.custom_values.where(custom_field_id: favourite_page_field.id).first.value
            if page_url.empty?
              redirect_back_or_default my_page_path
            else
              redirect_to(page_url)
            end
          end
          def self.hello
            puts "Hello"
          end
        end
      end
    end
    

    Using a seperate module is only really needed for class methods when using the module mixin pattern. In framework code using a seperate InstanceMethods module can be used for organizational purposes. But in a simple monkeypatch it just makes more of a mess.

    # config/initializers/my_monkey_patch.rb
    # You should explicity require classes in initializers 
    require Rails.root.join('path', 'to', 'monkeypatch')
    require Rails.root.join('path', 'to', 'target')
    
    ::AccountController.prepend RedmineKapDesign::Patches::AccountControllerPatch