Can I monkey-patch an existing ActiveRecord Observer? - ruby-on-rails

I'm trying to monkey patch an existing ActiveRecord Observer; specifically, IssueObserver from the code base of Redmine 1.0.4.
In a plugin's init.rb, I have include the *patch.rb file:
require File.join(File.dirname(__FILE__), 'lib/issue_observer_patch.rb')
IssueObserver.send :include, IssueObserverPatch
This is done outside the Redmine::Plugin.register block.
The module does something like:
module IssueObserverPatch
def self.included(base)
base.send :alias_method_chain, :after_create, :audit
end
def after_create_with_audit(issue)
after_create_without_audit(issue)
issue.logger.info('***'*50)
# Insert a new Audit instance.
end
end
However, when I run the server (using script/server, which is using Mongrel), the patch seems to do nothing. The ***..*** string does not get logged when I create a new issue.
By changing the IssueObserver and including a sentence to log the after_create event, and then running the server and creating an issue; I can see this log, but not the log on after_create_with_audit method.
Is there any proper way to patch an ActiveRecord Observer?

It seems that Rails VM are loaded and unloaded on demand. So, changing the code on the init.rb to the following solves the problem:
config.to_prepare do
IssueObserver.send :include, IssueObserverPatch
end
This makes this block of code to be execute per VM on production and per request on development.

Related

Rails: trouble including modules via a plugin

I'm trying to create a rails plugin and the problem I'm facing is that the app won't include my modules when migrating the plugin.
Here's what I have so far:
1. A file lib/patch/settings_helper_patch.rb with extension code
2. An init.rb file with require_dependency 'patch/settings_helper_patch'
3. Some code in settings_helper_patch.rb which is as follows:
module ValidateIssuePatch
module Patch
module SettingsHelperPatch
def self.included(base)
base.send(:include, InstanceMethods)
end
module InstanceMethods
def issue_options
#some code here
end
end
end
end
end
unless SettingsHelper.included_modules.include?(ValidateIssuePatch::Patch::SettingsHelperPatch)
SettingsHelper.send(:include, ValidateIssuePatch::Patch::SettingsHelperPatch)
end
After I migrate the plugin, I wish to use the issue_options method, but I get undefined local variable or method error.
If I run SettingsHelper.included_modules.include?(ValidateIssuePatch::Patch::SettingsHelperPatch) from the console, I get uninitialized constant Patch::SettingsHelperPatch.
However, if I call ValidateIssuePatch from the console, I get => ValidateIssuePatch in response.
Can anyone tell me what is the magic I'm missing here?
Firstly, if your module is only going to have instance methods, I would recommend using the following easy-to-follow syntax:
module ValidateIssuePatch
module Patch
module SettingsHelperPatch
def issue_options
# code
end
end
end
end
SettingsHelper.include(ValidateIssuePatch::Patch::SettingsHelperPatch)
Secondly, the reason why ValidateIssuePatch might be defined is that some other file has it which is being required properly. This file isn't being executed in any way. I would raise an error somewhere that, when raised, will verify that the code is / isn't being executed. Something like the following:
module ValidateIssuePatch
module Patch
module SettingsHelperPatch
raise "All good" # remove this afterwards
def issue_options
# code
end
end
end
end
SettingsHelper.include(ValidateIssuePatch::Patch::SettingsHelperPatch)
Chances are that the error won't be raised and it'll confirm that your file isn't being required - either not at all or not in the right order.
To further verify this, simply open up your console and do the following with your existing code:
ValidateIssuePatch::Patch::SettingsHelperPatch #=> error
require path_of_file
ValidateIssuePatch::Patch::SettingsHelperPatch #=> no more error
Finally, why do you check for the module already being included in SettingsHelper? (referring to the unless condition) Your code should be including the module only once, not "maybe only once".

Including methods to a controller from a plugin

Using Rails 2.3.11, I'm creating a plugin for Redmine that add methods to ApplicationController.
I've created the following module, in the plugin :
module ApplicationControllerPatch
def self.included(base) # :nodoc:
base.class_eval do
rescue_from AnException, :with => :rescue_method
def rescue_method(exception)
...
end
end
end
end
Now, if I include this module directly into the application_controller.rb file, like this:
class ApplicationController < ActionController::Base
include ApplicationControllerPatch
...
end
Everything works just fine, however I would like to avoid editing the core source by including this module from the plugin itself.
So far, if I do:
ApplicationController.send(:include, ApplicationControllerPatch)
directly from this module file (located in the plugin folder). This will load properly for the request and then it gets overwritten by the controller (I guess).
What's is the way to accomplish this ?
A common pattern is to use Dispatcher.to_prepare inside your plugin's init.rb. This is required because in development mode (or generally if config.cache_classes = false) Rails reloads all classes right before each request to pick up changes without the need to completely restart the application server each time.
This however means the you have to apply your patch again after the class got reloaded as Rails can't know which modules got injected later on. Using Dispatcher.to_prepare you can achieve exactly that. The code defined in the block is executed once in production mode and before each request in development mode which makes it the premier place to monkey patch core classes.
The upside of this approach is that you can have your plugins self-contained and do not need to change anything in the surrounding application.
Put this inside your init.rb, e.g. vendor/plugins/my_plugin/init.rb
require 'redmine'
# Patches to the Redmine core.
require 'dispatcher'
Dispatcher.to_prepare do
ApplicationController.send(:include, MyPlugin::ApplicationControllerPatch) unless ApplicationController.include?(RedmineSpentTimeColumn::Patches::IssuePatch)
end
Redmine::Plugin.register :my_plugin do
name 'My Plugin'
[...]
end
Your patch should always be namespaced inside a module named after your plugin to not run into issues with multiple plugins defining the same module names. Then put the patch into lib/my_plugin/application_controller_patch.rb. That way, it will be picked up automatically by the Rails Autoloader.
Put this into vendor/plugins/my_plugin/lib/my_plugin/application_controller_patch.rb
module MyPlugin
module ApplicationControllerPatch
def self.included(base) # :nodoc:
base.class_eval do
rescue_from AnException, :with => :rescue_method
def rescue_method(exception)
[...]
end
end
end
end
end
This kind of problem occurs only in dev because the classes are reloaded but not gems.
So add your send method in a config.to_prepare block within config/environments/development.rb
Read Rails doc concerning initialization process for further details.

Include plugin in module in Rails

This is a definite newb question:
I have a module defined in my lib/ directory that I call from a sweeper and as a rake task. Inside the module I want to reference my spawn plugin. Just including methods from that plugin doesn't work (undefined method error), nor does any version of require or include that I've tried. What do I need to do to include it?
Thank you!
EDIT:
Here is my latest effort:
require 'vendor/plugins/spawn/init.rb'
module MyModule
include Spawn
def self.my_method
spawn(:method => :thread, :nice => 9) do # also tried Spawn::spawn
...
end
end
end
The error I get is:
undefined method 'spawn' for MyModule:Module #spawn or undefined method 'spawn' for Spawn:Module # Spawn::spawn
The including has to be done from within the plugin. When you "reopen" a module (which is in the lib/ dir), remember to require the original file before making changes to it (the module).
EDIT:
Considering that the plugin is third party, you could try:
Spawn.module_eval do
module_function :spawn
public :spawn
end
module MyModule
def self.my_method
Spawn.spawn(...)
end
end

How can I extend ActiveRecord from app/modules?

I have several different acts_as_... custom class methods I'd like to use in my app. I would like the code for those methods to be in files in the app/modules directory.
I have been unable to get this working.
For instance, I have a file: app/modules/acts_as_lockable
module ActsAsLockable
def acts_as_lockable
before_create :set_lock
include InstanceMethods
end
module InstanceMethods
protected
def set_lock
now = Time.now.to_s
self.lock = Digest::SHA1.hexdigest(now)
end
end
end
ActiveRecord::Base.extend ActsAsLockable
And in application.rb
config.autoload_paths += %W(#{config.root}/app/modules)
When I try to load up a model that calls acts_as_lockable I get the following error:
NameError: undefined local variable or
method `acts_as_lockable'
My guess is that I shouldn't be autoloading the modules folder because ActiveRecord has already been loaded when I extend it? Is there another way to do this? I would like to be able to alter the file during development without restarting my server but that's more of a want that a need.
I think you're thinking about this in the wrong way.
You are adding this module to the load path,
but it will only load if you either say;
require 'acts_as_lockable'
or
ActsAsLockable
I'd suggest you never really want to say either of these inside your code.
The correct paradigm you're looking for is an "initializer".
I suggest you create a file called "config/initializers/acts_as_lockable.rb"
In this file you can either include the whole code,
or just include a require 'acts_as_lockable'
Normally I keep things like this inside the libs directory
ensure lib is in the load path
** config/application.rb **
config.autoload_paths += %W(#{config.root}/lib)
** lib/acts_as_lockable.rb **
module ActsAsLockable
def acts_as_lockable
before_create :set_lock
include InstanceMethods
end
module InstanceMethods
protected
def set_lock
now = Time.now.to_s
self.lock = Digest::SHA1.hexdigest(now)
end
end
end
then in the initializer
** config/initializers/acts_as_lockable.rb **
require 'acts_as_lockable'
ActiveRecord::Base.extend ActsAsLockable
The problem is that ruby autoload mechanism is a lazy process: When a constant like ActsAsLockable is used within your code, it looks for a file called acts_as_lockable.rb within the autoload_paths. As You never actually use ActsAsLockable, the file never gets loaded. You could do (although not tremendously beautiful):
ActsAsLockable
class MyModel < ActiveRecord::Base
acts_as_lockable
...
end
I think the acts_as_* pattern is ment to be used be plugins and gems to easily integrate functionality into your code. Plugins and gems are supposed to be in a final state when you integrate them into your project so you would not need the reloading functionality for the development mode.
I hope this helps.

Wrapping class method via alias_method_chain in plugin for Redmine

I am not sure if this problem is a general Rails problem or Redmine specific.
There is a class User which has a class method try_to_login. I wrote a module containing a method_alias_chain to wrap that method and provide additional functionality. This works fine if I go into the console and call try_to_login. My wrapper will be executed and everything is fine. However, when I run this on the server just the vanilla method is called. The wrapper is never touched. I added a logger command to the vanilla method to be sure and it is in deed being called.
Here is a simplified version of the code:
require_dependency 'principal'
require_dependency 'user'
require 'login_attempt_count'
module UserLoginAttemptLimiterPatch
def self.included(base)
base.extend ClassMethods
base.class_eval do
class << self
alias_method_chain :try_to_login, :attempt_limit
end
end
end
module ClassMethods
def try_to_login_with_attempt_limit(login, password)
user = try_to_login_without_attempt_limit login, password
#stuff here gets called via console but not via browser
user
end
def authentication_failed(login)
#important code here
end
end
end
User.send(:include, UserLoginAttemptLimiterPatch)
In addition this module is required when the plugin is loaded.
How are you requiring the module? If you are running in development mode, the User class could be reloaded after the first request which would clear out your patch and alias_method_chain.
You can get around it by doing the patch inside of a Dispatcher (which runs with every code reload):
require 'dispatcher'
Dispatcher.to_prepare do
Issue.send(:include, MyMooPatch)
end
Reference: http://theadmin.org/articles/2009/04/13/how-to-modify-core-redmine-classes-from-a-plugin/

Resources