views:

365

answers:

3

I'm working with an external framework (redmine) which has one Project model that has_many EnabledModules.

Projects can have EnabledModules "attached" or "removed" via the module names, like this:

class Project < ActiveRecord::Base
  ...
  has_many :enabled_modules, :dependent => :delete_all
  ...
  def enabled_module_names=(module_names)
    enabled_modules.clear
    module_names = [] unless module_names && module_names.is_a?(Array)
    module_names.each do |name|
      enabled_modules << EnabledModule.new(:name => name.to_s)
    end
  end
end

I'd like to detect when new modules are attached/removed via callbacks on EnabledModule, and not modify the "original source code" if possible.

I can detect "attachments" like this:

class EnabledModule < ActiveRecord::Base
  belongs_to :project

  after_create :module_created

  def module_created
    logger.log("Module attached to project #{self.project_id}")
  end
end

I thought that a before_destroy would work for detecting removals, but it will not. This happens because the enabled_modules.clear call on Project.enabled_module_names=, doesn't invoke 'destroy' on the modules. It just sets their project_id to nil. So I figured I should use a after_update or before_update.

If I use after_update, how can I get the 'previous' project_id?

If I use before_update, how can I differentiate between modules that are 'just updated' and modules whose project_id is going to be reset to nil?

Should I use a totally different approach here?

EDIT: I just found out that I could get the old values with '_was' (i.e. self.project_was). However, collection.clear doesn't seem to trigger update callbacks. Any other solutions?

EDIT 2: Changed title

A: 

Regarding:

If I use after_update, how can I get the 'previous' project_id

Maybe try project_id_was, it's provided by ActiveRecord::Dirty

Kamil Sarna
Hi szelmek, thanks for your quick response, you told me about ActiveRecord::Dirty at the same time I was looking for it. However, neither after_update or before_update are being triggered by collection.clear, so I must try something else.
egarcia
+1  A: 

It looks like revision 2473 onwards of Redmine should solve your problem. See the diffs here: http://www.redmine.org/projects/redmine/repository/diff/trunk/app/models/project.rb?rev=2473&amp;rev_to=2319

Basically the code has been modified such that removed modules are destroyed rather than deleted, the difference being that model callbacks are not fired for deletes.

There's another related fix in revision 3036 that seems important (see http://www.redmine.org/issues/4200) so you might want to pick up at least that version.

Steven
Hi Steven, thanks for answering. You are right. I ended up "re-implementing" the enabled_module_names= on projects so the "destroy" callbacks were called. I'm putting my own solution below.
egarcia
A: 

I ended up reimplementing the enabled_module_names= method of projects, including a file in vendor/plugins/my_plugin/lib/my_plugin/patches/project_patch.rb and alias.

module MyPlugin
  module Patches
    module ProjectPatch
      def self.included(base)
        base.send(:include, InstanceMethods)
        base.extend(ClassMethods)
        base.class_eval do
          unloadable # Send unloadable so it will not be unloaded in development

          # This replaces the existing version of enabled_module_names with a new one
          # It is needed because we need the "destroy" callbacks to be fired,
          # and only on the erased modules (not all of them - the default 
          # implementation starts by wiping them out in v0.8'ish)
          alias_method :enabled_module_names=, :sympa_enabled_module_names=
        end
      end

      module ClassMethods
      end

      module InstanceMethods

        # Redefine enabled_module_names so it invokes 
        # mod.destroy on disconnected modules
        def sympa_enabled_module_names=(module_names)

          module_names = [] unless module_names and module_names.is_a?(Array)
          module_names = module_names.collect(&:to_s)
          # remove disabled modules
          enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}

          # detect the modules that are new, and create those only
          module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name) }
        end
      end
    end
  end
end

I had to include some code on my vendor/plugins/my_plugin/init.rb file, too:

require 'redmine'

require 'dispatcher'

# you can add additional lines here for patching users, memberships, etc...
Dispatcher.to_prepare :redmine_sympa do
  require_dependency 'project'
  require_dependency 'enabled_module'

  Project.send(:include, RedmineSympa::Patches::ProjectPatch)
  EnabledModule.send(:include, RedmineSympa::Patches::EnabledModulePatch)

end

Redmine::Plugin.register :redmine_sympa do
# ... usual redmine plugin init stuff
end

After this, I was able to detect deletions on enabled modules (via before_delete) on my patcher.

egarcia