How to create a child model from a parent - ruby-on-rails

I've been spinning may wheels with Rails on this for a few days. I'm working with three models: IngredientBase, Ingredient, and IngredientList. IngredientBase contains a string that may be "Chicken", "Eggs", or "Bacon." Ingredient will have a single Ingredient and a quantity. IngredientList will have many Ingredients.
I want the user to be able to ingredient_lists/new and be able to create several Ingredients before submitting the IngredientList. How could I do this without having the user submit multiple forms?

Associative
What you're looking at is nested models.
Nested models basically allow you to append attributes to an instance of one model; those attributes being sent to an associative model.
This is achieved with accepts_nested_attributes_for:
#app/models/ingredient_list.rb
class IngrendientList < ActiveRecord::Base
has_many :ingredients
accepts_nested_attributes_for :ingredients
end
#app/models/ingredient.rb
class Ingredient < ActiveRecord::Base
belongs_to :ingredient_list
end
Because your models made me confused, I rewrote the structure for you. I think you're getting confused with join models etc:
#app/models/ingredient.rb
class Ingredient < ActiveRecord::Base
#columns id | name | weight | etc | created_at | updated_at
has_many :recipe_ingredients
has_many :recipes, through: :recipe_ingredients
end
#app/models/recipe_ingredient.rb
class RecipeIngredient < ActiveRecord::Base
#columns id | recipe_id | ingredient_id | quantity | created_at | updated_at
belongs_to :recipe
belongs_to :ingredient
accepts_nested_attributes_for :ingredient
end
#app/models/recipe.rb
class Recipe < ActiveRecord::Base
#columns id | name | etc | etc | created_at | updated_at
has_many :recipe_ingredients #-> allows extra attributes
has_many :ingredients, through: :recipe_ingredients
accepts_nested_attributes_for :recipe_ingredients
end
This will give you the ability to create a Recipe, add new Ingredients, and generally keep your model much more robust.
Here's how you can make it work with controller and views etc:
#app/controllers/recipes_controller.rb
class RecipesController < ApplicationController
def new
#recipe = Recipe.new
end
def create
#recipe = Recipe.new recipe_params
#recipe.save
end
private
def recipe_params
params.require(:recipe).permit(:recipe, :params, recipe_ingredients_attributes: [:quantity, ingredient_attributes:[:name]])
end
end
This will create a basic form, but we need to include the associated fields. There are several ways to do this, including the "manual" method from RailsCasts and using Cocoon.
The important thing to note is thus:
Every time you call a nested form field, you're basically getting
Rails to add another instance of f.fields_for to your form. The
VITAL thing to note here is you need to have the child_index of the
fields_for block. This is what Rails uses to identify the fields,
and needs to be kept unique.
You can see more about this on an answer I wrote some time back.
For your form, you need the following:
#app/views/recipes/new.html.erb
<%= form_for #recipe do |f| %>
<%= f.text_field :title %>
<%= render "ingredients", locals: {f: f, child_index: Time.now.to_i} %>
<%= link_to "Add Ingredient", recipes_add_field_path, class: "ingredient" %>
<%= f.submit %>
<% end %>
#app/views/recipes/_ingredients.html.erb
<%= f.fields_for :recipe_ingredients, child_index: child_index do |ri| %>
<%= ri.text_field :quantity %>
<%= ri.fields_for :ingredient do |ingredient| %>
<%= ingredient.text_field :name %>
<%= ingredient.text_field :weight %>
<% end %>
<% end %>
#config/routes.rb
resources :recipes do
member "add_field", to: "recipes#add_field"
end
#app/controllers/recipes_controller.rb
class RecipesController < ApplicationController
def add_field
#recipe = Recipe.new
#recipe.recipe_ingredients.build.build_ingredient
render "new"
end
end
#app/assets/javascripts/application.js
$(document).on("click", "a.ingredient", function(){
$.ajax({
url: '/recipes/add_field',
success: function(data){
el_to_add = $(data).html();
$('#recipes').append(el_to_add);
}
error: function(data){
alert("Sorry, There Was An Error!");
}
});
});

Best way to handle nested relations is to use accept_nested_attributes
For better understating here is a link
And to manage ui level form within form here is a gem

Related

creating a record with a has many through association

I would like to create an association to another a model when creating a record.
The models use the has_many through association.
Models
Recipe
class Recipe < ApplicationRecord
attribute :name
attribute :published
has_many :ingridients, dependent: :destroy
has_many :instructions, dependent: :destroy
has_many :recipe_seasons
has_many :seasons, through: :recipe_seasons
accepts_nested_attributes_for :recipe_seasons
validates_presence_of :name
end
Season
class Season < ApplicationRecord
has_many :recipe_seasons
has_many :recipes, through: :recipe_seasons
end
RecipeSeason
class RecipeSeason < ApplicationRecord
belongs_to :recipe
belongs_to :season
validates_presence_of :recipe
validates_presence_of :season
accepts_nested_attributes_for :season
end
Controller
def new
#month = 1
#recipe = Recipe.new
#recipe.recipe_seasons.build(season_id: #month).build_recipe
end
def create
#recipe = Recipe.new(recipe_params)
#recipe.save
redirect_to recipes_path
flash[:notice] = I18n.t("recipe.created")
end
private
def recipe_params
params.require(:recipe)
.permit(:name, :published, recipe_seasons_attributes:[:recipe_id, :season_id ])
end
When the Recipe is created, I'd like a defauly value of #month to be inserted into a record on the table recipe_seasons using the id of the newly created Recipe.
Form
<%= form_with(model: #recipe) do |f| %>
<%= f.label :name %>
<%= f.text_field :name, required: true %>
<%= f.label :published %>
<%= f.check_box :published, class: "form-control", placeholder: "Tick if done" %>
<%= f.submit %>
<% end %>
<%=link_to t("back"), recipes_path %>
When I create a recipe, I would like a record to be inserted into recipe_seasons at the same time, using the id that is created on the recipe as the recipe_id on the table recipe_seasons. For now I will hard code a value for #month that is used for the season_id.
You're actually overdoing and overcomplicating it here. You don't need anything in your RecipeSeason class except:
class RecipeSeason < ApplicationRecord
belongs_to :recipe
belongs_to :season
end
The presence validations are added by default to belongs_to assocations since Rails 5. You do not need nested attributes to just assign assocatiated items.
Normally when dealing with join tables you do not need to explicitly create the join models as they are created implicitly through the assocation:
def create
#recipe = Recipe.new(recipe_params)
# Always check if the record was valid and saved
if #recipe.save
redirect_to recipes_path, status: :created
flash[:notice] = I18n.t("recipe.created")
else
render :new
end
end
def recipe_params
params.require(:recipe)
.permit(:name, :published, season_ids: [])
end
<%= form_with(model: :recipe) do |form| %>
<%= form.collection_select(
:season_ids, # name of the attribute
Season.all, # the collection which should be available as options
:id, # value method
:name # label method
) %>
<% end %>
This uses the setter created by has_many :seasons, through: :recipe_seasons and will automatically create/delete rows in the recipe_seasons table.
If you want to create a default you just set the selected attribute on the select element.
<%= form.collection_select :season_ids, #seasons, :id, :name, selected: #seasons.first.id %>
The only time you need to explicitly create the intermediate model is when its not just used as a simple join table and your passing properties for that table. For example if your have an order form and you want to add a product to the order together with a quantity:
# order.products << #product won't let us pass a quantity
order.line_items.create(
product: #product,
quantity: 50
)
That's when nested attributes actually becomes relevant.

Repeating form fields and updating to database

any help would be most appreciated, I am rather new to Rails.
I have two models a Shopping List and a Product. I'd like to save/update multiple products to a shopping list at a time.
The suggested changes are not updating the models. I've been googling and is "attr_accessor" or find_or_create_by the answer(s)?
Attempt 1 - Existing code
Error
> unknown attribute 'products_attributes' for Product.
Request
Parameters:
{"_method"=>"patch",
"authenticity_token"=>"3BgTQth38d5ykd3EHiuV1hkUqBZaTmedaJai3p9AR1N2bPlHraVANaxxe5lQYaVcWNoydA3Hb3ooMZxx15YnOQ==",
"list"=>
{"products_attributes"=>
{"0"=>{"title"=>"ten", "id"=>"12"},
"1"=>{"title"=>"two", "id"=>"13"},
"2"=>{"title"=>"three", "id"=>"14"},
"3"=>{"title"=>"four", "id"=>"15"},
"4"=>{"title"=>"five", "id"=>"16"},
"5"=>{"title"=>""},
"6"=>{"title"=>""},
"7"=>{"title"=>""},
"8"=>{"title"=>""},
"9"=>{"title"=>""},
"10"=>{"title"=>""}}},
"commit"=>"Save Products",
"id"=>"7"}
Attempt 2 - no errors the page reloads and none of the expected fields are updated. In earnest, I am Googling around and copying and pasting code snippets in the vain hope of unlocking the right combo.
Added to Products mode
class Product < ApplicationRecord
attr_accessor :products_attributes
belongs_to :list, optional: true
end
<%= content_tag(:h1, 'Add Products To This List') %>
<%= form_for(#list) do |f| %>
<%= f.fields_for :products do |pf| %>
<%= pf.text_field :title %><br>
<% end %>
<p>
<%= submit_tag "Save Products" %>
</p>
<% end %>
<%= link_to "Back To List", lists_path %>
list controller
def update
#render plain: params[:list].inspect
#list = List.find(params[:id])
if #list.products.update(params.require(:list).permit(:id, products_attributes: [:id, :title]))
redirect_to list_path(#list)
else
render 'show'
end
list model
class List < ApplicationRecord
has_many :products
accepts_nested_attributes_for :products
end
original do nothing - product model
class Product < ApplicationRecord
belongs_to :list, optional: true
end
If you just want a user to be able to select products and place them on a list you want a many to many association:
class List < ApplicationRecord
has_many :list_items
has_many :products, through: :list_products
end
class ListItem < ApplicationRecord
belongs_to :list
belongs_to :product
end
class Product < ApplicationRecord
has_many :list_items
has_many :lists, through: :list_products
end
This avoids creating vast numbers of duplicates on the products table and is known as normalization.
You can then select existing products by simply using a select:
<%= form_for(#list) do |f| %>
<%= f.label :product_ids %>
<%= f.collection_select(:product_ids, Product.all, :name, :id) %>
# ...
<% end %>
Note that this has nothing to with nested routes or nested attributes. Its just a select that uses the product_ids setter that's created by the association. This form will still submit to /lists or /lists/:id
You can whitelist an array of ids by:
def list_params
params.require(:list)
.permit(:foo, :bar, product_ids: [])
end
To add create/update/delete a bunch of nested records in one form you can use accepts_nested_attributes_for together with fields_for:
class List < ApplicationRecord
has_many :list_items
has_many :products, through: :list_products
accepts_nested_attributes_for :products
end
<%= form_for(#list) do |f| %>
<%= form.fields_for :products do |pf| %>
<%= pf.label :title %><br>
<%= pf.text_field :title %>
<% end %>
# ...
<% end %>
Of course fields_for won't show anything if you don't seed the association with records. That's where that loop that you completely misplaced comes in.
class ListsController < ApplicationController
# ...
def new
#list = List.new
5.times { #list.products.new } # seeds the form
end
def edit
#list = List.find(params[:id])
5.times { #list.products.new } # seeds the form
end
# ...
def update
#list = List.find(params[:id])
if #list.update(list_params)
redirect_to #list
else
render :new
end
end
private
def list_params
params.require(:list)
.permit(
:foo, :bar,
product_ids: [],
products_attrbutes: [ :title ]
)
end
end
Required reading:
Rails Guides: Nested forms
ActiveRecord::NestedAttributes
fields_for

Associate many records to one with has_many_through

I'm trying to associate several dinners to a meal with a has_many: through relationship when the user hits "save". My question is not with the mechanics of has_many: through. I know how to set that up and I have it working in the Rails console, but I just don't know how to set up the view to associate several records at once.
I have models set up like this:
class Dinner < ApplicationRecord
has_one :user
has_many :meals
has_many :meal_plans, through: :meals
end
class MealPlan < ApplicationRecord
belongs_to :user
has_many :meals
has_many :dinners, through: :meals
end
class Meal < ApplicationRecord
belongs_to :dinner
belongs_to :meal_plan
end
With a meal plan controller:
def create
#meal_plan = current_user.meal_plans.build(meal_plan_params)
respond_to do |format|
if #meal_plan.save
format.html { redirect_to root_path, notice: 'Dinner was successfully created.' }
end
end
end
private
def meal_plan_params
params.require(:meal_plan).permit(dinners: [])
end
My question is about the view, in the new view, I create a #meal_plan and I want to pass several different dinners into the meal plan. Below the value: #dinners is just 7 random dinners pulled from the Dinners table.
<%= form_with model: #meal_plan do |f| %>
<%= f.hidden_field(:dinners, value: #dinners)%>
<%= f.submit 'Save'%>
<% end %>
Again, I've gotten this to work by running something like `usr.meal_plans.create(dinners: [d1, d2])`` in the Rails console but I don't
You can use the form option helpers to generate selects or checkboxes:
<%= form_with model: #meal_plan do |f| %>
<%= f.collection_select :dinner_ids, Dinner.all, :id, :name, multiple: true %>
<%= f.collection_checkboxes :dinner_ids, Dinner.all, :id, :name %>
<%= f.submit 'Save'%>
<% end %>
_ids is a special setter / getter generated by ActiveRecord for has many assocations. You pass an array of ids and AR will take care of inserting/removing the join table rows (meals).
You also need to change the name in your params whitelist:
def meal_plan_params
params.require(:meal_plan).permit(dinner_ids: [])
end
If you want to to pass an array through hidden inputs you can do it like so:
<% #dinners.each do |dinner| >
<%= hidden_field_tag "meal_plans[dinner_ids][]", dinner.id %>
<% end %>
See Pass arrays & objects via querystring the Rack/Rails way for an explaination of how this works.

rails nasted model form

I have model association like this
post.rb
title:string description:text
class Post < ActiveRecord::Base
belongs_to :user
has_many :items
accepts_nested_attributes_for :items
end
item.rb
post_id:integer order:integer
class Item < ActiveRecord::Base
belongs_to :post
has_one :link
has_one :movie
has_one :photo
has_one :quate
end
link, movie, photo, quate.rb
link.rb : item_id:integer url:string url-text:string
movie.rb : item_id:integer youtube-url:string
photo.rb : item_id:integer image:string comment:string title:string
quate.rb : item_id:integer quate:string q-url:string q-title:string
belongs_to :item
I want to build user-post application by ruby on rails.
Item model has order column ,so user can choose and add whatever movie, link , photo to build there own post.
How can I build form for these nasted models?
This might not be as defined as you need; I'll delete if required.
You have a huge antipattern with your belongs_to models. They look like they represent a single data-set, and I would merge them into the Item model, using an enum to differentiate their state:
#app/models/item.rb
class Item < ActiveRecord::Base
#schema id | post_id | state | url | title | comment | created_at | updated_at
belongs_to :post
enum state: [:link, :movie, :photo, :quate]
end
This will give you the ability to create instances of Item, which you can then assign different "states":
#item = Item.new
#item.state = :link
Although it means changing your pattern, it gives you the ability to store items for each post directly (instead of having to append them with another model):
#app/controllers/posts_controller.rb
class PostsController < ApplicationController
def new
#post = Post.new
#post.items.build
end
def create
#post = Post.new post_params
#post.save
end
private
def post_params
params.require(:post).permit(items_attributes: [:state, :url, :title, :comment])
end
end
#app/views/posts/new.html.erb
<%= form_for #post do |f| %>
<%= f.fields_for :items do |i| %>
<%= i.select :state, Item.states.keys.map {|role| [role.titleize,role]}) %>
<%= i.text_field :url %>
<%= i.text_field :title %>
<%= i.text_field :comment %>
<% end %>
<%= f.submit %>
<% end %>
You also need to make sure you have accepts_nested_attributes_for in your Post model:
#app/models/post.rb
class Post < ActiveRecord::Base
has_many :items
accepts_nested_attributes_for :items
end

How to setup a deeply nested form in Rails 3.2

I'm trying to build a rather complex nested form in rails and am stuck.
Basically, I have three models - Applicant, DataPoint, ApplicantDataPointValue .
The user can create a new DataPoint, give it a name ("gender" etc.) select it's type ("string","integer" etc.). The type determines what column the data will eventually be saved in in the ApplicantDataPointValue table.
I then want the user, when they're creating a new Applicant, to be able to add a value for each DataPoint into the ApplicantDataPointValue table
My models look like the following:
Applicant:
class Applicant < ActiveRecord::Base
has_many :applicant_data_point_values, dependent: :destroy
has_many :data_points, :through => :applicant_data_point_values
accepts_nested_attributes_for :data_points
accepts_nested_attributes_for :applicant_data_point_values
attr_accessible :data_points_attributes, :applicant_data_point_values_attributes
end
DataPoint:
class DataPoint < ActiveRecord::Base
has_many :applicant_data_point_values
has_many :applicants, :through => :applicant_data_point_values
accepts_nested_attributes_for :applicant_data_point_values
end
ApplicantDataPointValue:
class ApplicantDataPointValue < ActiveRecord::Base
belongs_to :data_point
belongs_to :applicant
end
But I'm at a loss to what to do in the 'new' and 'create' sections of my controller or how to construct the form.
Any insight would be greatly appreciated.
From what I understand, the form for the User will also have multiple ApplicantDataPointValue fields. (but that form won't allow creating of new DataPoint fields, right?)
In the controller new action, you'll want to set up your model with associated data point values:
def new
#user = User.new
DataPoint.all.each do |data_point|
applicant_data_point_value = #user.applicant_data_point_values.build
applicant_data_point_value.data_point = data_point
end
end
And then, display a text box for each data point value.
<%= form_for #user do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<% #user.applicant_data_point_values.each do |data_point_value| %>
<%= f.fields_for :applicant_data_point_values, data_point_value do |fields| %>
<%= fields.label :value, data_point_value.data_point.type %>
<%= fields.text_field :value %>
<% end %>
<% end %>
Reference: http://railscasts.com/episodes/196-nested-model-form-part-1

Resources