Jason Newell
Jason Newell

Reputation: 601

How can I override a class method that a gem is adding to ActiveRecord::Base (in my decorator)

The interactions are a bit complicated here, so bear with me. I'm working with Spree. Spree uses delegate_belongs_to in some of its models, including 'Spree::Variant'. 'delegate_belongs_to :product, :available_on (...)' is being called in the original class body.

I would like for variants to be able to have their own available_on date. delegate_belongs_to is injecting itself like so:

module DelegateBelongsTo
  extend ActiveSupport::Concern
  module ClassMethods
    #...
    def delegate_belongs_to(association, *attrs)     
      #...
    end
  end
end

ActiveRecord::Base.send :include, DelegateBelongsTo  

I would prefer not to override the entire variant class to remove this one argument. This is one of my most recent attempts:

Spree::Variant.class_eval do
  class << self
    alias_method :original_dbt, :delegate_belongs_to

    def delegate_belongs_to(association, *attrs)
      attrs.delete [:available_on]
      original_dbt(association, attrs)
    end
  end

  attr_accessible :available_on
  #...
end

I've tried a number of variations on this. I'm not sure if it's because it's in a class_eval, if there's some issue with the order of execution, or what, but I can't seem to override this method. What am I failing to understand here?

Thanks.

Upvotes: 2

Views: 5613

Answers (5)

frankiek3
frankiek3

Reputation: 88

For anyone looking for a working solution, removing the getter and setter reverts to the ActiveRecord association.

`DATETIME_add_to_variant.rb`:
class AddToVariant < ActiveRecord::Migration
    def change
        add_column :spree_variants, :description, :text
        add_column :spree_variants, :available_on, :datetime
    end
end

`variant_decorator.rb`:
Spree::Variant.class_eval do
    remove_method :description
    remove_method :description=

    remove_method :available_on
    remove_method :available_on=
end

Upvotes: 0

Jason Newell
Jason Newell

Reputation: 601

I ended up just overriding Spree::Variant in my own app/models directory.

I really would have preferred not to do this for the sake of removing one argument, but the thing is that the spree models seem to get loaded into memory on instantiation, and since Ruby has executable class bodies, the original delegate_belongs_to call is fired (and triggers a number of side effects) before I can switch it out.

The method is being overridden, I can confirm, but it's too late by then. My attempts with setting up an initializer to get around this weren't successful.

As far as I can tell, anyone facing a similar situation just has to replace the entire class. I'd like to leave this question open for a bit in case someone has a better solution. Thanks for the responses.

Upvotes: 1

Peter Brown
Peter Brown

Reputation: 51717

I don't think I fully understand your problem, but whenever I see someone using class_eval or alias_method, I think there has to be a better way. Have you tried overriding the method in your class and just calling super?

class MyModel < ActiveRecord::Base
  def self.delegate_belongs_to(association, *attrs)
    attrs.delete [:available_on]
    super(association, attrs)
  end
end

Upvotes: 1

lisowski.r
lisowski.r

Reputation: 3761

I usually do it in lib, so I'm sure that my changes are evaluated after all initializers.

  1. allow load lib files in application.rb

    # ...
    module App
      class Application < Rails::Application
        # ...        
        config.autoload_paths += %W(#{config.root}/lib)
        # ...
        require 'spree_variants'
      end
    end
    
  2. create file lib/spree_variants.rb with content

    require 'spree_core'
    
    module SpreeOldPriceProducts
      class Engine < Rails::Engine
        def self.activate
          Variant.class_eval do
            alias_method :original_dbt, :delegate_belongs_to
    
            def delegate_belongs_to(association, *attrs)
              attrs.delete [:available_on]
              original_dbt(association, attrs)
            end
          end
        end
    
        config.to_prepare &method(:activate).to_proc
      end
    end
    

I was done something like that about 2 months ago with rails 3.0.9 and spree_core 0.60.1 so my answer can be useless for you, but maybe gives you some directions.

Upvotes: 1

Gazler
Gazler

Reputation: 84180

This isn't really an answer, I am pointing out what I have tried

I am not sure how much this will help, but I took your code and simplified it a bit to see if overwriting the method would work and it does, which means your method is correct and if you overwrite the class directly then it will call the new method.

module DelegateBelongsTo

  module ClassMethods
    def delegate_belongs_to(association, *attrs)     
      p "METHOD INSIDE MODULE"
    end
  end

  def self.included(base)
    base.extend(ClassMethods)
  end
end

module Spree
  class Variant
    include DelegateBelongsTo

    def self.some_method
      delegate_belongs_to("foo", "bar")
    end
  end
end

Spree::Variant.some_method  #METHOD INSIDE MODULE


Spree::Variant.class_eval do
  class << self
    alias_method :original_dbt, :delegate_belongs_to
    def delegate_belongs_to(association, *attrs)
       p "OVERWRITTEN METHOD"
      original_dbt(association, *attrs)
    end
 end
end

Spree::Variant.some_method # "OVERWRITTEN METHOD", "METHOD INSIDE MODULE"

Upvotes: 1

Related Questions