<< won't work if adding duplicate item - ruby-on-rails

Ticket has_many :products and Product belongs_to :ticket
This code:
def prepare
#ticket = Ticket.last
if #ticket.status != "open"
#ticket = Ticket.create!
end
#ticket.products<<(Product.find(params[:id]))
respond_to :js
end
will not add a new product to #ticket.products, if any instance of the same Product (with the same id) already exists in the #ticket. I want to be able to add two identical products to one ticket - a customer should be able to order two identical beers, shouldn't it?
I digged here in edgeguides, but seems as if only avoiding duplication was covered, not enabling it.

It sounds more like a many-to-many relationship, which can be managed by a has_many :through association:
http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
Consider a shopping cart example, where a customer can order more than one of the same product through a line item, which has a quantity.

You can just try to add not-yet added products, using arel gem:
#ticket.products << Product.where(Product.arel_table[:id].eq(params[:id])
.and(Product.arel_table[:ticket_id].not_eq(#ticket.id)))
or for ruby-on-rails-4 you can use where.not negation:
#ticket.products << Product.where(id: params[:id]).where.not(ticket_id: #ticket.id)

Related

Adding existing has_many records to new record with accepts_nested_attributes_for

The error "Couldn't find Item with ID=123 for Payment with ID=" occurs when adding existing Item models to a new Payment model. This is in a has_many relationship and using accepts_nested_attributes_for.
class Payment < ActiveRecord::Base
has_many :items
accepts_nested_attributes_for :items
...
class Item < ActiveRecord::Base
belongs_to :payment
...
The payment and item models are hypothetical but the problem is real. I need to associate the Items with the Payment (as I do in the new action) before saving the Payment (done in the create action). The user needs to modify attributes on the Items as they create the Payment (adding a billing code would be an example).
Specifically, the error occurs in the controller's create action:
# payments_controller.rb
def new
#payment = Payment.new
#payment.items = Item.available # where(payment_id: nil)
end
def create
#payment = Payment.new payment_params # <-- error happens here
...
end
def payment_params
params.require(:payment).permit( ..., [items_attributes: [:id, :billcode]])
end
lib/active_record/nested_attributes.rb:543:in 'raise_nested_attributes_record_not_found!' is immediately prior to it in the callstack. It is curious that ActiveRecord is including payment_id as part of the search criteria.
For the sake of being complete, the form looks something like this...
form_for #payment do |f|
# payment fields ...
fields_for :items do |i|
# item fields
and it renders the Items correctly on the new action. The params passed through the form look like this:
{ "utf8"=>"✓",
"authenticity_token"=>"...",
"payment"=>{
"items_attributes"=>{
"0"=>{"billcode"=>"123", "id"=>"192"}
},
}
}
If there is a better way to approach this that doesn't use accepts_nested_attributes_for I'm open to suggestions.
I got this to work by just adding an item_ids collection to the params (in addition to the items_attributes). You should just be able to massage your parameters in the controller to look like this
{ "utf8"=>"✓",
"authenticity_token"=>"...",
"payment"=>{
"item_ids"=>[192]
"items_attributes"=>{
"0"=>{"billcode"=>"123", "id"=>"192"}
},
}
}
UPDATE 1: For some reason this only works if item_ids is before items_attributes in the hash. Have not reviewed Rails docs yet to figure out why.
This confused me...
"Adding existing records to a new record..."
So you have Item 1, 2, 3, and wish to associate them to a new Product object?
--
Join Model
The way to do this will be to use a join model (habtm) rather than sending the data through accepts_nested_attributes_for
The bottom line is every time you create a new Product object, its associated Item objects can only be associated to that product:
#items table
id | product_id | information | about | item | created_at | updated_at
So if you're looking to use existing Item objects, how can you define multiple associations for them? The fact is you can't - you'll have to create an intermediary table / model, often cited as a join model:
#app/models/product.rb
Class Product < ActiveRecord::Base
has_and_belongs_to_many :items
end
#app/models/item.rb
Class Item < ActiveRecord::Base
has_and_belongs_to_many :products
end
#items_products (table)
item_id | product_id
--
HABTM
If you use a HABTM setup (as I have demonstrated above), it will allow you to add / delete from the collection your various objects have, as well as a sneaky trick where you can just add Items to a product using item_ids:
#app/controllers/products_controller.rb
Class ProductsController < ApplicationController
def create
#product = Product.new(product_params)
#product.save
end
private
def product_params
params.require(:product).permit(item_ids: [])
end
end
If you then pass the param item_ids[] to your create_method, it will populate the collection for you.
If you want to add specific items to a product, or remove them, you may wish to do this:
#app/controllers/products_controller.rb
Class ProductsController < ApplicationController
def add
#product = Product.find params[:id]
#item = Item.find params[:item_id]
#product.items << #item
#product.items.delete params[:item_id]
end
end

Ruby model associations

I have about 12 models but I'm not sure how to make relations and another stuff with 3 model:
Current relationships:
purchase has_many purchase_items
purchase_item belongs_to item
purchase_item belongs_to purchase
item has_many purchase_items
I've made working form, where I can create new purchase (add multiple items), but now i want to store in table 'inventory' actual quantity of items, so after create/update/delete model purchase or purchase_items (I'm not sure which) model 'inventory' should be updated also. I know that I have to make this in ActiveRecord callback.
Questions:
Which relation should I use to make that and between which models?
Current snippet of code in view (new purchase):
<%= purchase_form.nested_fields_for :purchase_items do |nested| %>
Do I have to add something to this ^ for model inventory?
You wouldn't necessarily need to update your inventory model to accomplish this. Consider solving it like this:
# purchase_item.rb
after_save :update_quantity
def update_quantity
item.quantity --
end
And accessing the inventory for each specific item could be something like this:
# inventory.rb
def get_inventory_for_item(item)
item.quantity
end

Rails 3: Join Table with additional attributes

I have a simple cart system that I have been working on for a little while for an application and am needing a little help in trying to figure out how to update a particular attribute in a join table (Between Order and Products).
Here is the code:
def add_product_to_cart
#product = Product.by_client(current_client).first
#order = current_order
unless #order.products.exists? :id => #product.id
#order.products << #product
end
end
I am trying to update a particular attribute when I update the #order.products...
This is what I am trying to do:
#order.products << #product --> When this happens I need to update a :price attribute..
Anyway of doing this?
class Order
has_many :products
def price
products.sum(:price)
end
end
Just off the top of my head. Here's the sum reference:
http://ar.rubyonrails.org/classes/ActiveRecord/Calculations/ClassMethods.html#M000296
Desire to put attributes into join table may be a sign of missing model. You can promote join table into model, say OrderItem, by adding primary key to it. HABTM associations in Order and Product then become has_many through associations. The new model would be a good place for setting up callback which populates price attribute. It can also unlock additional benefits, like time-stamping items and making them act_as_list, etc.

how to add records to has_many :through association in rails

class Agents << ActiveRecord::Base
belongs_to :customer
belongs_to :house
end
class Customer << ActiveRecord::Base
has_many :agents
has_many :houses, through: :agents
end
class House << ActiveRecord::Base
has_many :agents
has_many :customers, through: :agents
end
How do I add to the Agents model for Customer?
Is this the best way?
Customer.find(1).agents.create(customer_id: 1, house_id: 1)
The above works fine from the console however, I don't know how to achieve this in the actual application.
Imagine a form is filled for the customer that also takes house_id as input. Then do I do the following in my controller?
def create
#customer = Customer.new(params[:customer])
#customer.agents.create(customer_id: #customer.id, house_id: params[:house_id])
#customer.save
end
Overall I'm confused as to how to add records in the has_many :through table?
I think you can simply do this:
#cust = Customer.new(params[:customer])
#cust.houses << House.find(params[:house_id])
Or when creating a new house for a customer:
#cust = Customer.new(params[:customer])
#cust.houses.create(params[:house])
You can also add via ids:
#cust.house_ids << House.find(params[:house_id])
Preface
This is a strange scenario and I hesitated to answer. It seems like Agents should have many Houses rather than a one-to-one relationship, and a House should belong to just one Agent. But with that in mind....
"The best way" depends on your needs and what feels most comfortable/readable to you. Confusion comes from differences in ActiveRecord's behavior of the new and create methods and the << operator, but they can all be used to accomplish your goal.
The new Method
new will not add an association record for you. You have to build the House and Agent records yourself:
# ...
house = #cust.houses.new(params[:house])
house.save
agent = Agent.new(customer: #cust house: house)
agent.save
Note that #cust.houses.new and House.new are effectively the same because you still need to create the Agent record in both cases.
(This code looks weird, you can't easily tell what it's supposed to be doing, and that's a smell that maybe the relationships are set up wrong.)
The << Operator
As Mischa mentions, you can also use the << operator on the collection. This will only build the Agent model for you, you must build the House model:
house = House.create(params[:house])
#cust.houses << house
agent = #cust.houses.find(house.id)
The create Method
create will build both House and Agent records for you, but you will need to find the Agent model if you intend to return that to your view or api:
house = #cust.houses.create(params[:house])
agent = #cust.agents.where(house: house.id).first
As a final note, if you want exceptions to be raised when creating house use the bang operators instead (e.g. new! and create!).
Another way to add associations is by using the foreign key columns:
agent = Agent.new(...)
agent.house = House.find(...)
agent.customer = Customer.find(...)
agent.save
Or use the exact column names, passing the ID of the associated record instead of the record.
agent.house_id = house.id
agent.customer_id = customer.id

Rails checkout form with multiple models

I'm creating a shopping cart based on the one in Agile Web Development With Rails (version 3). I have it set up where "items" are added to a "cart", then upon starting the checkout process, they are added to an "order" object as "line_items". "line_items" represent one "item" in any quantity. Up to this point, I'm not deviating from the examples in the book. However, here is where it gets complicated for me. Every "item" in my store is customizable with text, and I need to be able to store the custom text with the "line_items" in the "orders".
As mentioned above, the "line_items" hold any quantity of an "item", but customers need to be able to customize every item, so each "line_item" will have to hold the different customizations for each individual "item". So, there cannot be just one column for the customization in the "line_items" table. The way I decided to organize it was to create a new model/table "line_item_attributes". For every individual "item" in a "line_item" there is a new "line_item_attributes".
I'm still pretty new to Rails, and I'm having some trouble getting this to work. I'm not convinced I'm even doing this the "Right Way". What I've run into is a sort of chicken/egg problem. When I create an "order", I add the "items" from the cart to it as "line_items". Now in order to customize the products they are ordering, I have to also add "line_item_attributes" to each "line_item" so that the customization form will have something to work with.
Here is what I don't know: I don't know how to "fill in" the blank "line_item_attributes" after the customer submits the form. I can't create "dummy" line_item_attributes for the form, and then upon submitting create new ones (the ones that will actually be saved) from the submitted data. The reason for this is that they must be tied into the "line_items" they belong to. I had hoped that Rails would just fill them in when I called "#order.save", but it doesn't. I hope this isn't to hard to understand.
I've included pertinent code below:
buy.rb (controller)
-SNIP-
def purchase
#cart = find_cart
if #cart.items.empty?
redirect_to_index("Your order is empty")
end
end
def customize
#renderable_partials = [1, 2, 3]
#order = find_order
if #order.nil?
redirect_to_index("Your order is empty")
end
end
def save_purchase
#cart = find_cart
#order = find_order(params[:cart_owner])
#order.add_line_items_from_cart(#cart)
redirect_to :action => 'customize'
end
def save_customize
#order = find_order
if #order.save
redirect_to :action => 'purchase'
else
flash[:error] = "Your information could not be saved"
redirect_to :action => 'customize'
end
end
-SNIP-
order.rb (model)
class Order < ActiveRecord::Base
has_many :line_items
has_many :line_item_attributes
accepts_nested_attributes_for :line_items
accepts_nested_attributes_for :line_item_attributes
def add_line_items_from_cart(cart)
cart.items.each do |item|
li = LineItem.from_cart_item(item)
line_items << li
end
end
end
line_item.rb (model)
class LineItem < ActiveRecord::Base
belongs_to :order
belongs_to :item
has_many :line_item_attributes
accepts_nested_attributes_for :line_item_attributes
def self.from_cart_item(cart_item)
li = self.new
li.item = cart_item.item
li.quantity = cart_item.quantity
li.total_price = cart_item.price
li.quantity.times do |single_item|
lia = LineItemAttribute.new
li.line_item_attributes << lia
end
li
end
end
line_item_attributes.rb (model)
class LineItemAttribute < ActiveRecord::Base
belongs_to :order
belongs_to :line_item
end
Thanks for any help!
I recommend moving the Order and LineItems creation to a separate "service object" or "form object." Inside the service/form object, wrap the Order and Line Items creation in a single transaction. The code will be easier to read and your models will not be polluted with cross-model. From the checkout controller, pass the #cart object to the service object instead of calling the Order object directly.
Look at #2 and #3 of this post for more info about service objects: http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/

Resources