Create instance of Rails model with has_many association prepopulated - ruby-on-rails

This is best explained by example. The following is simple to do:
class Foo < ActiveRecord::Base
has_many :bars
end
1a>> foo = Foo.new
=> #<Foo id: nil>
2a>> foo.bars << Bar.new
=> [#<Bar id: nil, foo_id: nil>]
3a>> foo.bars
=> [#<Bar id: nil, foo_id: nil>]
However, I want all Foo objects initialized with a Bar without having to explicitly run line 2:
class Foo < ActiveRecord::Base
has_many :bars
# [...] Some code here
end
1b>> foo = Foo.new
=> #<Foo id: nil>
2b>> foo.bars
=> [#<Bar id: nil, foo_id: nil>]
Is this possible? Ideally the 'default' object would still be associated in the same way as if I'd explicitly run line 2a, so that it gets saved when the parent Foo object is saved.

Let me preface this by saying that from a design perspective, this probably isn't the greatest idea. Assuming your code is much more complicated than Foo and Bar, you might be headed toward something more along the lines of a builder.
However, if you insist on doing what you ask, this will do it.
class Foo < ActiveRecord::Base
has_many :bars
def after_initialize
self.bars << Bar.new if self.new_record?
end
end

by concept, this isn't the cleanest way to do something like this, because you're mixing a model logic inside another one.
there's more: you haven't yet any instance during initialization, so I think this is not possible.
keep in mind that if you need to save associated objects, you can do it in controllers or using Nested Models Forms
what's the task you need to achieve?

Related

Rails - How to use accepts nested attributes within .new (without saving to the database)

I want to build up a model without storing it in the database, but with accepting nested attributes. It appears that passing parameters with nested attributes to .new does not accept them, and just creates a new model, without any of the passed associations.
Is there a way to have .new accept nested attributes, or another method I can use to accomplish this?
Thanks!
You're basing the entire question on a faulty premise.
class Company < ApplicationRecord
has_many :products
accepts_nested_attributes_for :products
end
class Product < ApplicationRecord
belongs_to :company
end
irb(main):002:0> c = Company.new(name: 'Acme', products_attributes: [ { name: 'Anvil' }])
=> #<Company:0x000056417471c2b0 id: nil, name: "Acme", created_at: nil, updated_at: nil>
irb(main):003:0> c.products
=> [#<Product:0x0000564174972258 id: nil, name: "Anvil", company_id: nil, created_at: nil, updated_at: nil>]
As you can see here the attributes are very much passed to .new - in fact thats exactly what the entire feature is supposed to do so it would be strange otherwise.
So whats actually going wrong? The most likely explaination is that you aren't whitelisting the parameters for the nested records. Make sure that you're using the correct pluralization - the parameter key is singular_attributes for belongs_to / has_one and plural_attributes for has_many/has_and_belongs_to_many.

Rails make model methods appear as attributes

I have a model connecting to a Postgres db.
class Person < ApplicationRecord
def say_id
"#{name} has id: #{id}"
end
end
I have some attributes id,name,email as well as the method above: say_id that can be accessed via:
person = Person.new
person.id => 1
person.say_id => "John has id: 1"
I would like to have the method 'say_id' listed as an attribute as well, now when running person.attributes, I'm only seeing: id, name, email
How can I have my method included as a listable information in full, as with person.attributes but which will include my method? A usecase would be for lazily just laying out all these fields in a table of the Person-object.
In Rails 5+ you can use the attributes api to create attributes that are not backed by a database column:
class Person < ApplicationRecord
attribute :foo
end
irb(main):002:0> Person.new.attributes
=> {"id"=>nil, "email"=>nil, "name"=>nil, "created_at"=>nil, "updated_at"=>nil, "foo"=>nil}
Unlike if you used attr_accessor these actually behave very much like database backed attributes.
You can then override the getter method if you wanted to:
class Person < ApplicationRecord
attribute :foo
def foo
"foo is #{super}"
end
end
irb(main):005:0> Person.new(foo: 'bar').foo
=> "foo is bar"
But for whatever you're doing its still not the right answer. You can get a list of the methods of an class by calling .instance_methods on a class:
irb(main):007:0> Person.instance_methods(false)
=> [:foo]
Passing false filters out inherited methods.

How to refer to the object I'm Fabricating inside my Fabricator?

I'm using Ruby 2.1.1p76 and Rails 4.0.4 and the Fabrication gem.
Is it possible to refer to the object currently being fabricated?
I have a class Foo and a class Bar. I have fabricators for each. My problem is that each of class Foo and Bar contain a field that refers to the other class:
class Foo < ActiveRecord::Base
has_many :bars
belongs_to :current_bar, class_name: "Bar"
end
class Bar < ActiveRecord::Base
belongs_to: :foo
end
It's bothersome to have to fabricate one, then the other and then set the reference for the first in my specs:
let!( :foo ) { Fabricate( :foo ) }
let!( :bar ) { Fabricate( :bar, foo: foo ) }
before( :each ) do
foo.update( current_bar: bar )
end
I'd much rather just fabricate a Foo and have its current_bar fabricated and already referring to the Foo I'm fabricating. I've read through the fabrication gem documentation and I can't find any way that this is possible. I may just be overlooking it. Does anyone know of a way to accomplish this?
For completeness -- fabricators:
Fabricator( :foo ) do
current_bar nil
end
Fabricator( :bar ) do
foo
end
Yup. overlooked it in the documentation.
You can define them in your Fabricators as a block that optionally receives the object being fabricated and a hash of any transient attributes defined. As with anything that works in the Fabricator, you can also define them when you call Fabricate and they will work just like you’d expect. The callbacks are also stackable, meaning that you can declare multiple of the same type in a fabricator and they will not be clobbered when you inherit another fabricator.
Fabricator(:place) do
before_validation { |place, transients| place.geolocate! }
after_create { |place, transients| Fabricate(:restaurant, place: place) }
end
Also, in my case, I needed to use the after_save callback. I was able set the current_bar on my foo object inside the fabricator, but once in the spec, the current_bar was still nil. The update method isn't available inside after_create (I'm new to ruby so I'm not sure why), but it is available inside after_save. Calling update got me going.
Fabricator(:foo) do
transient :current_bar_data
after_save { |foo, transients|
bar = Fabricate( :bar, foo: foo, bar_data: transients[ :current_bar_data ] )
foo.update( current_bar: bar )
}
current_bar nil
end
Now I can fabricate a foo complete with current_bar for my specs:
let!( :some_foo ) { Fabricate( :foo, current_bar_data: "some bar data" ) }

Reverse Polymorphic Association in Rails (Accessing the parent's attributes)

I implemented an example of Reverse Polymorphism in Rails with the selected answer from this question:
Reverse Polymorphic Associations
With this we are able to do the following:
t = Article.new
t.article_elements # []
p = Picture.new
t.article_elements.create(:element => p)
t.article_elements # [<ArticleElement id: 1, article_id: 1, element_id: 1, element_type: "Picture", created_at: "2011-09-26 18:26:45", updated_at: "2011-09-26 18:26:45">]
t.pictures # [#<Picture id: 1, created_at: "2011-09-26 18:26:45", updated_at: "2011-09-26 18:26:45">]
I'm wondering if it's possible to modify this such that if I do
t.article_elements that I can also see the attributes for the picture to. So for example, if I had an picture_name attribute for the variable p, how can I access that from t.article_elements ? So basically, I am trying to access the parent's attributes from the child object.
Note that t.article_elements is a collection. I will use article_element to refer to one member of the collection.
Per your example,
article_element.element.picture_name
will work.
However, you run into a problem with undefined methods by mismatched attributes. For example, a video would not have a picture_name attribute. If all element types shared a common attribute, such as name, it would be fine.
One way to avoid this problem is to check whether the element responds to a given attribute method. For example:
# models/article_element.rb
def element_try(method)
self.element.respond_to?(method) ? self.element.send(method) : ""
end
If our element is a video and we call:
article_element.element_try(:picture_name) # => ""
we will get a blank string instead of NoMethodError.
This solution is a bit hacky, so use at your own risk. Personally, I'd use common attributes instead.
I had similar situation.
class User < ActiveRecord::Base
has_one :image, as: :imageable
end
class Person < ActiveRecord::Base
has_one :image, as: :imageable
end
class Image < ActiveRecord::Base
belongs_to :imageable, :polymorphic => true
end
When you wanna access parent of image, you can do Image.last.imageable and that will give you either User or Person object. It works same with has_many relations.

validates_presence_of causes after_initialize to be called with a weird self

I've had this model which was working fine:
class Weight < ActiveRecord::Base
belongs_to :user
validates_presence_of :weight, :measured_on
attr_accessible :weight, :measured_on
def after_initialize
self.measured_on ||= Date.today
end
end
I added it this line
validates_uniqueness_of :measured_on, :scope => :user_id
and it started throwing an error on validation. Not a validation error but a Ruby error:
>> w.valid?
ActiveRecord::MissingAttributeError: missing attribute: measured_on
from /Users/pupeno/Projects/sano/app/models/weight.rb:8:in `after_initialize'
I've put a debugger statement in after_initialize and I've noticed something unexpected. When I create a new weight it works as expected and the self object on after_initialize is the expected weight:
>> w = Weight.new
/Users/pupeno/Projects/sano/app/models/weight.rb:9
self.measured_on ||= Date.today
(rdb:1) p self
#<Weight id: nil, user_id: nil, weight: nil, measured_on: nil, created_at: nil, updated_at: nil>
(rdb:1) c
=> #<Weight id: nil, user_id: nil, weight: nil, measured_on: "2009-11-22", created_at: nil, updated_at: nil>
When I run w.valid? it gets weird. after_initialize is called again, I'm not sure why, and the self object is nothing I expected:
>> w.valid?
/Users/pupeno/Projects/sano/app/models/weight.rb:9
self.measured_on ||= Date.today
(rdb:1) p self
#<Weight id: 1>
(rdb:1) p self.inspect
"#<Weight id: 1>"
(rdb:1) p self.class
Weight(id: integer, user_id: integer, weight: float, measured_on: date, created_at: datetime, updated_at: datetime)
(rdb:1) p self.measured_on
ActiveRecord::MissingAttributeError Exception: missing attribute: measured_on
(rdb:1)
It seems like another Weight object was created without any attributes but the id set. Any ideas why? Is this a bug or the expected behavior? Am I doing something wrong by setting the measured_on on after_initialize?
My current workaround, in case anybody is having the same problem, is
class Weight < ActiveRecord::Base
belongs_to :user
validates_presence_of :weight, :measured_on
validates_uniqueness_of :measured_on, :scope => :user_id
attr_accessible :weight, :measured_on
def after_initialize
if self.has_attribute? :measured_on
self.measured_on ||= Date.today
end
end
end
but I'd like to have a proper solution.
I think you're hitting a rails bug I recently battled with. See This blog entry linking to the related lighthouse bug.
My understanding is that what's happening is that some prior piece of rails code does a "select id from tablename" to see if an entry exists or matches. The object then caches that the only field that exists for the table is "id". Your code then runs, and the "attributes" value is then incorrect, reflecting only the id field.
From what I could find, this only happened when this particular code path was hit, and didn't generally upset things, except when doing validations.
What I did to get around it was wrap the after_initialise code in a begin/rescue ActiveRecord::MissingAttributeError block. Then I wrote a big note in the application and above each item indicating when a new version of rails is released, we could remove this.
Yes, I'm sure there are more elegant solutions.
def after_initialize
begin
# ... updates here
# self.unique_reference = UUIDTools::UUID.random_create.to_s
rescue ActiveRecord::MissingAttributeError
end
end
Or you could also do:
def after_initialize
if self.has_attribute? :measured_on
self.measured_on ||= Date.today
end
end
This problem is related to the rails ticket #3165. Read up there, if you're interested in the how and why this is occuring
I just spent half a day on this, before I finally found that ticket. While I'm sad that it's been almost exactly a year since this was reported, and it hasn't been fixed yet, here's my simple work around for my scenario:
validates_uniqueness_of :email, :scope => :library_id
def after_initialize
self.status ||= "Invited"
end
This will cause a 'MissingAttributeError' to be thrown, if there are records returned by the validates_uniqueness_of query. My simple solution is this:
def after_initialize
self.status ||= "Invited" if new_record?
end
While other people are having more complex issues, this should solve the simple case, until an actual solution is commited into rails.
validates_uniqueness_of needs to scan your database for entries. ActiveRecord is loading all of those other entries as instances of your model. But to cut down on processor/memory use it it's not adding methods for the attributes, because it shouldn't need them for a quick existence check. However it's still instantiating all existing records as instances of your model so after_initialize is called.
The work around is to modify the #attributes hash directly instead of relying on accessors:
class Weight < ActiveRecord::Base
belongs_to :user
validates_presence_of :weight, :measured_on
validates_uniqueness_of :measured_on, :scope => :user_id
attr_accessible :weight, :measured_on
def after_initialize
#attributes["measured_on"] ||= Date.today
end
end

Resources