I'm trying to make a STI Base model which changes automatically to inherited class like that:
#models/source/base.rb
class Source::Base < ActiveRecord::Base
after_initialize :detect_type
private
def detect_type
if (/(rss)$/ ~= self.url)
self.type = 'Source::RSS'
end
end
end
#models/source/rss.rb
class Source::RSS < Source::Base
def get_content
puts 'Got content from RSS'
end
end
And i want such behavior:
s = Source::Base.new(:url => 'http://stackoverflow.com/rss')
s.get_content #=> Got content from RSS
s2 = Source::Base.first # url is also ending rss
s2.get_content #=> Got content from RSS
There are (at least) three ways to do this:
1. Use a Factory method
#Alejandro Babio's answer is a good example of this pattern. It has very few downsides, but you have to remember to always use the factory method. This can be challenging if third-party code is creating your objects.
2. Override Source::Base.new
Ruby (for all its sins) will let you override new.
class Source::Base < ActiveRecord::Base
def self.new(attributes)
base = super
return base if base.type == base.real_type
base.becomes(base.real_type)
end
def real_type
# type detection logic
end
end
This is "magic", with all of the super cool and super confusing baggage that can bring.
3. Wrap becomes in a conversion method
class Source::Base < ActiveRecord::Base
def become_real_type
return self if self.type == self.real_type
becomes(real_type)
end
def real_type
# type detection logic
end
end
thing = Source::Base.new(params).become_real_type
This is very similar to the factory method, but it lets you do the conversion after object creation, which can be helpful if something else is creating the object.
Another option would be to use a polymorphic association, your classes could look like this:
class Source < ActiveRecord::Base
belongs_to :content, polymorphic: true
end
class RSS < ActiveRecord::Base
has_one :source, as: :content
validates :source, :url, presence: true
end
When creating an instance you'd create the the source, then create and assign a concrete content instance, thus:
s = Source.create
s.content = RSS.create url: exmaple.com
You'd probably want to accepts_nested_attributes_for to keep things simpler.
Your detect_type logic would sit either in a controller, or a service object. It could return the correct class for the content, e.g. return RSS if /(rss)$/ ~= self.url.
With this approach you could ask for Source.all includes: :content, and when you load the content for each Source instance, Rails' polymorphism will instanciate it to the correct type.
If I were you I would add a class method that returns the right instance.
class Source::Base < ActiveRecord::Base
def self.new_by_url(params)
type = if (/(rss)$/ ~= params[:url])
'Source::RSS'
end
raise 'invalid type' unless type
type.constantize.new(params)
end
end
Then you will get the behavior needed:
s = Source::Base.new_by_url(:url => 'http://stackoverflow.com/rss')
s.get_content #=> Got content from RSS
And s will be an instance of Source::RSS.
Note: after read your comment about becomes: its code uses klass.new. And new is a class method. After initialize, your object is done and it is a Source::Base, and there are no way to change it.
Related
I was under the, apparently incorrect, impression that when I pass a hash into a class the class requires an initialization method like this:
class Dog
attr_reader :sound
def initialize(params = {})
#sound = params[:sound]
end
end
dog = Dog.new({sound:"woof"})
puts dog.sound
But I've run into a bit of code (for creating a password digest) that works within a rails application that doesn't use an initialization method and seems to work just fine and it's kind of confuses me because when I try this anywhere else it doesn't seem to work. Here is the sample code that works (allows me to pass in a hash and initializes without an initialization method):
class User < ActiveRecord::Base
attr_reader :password
validates :email, :password_digest, presence: true
validates :password, length: { minimum: 6, allow_nil: true }
def password=(pwd)
#password = pwd
self.password_digest = BCrypt::Password.create(pwd)
end
end
NOTE: In the create action I pass in a hash via strong params from a form that, at the end of the day, looks something like this {"email"=>"joeblow#gmail.com", "password"=>"holymolycanoli”}
In this bit of code there is no initialization method. When I try something like this (passing in a hash without an initialization method) in pry or in a repl it doesn't seem to work (for instance the following code does not work):
class Dog
attr_reader :sound
def sound=(pwd)
#sound = pwd
end
end
dog = Dog.new({sound:"woof"})
puts dog.sound
The error I get is:
wrong number of arguments (1 for 0)
Is it rails that allows me to pass in hashes like this or ActiveRecord? I'm confused as to why it works within rails within this context but generates an error outside of rails. Why does this work in rails?
If you look at the top you have this:
class Dog < ActiveRecord::Base
This causes your class Dog to inherit from ActiveRecord::Base
when it does so it gains a bunch of methods that allows you to set things up.
Now when you call for example:
Dog.create(password: 'some_password', username: 'some_username')
Your calling a method on the class object that then returns an instance of the class.
so taking your example
class Dog
attr_reader :sound
def sound=(pwd)
#sound = pwd
end
def self.create data_hash
new_dog = self.new #create new instance of dog class
new_dog.sound = data_hash[:sound] #set instance of dog classes sound
new_dog # return instance of dog class
end
end
It's essentially what we would term a factory method, a method that takes in data and returns an object based on that data.
Now I have no doubt that ActiveRecord::Base is doing something more complicated than that but that is essentially what it's doing at the most basic of levels.
I'd also like to point out that when inheriting from ActiveRecord::Base your also inheriting its 'initialize' method so you don't have to set one yourself.
The class knows what attribute methods to create based on the schema you set when you did the DB migrations for a table that matches (through rail's conventions) the class.
A lot of things happen when you subclass ActiveRecord::Base. Before looking at other issues I'm guessing that Dog is a rails ActiveRecord model and you just forgot to add
class Dog < ActiveRecord::Base
I have a model with some attributes and a virtual attribute.
This virtual attribute is used to make a checkbox in the creation form.
class Thing < ActiveRecord::Base
attr_accessor :foo
attr_accessible :foo
end
Since the field is a checkbox in the form, the foo attribute will receive '0' or '1' as value. I would like it to be a boolean because of the following code:
class Thing < ActiveRecord::Base
attr_accessor :foo
attr_accessible :foo
before_validation :set_default_bar
private
def set_default_bar
self.bar = 'Hello' if foo
end
end
The problem here is that the condition will be true even when foo is '0'. I would like to use the ActiveRecord type casting mechanism but the only I found to do it is the following:
class Thing < ActiveRecord::Base
attr_reader :foo
attr_accessible :foo
before_validation :set_default_bar
def foo=(value)
#foo = ActiveRecord::ConnectionAdapters::Column.value_to_boolean(value)
end
private
def set_default_bar
self.bar = 'Hello' if foo
end
end
But I feel dirty doing it that way. Is there a better alternative without re-writing the conversion method ?
Thanks
Your solution from the original post looks like the best solution to me.
class Thing < ActiveRecord::Base
attr_reader :foo
def foo=(value)
#foo = ActiveRecord::ConnectionAdapters::Column.value_to_boolean(value)
end
end
If you wanted to clean things up a bit, you could always create a helper method that defines your foo= writer method for you using value_to_boolean.
I would probably create a module with a method called bool_attr_accessor so you could simplify your model to look like this:
class Thing < ActiveRecord::Base
bool_attr_accessor :foo
end
It seems like ActiveModel ought provide something like this for us, so that virtual attributes act more like "real" (ActiveRecord-persisted) attributes. This type cast is essential whenever you have a boolean virtual attribute that gets submitted from a form.
Maybe we should submit a patch to Rails...
In Rails 5 you can use attribute method. This method defines an attribute with a type on this model. It will override the type of existing attributes if needed.
class Thing < ActiveRecord::Base
attribute :foo, :boolean
end
Caution: there is incorrect behaviour of this attribute feature in rails 5.0.0 on models loaded from the db. Therefore use rails 5.0.1 or higher.
Look at validates_acceptance_of code (click Show source).
They implemented it with comparing to "0".
I'm using it in registrations forms in this way:
class User < ActiveRecord::Base
validates_acceptance_of :terms_of_service
attr_accessible :terms_of_service
end
If you really want cast from string etc you can use this:
def foo=(value)
self.foo=(value == true || value==1 || value =~ (/(true|t|yes|y|1)$/i)) ? true:false
end
Or add typecast method for String class and use it in model:
class String
def to_bool
return true if self == true || self =~ (/(true|t|yes|y|1)$/i)
return false if self == false || self.blank? || self =~ (/(false|f|no|n|0)$/i)
raise ArgumentError.new("invalid value for Boolean: \"#{self}\"")
end
end
Why you don't do this :
def foo=(value)
#foo = value
#bar = 'Hello' if value == "1"
end
I have a model, ModelRun, that accepts nested attributes for another model, ParameterValue. (ModelRun has_many :parameter_values.) However, ParameterValue also employs single-table inheritance to save two subclasses: NumericParameter and FileParameter. FileParameter uses CarrierWave to store a file.
The problem is that in ModelRunController when saving or updating a ModelRun, by default, #model_run.save or #model_run.update_attributes does not identify the type of ParameterValue attributes - it just tries to store them as ParameterValue. This works for NumericParameter values, but it raises an exception for FileParameters because the CarrierWave uploader doesn't get mounted to handle the file upload so ActiveRecord fails when trying to serialize the file to the database.
What's the cleanest way to handle this problem? The only solution that occurred to me was to manually populate the #model_run.parameter_values collection in the controller's create and update methods, since I can tell which type each ParameterValue should be and create the correct objects one by one. However, this seems like reimplementing a lot of Rails magic since I can't just use ModelRun.new(params[:model_run]) or #model_run.update_attributes anymore - seems like it throws away much of the advantage of using accepts_nested_attributes_for in the first place. Is there a better way, a Rails Way™?
Relevant parts of each model are copied below.
model_run.rb
class ModelRun < ActiveRecord::Base
has_many :parameter_values, dependent: :destroy
accepts_nested_attributes_for :parameter_values, allow_destroy: true
attr_accessible :name,
:description,
:geometry_description,
:run_date,
:run_date_as_string,
:parameter_values_attributes
end
parameter_value.rb
class ParameterValue < ActiveRecord::Base
belongs_to :model_run
attr_accessible :type,
:parameter_id,
:description,
:numeric_value,
:model_run_id,
:parameter_file
end
numeric_parameter.rb
class NumericParameter < ParameterValue
attr_accessible :numeric_value
end
file_parameter.rb
class FileParameter < ParameterValue
mount_uploader :parameter_file, ParameterFileUploader
attr_accessible :parameter_file
end
parameter_file_uploader.rb
class ParameterFileUploader < CarrierWave::Uploader::Base
storage :file
def store_dir
"#{Rails.root}/uploads/#{model.class.to_s.underscore}/#{model.id}"
end
def cache_dir
"#{Rails.root}/tmp/uploads/cache/#{model.id}"
end
end
If i understand you well, you are trying to find convinient way of instantiating right subclass in STI hierarchy by passing :type?. If you don't need to change the type later, you can just add this hack to your ParameterValue class
class ParameterValue < ActiveRecord::Base
class << self
def new_with_cast(*attributes, &block)
if (h = attributes.first).is_a?(Hash) && !h.nil? && (type = h[:type] || h['type']) && type.length > 0 && (klass = type.constantize) != self
raise "wtF hax!!" unless klass <= self
return klass.new(*attributes, &block)
end
new_without_cast(*attributes, &block)
end
alias_method_chain :new, :cast
end
end
After this, passing right type will cause right ParameterValue instatntiating, including uploaders, validation etc.
Say I have two classes,
Image and Credit
class Image < ActiveRecord::Base
belongs_to :credit
accepts_nested_attributes_for :credit
end
class Credit < ActiveRecord::Base
#has a field called name
has_many :images
end
I want associate a Credit when Image is created, acting a bit like a tag. Essentially, I want behavior like Credit.find_or_create_by_name, but in the client code using Credit, it would be much cleaner if it was just a Create. I can't seem to figure out a way to bake this into the model.
Try this:
class Image < ActiveRecord::Base
belongs_to :credit
attr_accessor :credit_name
after_create { Credit.associate_object(self) }
end
class Credit < ActiveRecord::Base
#has a field called name
has_many :images
def self.associate_object(object, association='images')
credit = self.find_or_create_by_name(object.credit_name)
credit.send(association) << object
credit.save
end
end
Then when you create an image what you can do is something like
Image.create(:attr1 => 'value1', :attr2 => 'value2', ..., :credit_name => 'some_name')
And it will take the name that you feed into the :credit_name value and use it in the after_create callback.
Note that if you decided to have a different object associated with Credit later on (let's say a class called Text), you could do still use this method like so:
class Text < ActiveRecord::Base
belongs_to :credit
attr_accessor :credit_name
before_create { Credit.associate_object(self, 'texts') }
end
Although at that point you probably would want to consider making a SuperClass for all of the classes that belong_to credit, and just having the superclass handle the association. You might also want to look at polymorphic relationships.
This is probably more trouble than it's worth, and is dangerous because it involves overriding the Credit class's initialize method, but I think this might work. My advice to you would be to try the solution I suggested before and ditch those gems or modify them so they can use your method. That being said, here goes nothing:
First you need a way to get at the method caller for the Credit initializer. Let's use a class I found on the web called CallChain, but we'll modify it for our purposes. You would probably want to put this in your lib folder.
class CallChain
require 'active_support'
def self.caller_class
caller_file.split('/').last.chomp('.rb').classify.constantize
end
def self.caller_file(depth=1)
parse_caller(caller(depth+1).first).first
end
private
#Stolen from ActionMailer, where this was used but was not made reusable
def self.parse_caller(at)
if /^(.+?):(\d+)(?::in `(.*)')?/ =~ at
file = Regexp.last_match[1]
line = Regexp.last_match[2].to_i
method = Regexp.last_match[3]
[file, line, method]
end
end
end
Now we need to overwrite the Credit classes initializer because when you make a call to Credit.new or Credit.create from another class (in this case your Image class), it is calling the initializer from that class. You also need to ensure that when you make a call to Credit.create or Credit.new that you feed in :caller_class_id => self.id to the attributes argument since we can't get at it from the initializer.
class Credit < ActiveRecord::Base
#has a field called name
has_many :images
attr_accessor :caller_class_id
def initialize(args = {})
super
# only screw around with this stuff if the caller_class_id has been set
if caller_class_id
caller_class = CallChain.caller_class
self.send(caller_class.to_param.tableize) << caller_class.find(caller_class_id)
end
end
end
Now that we have that setup, we can make a simple method in our Image class which will create a new Credit and setup the association properly like so:
class Image < ActiveRecord::Base
belongs_to :credit
accepts_nested_attributes_for :credit
# for building
def build_credit
Credit.new(:attr1 => 'val1', etc.., :caller_class_id => self.id)
end
# for creating
# if you wanted to have this happen automatically you could make the method get called by an 'after_create' callback on this class.
def create_credit
Credit.create(:attr1 => 'val1', etc.., :caller_class_id => self.id)
end
end
Again, I really wouldn't recommend this, but I wanted to see if it was possible. Give it a try if you don't mind overriding the initialize method on Credit, I believe it's a solution that fits all your criteria.
Im trying set the single table inheritance model type in a form. So i have a select menu for attribute :type and the values are the names of the STI subclasses. The problem is the error log keeps printing:
WARNING: Can't mass-assign these protected attributes: type
So i added "attr_accessible :type" to the model:
class ContentItem < ActiveRecord::Base
# needed so we can set/update :type in mass
attr_accessible :position, :description, :type, :url, :youtube_id, :start_time, :end_time
validates_presence_of :position
belongs_to :chapter
has_many :user_content_items
end
Doesn't change anything, the ContentItem still has :type=nil after .update_attributes() is called in the controller. Any idea how to mass update the :type from a form?
we can override attributes_protected_by_default
class Example < ActiveRecord::Base
def self.attributes_protected_by_default
# default is ["id","type"]
["id"]
end
end
e = Example.new(:type=>"my_type")
You should use the proper constructor based on the subclass you want to create, instead of calling the superclass constructor and assigning type manually. Let ActiveRecord do this for you:
# in controller
def create
# assuming your select has a name of 'content_item_type'
params[:content_item_type].constantize.new(params[:content_item])
end
This gives you the benefits of defining different behavior in your subclasses initialize() method or callbacks. If you don't need these sorts of benefits or are planning to change the class of an object frequently, you may want to reconsider using inheritance and just stick with an attribute.
Duplex at railsforum.com found a workaround:
use a virtual attribute in the forms
and in the model instead of type
dirtectly:
def type_helper
self.type
end
def type_helper=(type)
self.type = type
end
Worked like a charm.
"type" sometimes causes troubles... I usually use "kind" instead.
See also: http://wiki.rubyonrails.org/rails/pages/ReservedWords
I followed http://coderrr.wordpress.com/2008/04/22/building-the-right-class-with-sti-in-rails/ for solving the same problem I had. I'm fairly new to Rails world so am not so sure if this approach is good or bad, but it works very well. I've copied the code below.
class GenericClass < ActiveRecord::Base
class << self
def new_with_cast(*a, &b)
if (h = a.first).is_a? Hash and (type = h[:type] || h['type']) and (klass = type.constantize) != self
raise "wtF hax!!" unless klass < self # klass should be a descendant of us
return klass.new(*a, &b)
end
new_without_cast(*a, &b)
end
alias_method_chain :new, :cast
end
class X < GenericClass; end
GenericClass.new(:type => 'X') # => #<X:0xb79e89d4 #attrs={:type=>"X"}>