Rails 4 Create Associated Object on Save - ruby-on-rails

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

Related

Ruby on rails associations parents

I would like to know, whatever the association is (simple belongs_to, polymorphic ...), when I make an association like :
class Toto < ActiveRecord::Base
belongs_to :test_one
belongs_to :test_two
end
class TestOne < ActiveRecord::Base
has_many :totos
end
class TestTwo < ActiveRecord::Base
has_many :totos
end
and then
test_one = TestOne.create
test_two = TestTwo.create
test1 = test_one.totos.create
test2 = test_two.totos.create
I would like to know into a callback of Toto what object instantiate me. In this case, it's obviously test_one and then test_two. I know I could check ids for example but the problem is when i do :
test3 = test_one.totos.create(test_two: test_two)
I can't know if test3 was created through test_one or test_two.
Thank you.
According to your example, I understand that you want to identify the type of object which is associated to your totos object (has_many :totos).
Since there are multiple different objects that might be associated to your totos object through the has_many and belongs_to associations, you might want to perform some kind of verification first to identify the type of the associated object.
First Answer:
This will only work if you know beforehand all the object types that has_many :totos
if test3.respond_to?(:test_one)
test = test3.test_one
elsif test3.respond_to?(:test_two)
test = test3.test_two
end
Second Answer:
I found this on Stackoverflow, and it somehow answeres your question. So if I rephrase the answer to:
def get_belongs_to(object)
associated = []
object.class.reflect_on_all_associations(:belongs_to).map do |reflection|
associated << object.try(reflection.name)
end
associated.compact
end
This method will return an array of all objects associated to your totos object. This will also work when totos belongs to multiple objects say test_one and test_two at the same time. So the following:
associated_objects = get_belongs_to(test3)
and in your case associated_objects[0] will yield the object you desire.
Hope this helps.
Rails does not persist the data you're looking for, so you'll have to store it yourself if you want it. This means you'll need a migration for the new field:
rails generate migration AddOriginalParentTypeToTotos original_parent_type:string
rake db:migrate
You can then override the assignment methods so that the first parent assigned will assign the original_parent_type attribute (and it will remain the same once assigned):
class Toto < ActiveRecord::Base
def test_one=(val)
self[:original_parent_type] ||= 'test_one'
super
end
def test_one_id=(val)
self[:original_parent_type] ||= 'test_one'
super
end
def test_two=(val)
self[:original_parent_type] ||= 'test_two'
super
end
def test_two_id=(val)
self[:original_parent_type] ||= 'test_two'
super
end
end
You can then use send to add an original_parent method:
class Toto < ActiveRecord::Base
def original_parent
send(original_parent_type) if original_parent_type
end
end

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

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/

Best practice: How to split up associations-functions in controllers with equal-access models

I have 2 equal-access models: Users and Categories
Each of these should have the standard-actions: index, new, create, edit, update and destroy
But where do I integrate the associations, when I want to create an association between this two models?
Do I have to write 2 times nearly the same code:
class UsersController << ApplicationController
# blabla
def addCategory
User.find(params[:id]).categories << Category.find(params[:user_id])
end
end
class CategoriessController << ApplicationController
# blabla
def addUser
Category.find(params[:id]).users << User.find(params[:user_id])
end
end
Or should I create a new Controller, named UsersCategoriesController?
Whats the best practice here? The above example doens't look very DRY.... And a new controller is a little bit too much, I think?
Thanks!
EDIT:
I need to have both of these associations-adding-functions, because f.e.
#on the
show_category_path(1)
# I want to see all assigned users (with possibility to assign new users)
and
#on the
show_user_path(1)
#I want to see all assigned categories (with possibility to assign new categories)
EDIT:
I'm taking about a HBTM relationship.
If you have a situation where you need to do this with has_and_belongs_to_many, you could take the approach you are currently using, or you could build this into your existing update actions.
When you add a habtm relationship, you will get an additional method on your classes...
class User < ActiveRecord::Base
has_and_belongs_to_many :categories
end
With this, you can do this:
user = User.find(params[:id])
user.category_ids = [1,3,4,7,10]
user.save
The categories with those ids will be set. If you name your form fields appropriately, the update can take care of this for you if you want to use checkboxes or multiselect controls.
If you need to add them one at a time, then the methods you've built in your original post are reasonable enough. If you think the repetition you have is a code smell, you are correct - this is why you should use the approach I outlined in my previous answer - an additional model and an additional controller.
You didn't mention if you are using has_and_belongs_to_many or if you are using has_many :through. I recommend has_many :through, which forces you to use an actual model for the join, something like UserCategory or Categorization something like that. Then you just make a new controller to handle creation of that.
You will want to pass the user and category as parameters to the create action of this controller.
Your form...
<% form_tag categorizations_path(:category_id => #category.id), :method => :post do %>
<%=text_field_tag "user_id" %>
<%=submit_tag "Add user" %>
<% end %>
Your controller...
class CategorizationsController < ApplicationController
def create
if Categorization.add_user_to_category(params[:user_id], params[:category_id])
...
end
end
then your categorization class...
class Categorization
belongs_to :user
belongs_to :category
def self.add_user_to_category(user_id, category_id)
# might want to validate that this user and category exist somehow
Categorization.new(:user_id => user_id, :category_id => category_id)
Categorization.save
end
end
The problem comes in when you want to send the users back, but that's not terribly hard - detect where they came from and send them back there. Or put the return page into a hidden field on your form.
Hope that helps.

Resources