Let’s imagine I run an imaginary art store with a couple models (and by models I’m referring to the Rails term not the arts term as in nude models) that looks something like this:
class Artwork < ActiveRecord::Base
belongs_to :purchase
belongs_to :artist
end
class Purchase < ActiveRecord::Base
has_many :artworks
belongs_to :customer
end
The Artwork is created and sometime later it is included in a Purchase. In my create or update controller method for Purchase I would like to associate the new Purchase with the existing Artwork.
If the Artwork did not exist I could do @purchase.artworks.build or @purchase.artworks.create, but these both assume that I’m creating a new Artwork which I am not. I could add the existing artwork with something like this:
params[:artwork_ids].each do |artwork|
@purchase.artworks << Artwork.find(artwork)
end
However, this isn’t transactional. The database is updated immediately. (Unless of course I’m in the create controller in which case I think it may be done “transactionally” since the @purchase doesn’t exist until I call save, but that doesn’t help me for update.) There is also the @purchase.artwork_ids= method, but that is immediate as well.
I think something like this will work for the update action, but it is very inelegant.
@purchase = Purchase.find(params[:id])
result = @purchase.transaction do
@purchase.update_attributes(params[:purchase])
params[:artwork_ids].each do |artwork|
artwork.purchase = @purchase
artwork.save!
end
end
This would be followed by the conventional:
if result
redirect_to purchase_url(@purchase), notice: 'Purchase was successfully updated.' }
else
render action: "edit"
end
What I’m looking for is something like the way it would work from the other direction where I could just put accepts_nested_attributes_for in my model and then call result = @artwork.save and everything works like magic.
I have figured out a way to do what I want which fairly elegant. I needed to make updates to each part of my
ProductMVC.Model:
I had to add artwork_ids to attr_accessible since it wasn’t included before.
View:
In my view I have an array for each artwork with a
check_box_tag. I couldn’t usecheck_boxbecause of the gotcha where not checking the box would cause a hidden value of “true” to be sent instead of an artwork id. However, this leaves me with the problem of deleting all the artwork from a purchase. When doingupdate, if I uncheck each check box, then theparams[:purchase]hash won’t have an:artwork_idsentry.Controller:
Adding this guarantees that the value is set, and will have the desired effect of removing all existing associations. However, this causes a pesky rspec failure
Purchase.any_instance.should_receive(:update_attributes).with({'these' => 'params'})fails because:update_attributesactually received{"these"=>"params", "artwork_ids"=>[]}). I tried setting ahidden_value_tagin the view instead, but couldn’t get it to work. I think this nit is worthy of a new question.