9

I'm in the process of writing an Importable concern for my rails project. This concern will provide a generic way for me to import a csv file into any model that includes Importable.

I need a way for each model to specify which field the import code should use to find existing records. Are there any recommended ways of adding this type of configuring for a concern?

Col
  • 2,300
  • 3
  • 16
  • 16

2 Answers2

11

A slightly more "vanilla-looking" solution, we do this (coincidentally, for the exactly some csv import issue) to avoid the need for passing arguments to the Concern. I am sure there are pros and cons to the error-raising abstract method, but it keeps all the code in the app folder and the models where you expect to find it.

In the "concern" module, just the basics:

module CsvImportable
  extend ActiveSupport::Concern

  # concern methods, perhaps one that calls 
  #   some_method_that_differs_by_target_class() ...

  def some_method_that_differs_by_target_class()
    raise 'you must implement this in the target class'
  end

end

And in the model having the concern:

class Exemption < ActiveRecord::Base
  include CsvImportable

  # ...

private
  def some_method_that_differs_by_target_class
    # real implementation here
  end
end
Tom Wilson
  • 797
  • 9
  • 26
  • i like this variant better, especially if the concern is app specific (not in a gem) and not included in a lot of other models. – srecnig May 19 '15 at 09:47
9

Rather than including the concern in each model, I'd suggest creating an ActiveRecord submodule and extend ActiveRecord::Base with it, and then add a method in that submodule (say include_importable) that does the including. You can then pass the field name as an argument to that method, and in the method define an instance variable and accessor (say for example importable_field) to save the field name for reference in your Importable class and instance methods.

So something like this:

module Importable
  extend ActiveSupport::Concern

  module ActiveRecord
    def include_importable(field_name)

      # create a reader on the class to access the field name
      class << self; attr_reader :importable_field; end
      @importable_field = field_name.to_s

      include Importable

      # do any other setup
    end
  end

  module ClassMethods
    # reference field name as self.importable_field
  end

  module InstanceMethods
    # reference field name as self.class.importable_field
  end

end

You'll then need to extend ActiveRecord with this module, say by putting this line in an initializer (config/initializers/active_record.rb):

ActiveRecord::Base.extend(Importable::ActiveRecord)

(If the concern is in your config.autoload_paths then you shouldn't need to require it here, see the comments below.)

Then in your models, you would include Importable like this:

class MyModel
  include_importable 'some_field'
end

And the imported_field reader will return the name of the field:

MyModel.imported_field
#=> 'some_field'

In your InstanceMethods, you can then set the value of the imported field in your instance methods by passing the name of the field to write_attribute, and get the value using read_attribute:

m = MyModel.new
m.write_attribute(m.class.imported_field, "some value")
m.some_field
#=> "some value"
m.read_attribute(m.class.importable_field)
#=> "some value"

Hope that helps. This is just my personal take on this, though, there are other ways to do it (and I'd be interested to hear about them too).

Chris Salzberg
  • 27,099
  • 4
  • 75
  • 82
  • Great answer, thank you. It all makes sense. I'll try what you've suggested tomorrow and award the solution after that. – Col Jan 13 '13 at 11:12
  • It's all working but I have one small issue. I get the following error unless I specifically require 'concerns/importable' at the top of each model file. NameError: undefined local variable or method `include_importable' for # I've added the concerns directory to the autoload_path like so: config.autoload_paths += Dir["#{config.root}/app/models/**/"] It's not that big of an issue but I'd prefer not to have to require it everywhere. Any suggestions? – Col Jan 13 '13 at 21:23
  • 2
    The problem is the `ActiveRecord::Base.extend(Importable::ActiveRecord)` line, which has to be called *before* your models are loaded in order for ActiveRecord to have the `includes_importable` method. Autoloading doesn't work because the actual `Importable` module name is not mentioned in your model code. There are a few ways to solve the problem, I guess the easiest is to create an initializer at `config/initializers/active_record.rb` and move the `extend` line in there, with a `require 'concerns/importable'` at the top. Then you won't have to require it in your models. – Chris Salzberg Jan 14 '13 at 02:57
  • 1
    For reference, see also this: http://stackoverflow.com/questions/2328984/rails-extending-activerecordbase – Chris Salzberg Jan 14 '13 at 02:59
  • 1
    Actually, if you have the concern somewhere in `config.autload_paths` then you don't need the `require 'concerns/importable'` line in the initializer. – Chris Salzberg Jan 14 '13 at 07:01
  • Updated my answer with the stuff discussed here. – Chris Salzberg Jan 14 '13 at 07:06
  • See also http://stackoverflow.com/questions/15529079/rails-passing-an-argument-to-a-concern – antinome Sep 03 '14 at 17:13
  • Related: http://stackoverflow.com/questions/26900356/how-to-create-a-rails-4-concern-that-takes-an-argument – jesal Nov 13 '14 at 21:54
  • how do you include this in config.autoload_paths? – carl Dec 19 '16 at 09:44