Rails: How do I transactionally add a has_many association to an existing model? - ruby-on-rails-3

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 Product MVC.
Model:
attr_accessible: artwork_ids
I had to add artwork_ids to attr_accessible since it wasn't included before.
View:
= check_box_tag "purchase[artwork_ids][]", artwork.id, artwork.purchase == #purchase
In my view I have an array for each artwork with a check_box_tag. I couldn't use check_box because 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 doing update, if I uncheck each check box, then the params[:purchase] hash won't have an :artwork_ids entry.
Controller:
params[:purchase][:artwork_ids] ||= []
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_attributes actually received {"these"=>"params", "artwork_ids"=>[]}). I tried setting a hidden_value_tag in the view instead, but couldn't get it to work. I think this nit is worthy of a new question.

It is probably best to use make the purchase model a join table and have many to many associations.
Here is an example for your use case.
Customer model
has_many :purchases
has_many :artwork, :through => :purchase
Artwork model
has_many :purchases
has_many :customers, :through => :purchase
Purchase model
belongs_to :customer
belongs_to :artwork
The purchase model should contain customer_id and artwork_id.
you would also need to create a purchase controller that allows you create a new purchase object.
When a customer presses the purchase button it would create a new purchase object which includes the customer_id and the artwork_id. This allows you to create an association between the customer and the artwork they purchase. You can also have a price_paid column to save the price the customer paid at the time of purchase.
if you need more help you can research join many to many associations using :through.
hope it helps

Related

Create action with a non-persisted record with persisted associations?

I'm trying to make a basic checkout page, and here's what I have so far:
The checkout is hosted off of transactions#new, and the form is built off of a new Transaction object. Transaction has a number of nested models underneath it:
class Transaction < ActiveRecord::Base
# ...
accepts_nested_attributes_for :user, :shipping_address, :products
# ...
end
User, Product, and Location (Shipping Address) can be persisted when you arrive at the checkout page, depending on the user flow. Product is always persisted upon arriving at the checkout page.
This setup works for me so far except on the failure cases. I've been trying to re-create the new Transaction record (with the previously entered in user info) to display the appropriate error messages, and I had tried doing this in my controller:
class TransactionsController < ApplicationController
def new
#transaction = Transaction.new
end
def create
#transaction = Transaction.new params[:transaction]
# ...
end
end
But I'm getting this error:
ActiveRecord::RecordNotFound in TransactionsController#create
Couldn't find Product with ID=1 for Transaction with ID=
Request Parameters
{"utf8"=>"✓", "authenticity_token"=>"blahblahblah",
"transaction"=>{"products_attributes"=>{"0"=>{"id"=>"1",
"quantity"=>"1"}}}}
Does anyone know what's up with this? Let me know if you need anymore info about my setup here... tried to pare this issue down to the bare essentials...
class Transaction < ActiveRecord::Base
has_many :product_transactions
has_many :products, :through => :product_transactions
end
and
class Product < ActiveRecord::Base
has_many :product_transactions
has_many :transactions, :through => :product_transactions
end
and
class ProductTransaction < ActiveRecord::Base
belongs_to :transaction
belongs_to :product
end
So, the reason you're getting that error is because you're supplying an id with products_attributes, since you're using accepts_nested_attributes_for the product with that id HAS to already be in the association. This is because the products_attributes= method is expecting to either create or modify the records in the products association.
Since the Product is already persisted and you're just trying to create the ProductTransaction you would need change your accepts_nested_attributes_for to include :product_transactions instead.
This part of your question threw me off
User, Product, and Location (Shipping Address) can be persisted when you arrive at the checkout page, depending on the user flow. Product is always persisted upon arriving at the checkout page.
I don't know if you need to be able to define a product... But if you need to create a Product on the checkout page it would make more sense to define it in the context of a ProductTransaction (ie. ProductTransaction accepts product_attributes or product_id) instead of the context of a Transaction.

Has_many :through association

I made a relationship with the three models using has_many :through:
class Curriculum class < ActiveRecord::Base
has_many :interests
has_many :vacancies,: through => :interests
end
class Vacancy class < ActiveRecord::Base
has_many :interests
has_many :resumes,: through => :interests
end
class Interest < ActiveRecord:: Base
belongs_to :vacancy
belongs_to :curriculum
end
And to create curriculum and vacancy, I create them by administrative, i need to know how can i create the interest to the id of the vacancy, and how it will be logged on the system I have to get the id of it and make the relationship in creating a new bank interest. I wonder how I can program it to do so, and I wonder how the controller will get the create action, and what better way to do this.
First, try to read the whole "Guide to Rails on Associations", especially the part about has_many :through. Then check your schema if your db is migrated and contains for the table interests the necessary foreign keys to curriculums and vacancies called curriculum_id and vacancy_id.
If that is all in place, the following code will create the relationship between two objects:
#curr = Curriculum.find(1)
#vac = Vacancy.find(1)
#curr.interests << #vac
#curr.save
The last two lines creates an interest between #curr and #vac and store that on the database. So you should not use IDs and handle them directly, but work with objects instead.
The second part now is to provide a UI to allow the definition (and removal) of interests between curricula and vacancies. The base flow here is:
You have one curriculum in focus.
You have a link to add / remove curricula.
The view that opens shows a list of possible vacancies, where every vacancy has a checkbox.
By selecting (or deselecting) the check boxes, the IDs of the vacancies will be held in the params of the request sent to the controller.
See the (older) podcast Railscast #52 how to do that in a similar context. Or see the example for has_many :through with checkboxes.
An alternative way would be to use JQuery autocomplete, and add so interests one-by-one. See the nice podcast Railscast #258 which uses JQuery Tokeninput for that.
I think this is what your looking for:
HABTM Checkboxes
That's the best way to use an Has and Belongs to many association.

has_many :through -- Adding metadata to the through relationship

I have a need to add metadata about a HABTM relationship. I wanted to use a has_many :through relationship to accomplish this, but it is not necessary. Here is the problem simplified:
class Customer < ActiveRecord::Base
has_many :customer_teddy_bears
has_many :teddy_bears, :through => :customer_teddy_bears
end
class CustomerTeddyBear < ActiveRecrod::Base
belongs_to :customer
belongs_to :teddy_bear
attr_accesible :most_favoritest # just to show it exists, boolean
end
class TeddyBear < ActiveRecord::Base
has_many :cusomter_teddy_bears
end
So what I need to do is start adding teddy bears to my customers, Teddy Bears are a fixed set of data, lets say a fireman_bear, doctor_bear, dominatrix_bear. Any customer can claim to own a kind of teddy bear, but they also specify which is their most favoritest bear. Since I cannot modify the bears model because that is globally shared among all customers I am adding the metadata (among other metadata) to CustomerTeddyBear.
The problem is that the following does not work.
customer = Customer.new # new record, not yet saved, this must be handled.
customer.teddy_bears << fireman_bear
customer.teddy_bears << doctor_bear
# now to set some metadata
favoritest_record = customer.customer_teddy_bears.select{|ctb| ctb.teddy_bear == doctor_bear}.first
favoritest_record.most_favoritest = true
The above code does not work since customer_teddy_bears entries are only populated during save when creating records in the database. Is there another mechanism for doing this?
If there is nothing "automated" built into rails I will just have to manually manage this relationship by including teddy_bears when I select customer_teddy_bears and using techniques like
def teddy_bears
self.customer_teddy_bears.map(&:teddy_bear)
end
along with manually creating the associations, and not using a :through relationship.
please note, all this must happen before the #save is executed on the Customer object, so I need to set all relevant metadata while still in-memory.
Recommendations I got from #RubyOnRails
ctb = customer.customer_teddy_bears.build({:customer => customer, :teddy_bear => fireman_bear})
ctb2 = customer.customer_teddy_bears.build({:customer => customer, :teddy_bear => doctor_bear})
...
ctb.most_favoritest = true
You can simply do this:
customer = Customer.new # new record, not yet saved, this must be handled.
customer.teddy_bears << fireman_bear
customer.teddy_bears << doctor_bear
customer.save
fav = CustomerTeddyBear.where(:customer_id => customer.id, :teddybear_id => doctor_bear.id)
fav.most_favoritest = true
fav.save
The solution I was forced to resort to is manually building the CustomerTeddyBear object and setting both the customer, teddy_bear, and most_favoritest. Basically most of the time, access is by customer.customer_teddy_bears.map(&:teddy_bear) at least in logic where the possibility is that the record is not yet saved, otherwise just short-cut to customer.teddy_bears.

rails and namespaced models issue

Using rails 3/3.1 I want to store invoices with their items (and later more associations like payments, etc…).
So in a first approach I set up the models like this:
class Invoice < ActiveRecord::Base
has_many :invoice_items
end
class InvoiceItem < ActiveRecord::Base
belongs_to :invoice
end
And the routes likes this:
resources :invoices do
resources :invoice_items
end
I chose InvoiceItem instead of Item because I already have a model named Item and I somehow want to namespace the model to invoices. But this name has the huge disadvantage that one has to use invoice.invoice_items instead of a intuitive invoice.items. Also the generated url helpers look real ugly, for example "new_invoice_invoice_item_path(invoice)" (notice the double invoice_invoice).
So I changed to namespaced models like this:
class Invoice < ActiveRecord::Base
has_many :items, :class_name => "Invoice::Item"
end
class Invoice::Item < ActiveRecord::Base
belongs_to :invoice
end
And the routes likes this:
resources :invoices do
resources :items, :module => "invoice"
end
Now the assocation is named nicely and also the url helpers look pretty. But I can't use dynamic urls (ex. [:new, invoice, :item]) anymore, because the controller is set to "invoice_item" instead of "invoice/item".
I wonder how other people solve this problem and what I'm doing wrong. Or is this simply a bug in rails 3.0.7/ 3.1.rc?
EDIT:
Sorry, I seems I didn't correctly express my concern. My model Item is not related to Invoice::Item. Order::Item is also not related to Item nor Invoice::Item. An Invoice::Item can only belong to one invoice. An Order::Item can only belong to an Order. I need to namespace - but why doesn't rails properly support namespacing out of the box? Or what am I doing wrong with namespacing?
Corin
If an order item and an invoice item are not the same object in the real world then I would name them differently rather than trying to namespace, for example OrderItem and InvoiceItem - this will keep things clearer as your codebase grows and avoid the need to make sure you use the right namespace everywhere you reference an Item.

Wishlist relationships in Rails?

Im building an app where Users have some sort of wishlist
a User can have only one wishlist, and can add existing Items to that wishlist
however the Items belong to other Users on the site
I need to be able to access the wishlist items through current_user.wishlist.items (im using Devise so current_user is available)
i though of adding a wishlist_id column to the items table, but that wouldnt work since items can belong to multiple wishlists.
this seems simple but im having a hard time visualizing the relationship or the migration im supposed to generate
class User < ActiveRecord::Base
has_one :wishlist # or belongs_to :wishlist, it depends which you prefer
end
class Wishlist < ActiveRecord::Base
belongs_to :user
has_and_belongs_to_many :items
end
And of course:
./script/rails generate migration create_item_wishlists wishlist_id:integer item_id:integer
to create join table between items and wishlists.
UPDATE: To answer "frank blizzard" question in comment:
Let's say you have the same structure as in my answer (just change Item to Product or other model name), with HABTM relationship you just need to add new "item" to collection of "items", and then save wishlist:
#user.wishlist.items << item
#user.wishlist.save
You can make it method in user:
class User
def add_to_wishlist(item)
wishlist.items << item
end
end
If you want to remove or modify collection of "items", just use any Ruby method from Array and then save wishlist, which will check differences for you and save only changes.