Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I add a model specific configuration option to a rails concern?

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?

like image 243
Col Avatar asked Jan 13 '13 03:01

Col


People also ask

What is ActiveSupport concern?

It is a bit of Rails carbohydrates sprinkled upon a Ruby module. What ActiveSupport::Concern does for you is it allows you to put code that you want evaluated inside the included block. For example, you want to extract the trashing logic out of your model.

What is a model in a Rails application?

A Rails Model is a Ruby class that can add database records (think of whole rows in an Excel table), find particular data you're looking for, update that data, or remove data. These common operations are referred to by the acronym CRUD--Create, Remove, Update, Destroy.

What is extend ActiveSupport concern?

Using extend ActiveSupport::Concern tells Rails that we are creating a concern. The code within the included block will be executed wherever the module is included. This is best for including third party functionality. In this case, we will get an error if the before_action is written outside of the included block.


2 Answers

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
like image 93
Tom Wilson Avatar answered Oct 25 '22 20:10

Tom Wilson


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).

like image 41
Chris Salzberg Avatar answered Oct 25 '22 19:10

Chris Salzberg