Rails checkout form with multiple models - ruby-on-rails

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/

Related

Rails 4 Create Associated Object on Save

How can I create multiple associated objects automatically just after I save a new primary object?
For example
In Rails 4, I have three objects: Businesses, Budgets, and Categories.
#app/models/business.rb
class Business < ActiveRecord::Base
#attrs id, name
has_many :budgets
end
#app/models/budget.rb
class Budget < ActiveRecord::Base
#attrs id, business_id, department_id, value
belongs_to :business
belongs_to :category
end
#app/models/category.rb
class Category < ActiveRecord::Base
#attrs id, name
has_many :budgets
end
When I create a new Business, after saving the new Business, I would like to atomically create a Budget for each Category and give it value of $0. This way, when I go to show or edit a new Business, it will already have the associated Categories and Budgets, which can then be edited. Thus, upon creating a new Business, multiple new Budgets will be created, one for each Category, each with the value of 0.
I read this article: Rails 3, how add a associated record after creating a primary record (Books, Auto Add BookCharacter)
And I am wondering if I should use the after_create callback in the Business model and have the logic then exist in the Budgets controller (not exactly sure how to do this) or if I should add logic to the businesses_controller.rb in the 'new' call with something similar to:
#business = Business.new
#categories = Category.all
#categories.each do |category|
category.budget.build(:value => "0", :business_id => #business.id)
end
In my experience, it's best to avoid using callbacks unless it relates to a given model's persistence. In this case, letting a budget set it's own default value when one isn't supplied is good use of a callback. That also removes some complexity from your logic.
class Budget
before_validate :set_value
...
private
def set_value
self.value ||= 0
end
end
For the rest, I would create custom classes, each with a single responsibility, to systematically generate a new business. Here's an example. Keep in mind that this is not meant to be copy and pasted, it's just to illustrate a concept:
class BusinessGenerator < Struct.new(:business_params)
attr_reader :business
def generate
create_business
create_budgets
end
private
def create_business
#business = Business.create!(business_params)
end
def create_budgets
BudgetGenerator.new(#business).create
end
end
class BudgetGenerator < Struct.new(:business)
def generate
categories.each do |c|
business.budgets.create!(category: c)
end
end
private
def categories
Category.all
end
end
This is nice because it separates concerns and is easily extensible, testable and doesn't use Rails magic like accepts_nested_attributes_for. For example, if in the future you decide that not all businesses need a budget in every category, you can easily pass the ones you want as an argument to BudgetGenerator.
You'll instantiate the BusinessGenerator class in the controller:
class BusinessController < ActionController::Base
...
def create
generator = BusinessGenerator.new(business_params)
if generator.generate
flash[:success] = "Yay"
redirect_to generator.business
else
render :new
end
end
...
end
Some sticking points you might have with this approach include:
Returning validation errors to your business form
If the creation of a budget fails, you're stuck with a budget-less business. You can't wait to save business until after the budgets are created because there is no id to associate. Perhaps consider putting a transaction inside of the generator method.
Regardless of Brent Eicher's great advice, I've never experienced anything bad from using callbacks. If you don't mind using them, you could do the following (if you're setting the budget at 0 each time):
#app/models/business.rb
class Business < ActiveRecord::Base
before_create :build_budgets
private
def build_budgets
Category.all.each do |category|
self.budgets.build(category: category, value: "0")
end
end
end
--
Also, you need to make sure your budget foreign keys are correct.
I see you have department_id when Budget belongs_to Category. You should make this category_id or define the foreign_key:
#app/models/budget.rb
class Budget < ActiveRecord::Base
belongs_to :category, foreign_key: "department_id"
end
I ended up adding the logic to the create method in the Business controller to loop through all Categories and create a budget just after save. Note that I was lazy and didn't put in any error handling. :
def create
#business = Business.new(params[:business])
#results = #business.save
#categories = Categories.all
#categories.each do |category|
category.budgets.create(:amount => "0", :business_id => #business.id)
end
respond_to do |format|
...
end
end

<< won't work if adding duplicate item

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)

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

How do you give a class in the controller the ID's it belongs to?

How do i give my products the user_id, Location_id and the product_dates_id in the controller?
class Product < ActiveRecord::Base
belongs_to :user
belongs_to :location
belongs_to :product_date
end
My Controller:
def new
#location = Location.new
#product = Product.new
product_date = #location.product_dates.build
product_date.products.build
end
def create
#location = Location.new(params[:location])
end
My Table:
create_table :products do |t|
t.integer :user_id
t.integer :location_id
t.integer :product_date_id
end
Thank you.
#location = Location.create(params[:location])
#location.products.each{ |p| p.update_attribute :user_id, current_user.id }
#location.product_dates.map(&:products).flatten.each{ |p| p.update_attribute :user_id, current_user.id }
or better add hidden field for :user_id into each product and product_date
To answer your question
def new
#First create the new product object assuming you have a current_user available. If not you will need to make a current_)user available.
#product = current_user.build_product
#Now build a new location for the product
location = #product.build_location # You don't need location to be an instance variable.
#when you reference #location in your form you should now be using fields_for :locations instead.
#build a new product_date object for the location_product_dates array
product_date = #location.product_dates.build
#Now add that product date object to the product
product.product_date = product_date
end
Add accepts_nested_attributes declarations to the relevant models and use fields_for declarations in the relevant product forms, that way your create and update actions don't need any changes to them as the fields_for declarations in the form will arrange for the related objects to have their data nested inside the products hash when posted back to the create and update actions.
Couple that with the accepts_nested_attributes_for declarations for all the right models and everything will automagically be updated/created
That answers you question. The new action builds all the records and sets up all the associations for you but it seems to me like you don't really need the product_date associated to the product as it is already associated to the location which is associated to the product and as a location can have many products (assumption based on your original new action) you would struggle to work out which product_date from the many location records and their associated product_date records should be added directly to the product.
I guess you have you associations slightly wrong.
You should also consider refactoring that new action into a class method on the product model to enable you to do something like
#product = Product.new_with_associations(current_user)
The def self.new_with_associations needs to just do pretty much exactly the same as the code I gave you for the new action but you will need to pass the current_user in as a param as the session hash (which is where the current_user will ultimately be coming from) is unavailable to models for very good reason.
Putting this in a class method for the product not only cleans up your controller code but also makes makes the code re-usable and simpler to test both from the console and from your test suite

How to save many has_many_through objects at the same time in Rails?

I have two models related as follows.
USERS
has_many :celebrations
has_many :boards, :through => :celebrations
BOARDS
has_many :celebrations
has_many :users, :through => :celebrations
CELEBRATIONS
:belongs_to :user
:belongs_to :board
In my controller I want to create the objects from form data. I do this as follows:
#user = User.new(params[:user])
#board = Board.new(params[:board])
if #user.save & #board.save
#user.celebrations.create(:board_id => #board,:role => "MANAGER")
redirect_to :action => some_action
end
Since the models are joined by the many through is there a way to save them in one time and then produce the error messages at one time so that they show on the form at the same time?
This will do
#user = User.new(params[:user])
#user.boards << #board
#user.save
This will save the user object and the board objects associated with the same command #user.save. It will create the intermediate celebrations record also with the user_id and board_id saved but in your case it might not be useful as you need to set values of other columns of celebrations table
Your method looks pretty standard to me. To answer your question...
When working with an association, the << operator is basically the same as the create method except:
<< uses a transaction. create does not.
<< triggers the :before_add and :after_add callbacks. create does not.
<< returns the association proxy (essentially the collection of objects) if successful, false is not successful. create returns the new instance that was created.
Using the << operator in your case wouldn't get you much since you would still have multiple transactions. If you wanted all of the database inserts/updates in your action to be atomic you could wrap the action into a transaction. See the Rails API for details:
http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html

Resources