I'm trying to create a form in Rails 5.2 for a model with a has_many :through relationship to another model. The form needs to include nested attributes for the other model. However, the params are not nesting properly. I've created the following minimal example.
Here are my models:
class Order < ApplicationRecord
has_many :component_orders, dependent: :restrict_with_exception
has_many :components, through: :component_orders
accepts_nested_attributes_for :components
end
class Component < ApplicationRecord
has_many :component_orders, dependent: :restrict_with_exception
has_many :orders, through: :component_orders
end
class ComponentOrder < ApplicationRecord
belongs_to :component
belongs_to :order
end
The Component and Order models each have one attribute: :name.
Here is my form code:
<%= form_with model: #order do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= fields_for :components do |builder| %>
<%= builder.label :name %>
<%= builder.text_field :name %>
<% end %>
<%= f.submit %>
<% end %>
When I fill out the form, I get the following params:
{"utf8"=>"✓", "authenticity_token"=>"ztA1D9MBp1IRPsiZnnSAIl2sEYjFeincKxivoq0/pUO+ptlcfi6VG+ibBnSREqGq3VzckyRfkQtkCTDqvnTDjg==", "order"=>{"name"=>"Hello"}, "components"=>{"name"=>"World"}, "commit"=>"Create Order", "controller"=>"orders", "action"=>"create"}
Specifically, note that instead of a param like this:
{
"order" => {
"name" => "Hello",
"components_attributes" => {
"0" => {"name" => "World"}
}
}
}
There are separate keys for "order" and "components" at the same level. How can I cause these attributes to nest properly? Thank you!
EDIT: Here is my controller code:
class OrdersController < ApplicationController
def new
#order = Order.new
end
def create
#order = Order.new(order_params)
if #order.save
render json: #order, status: :created
else
render :head, status: :unprocessable_entity
end
end
private
def order_params
params.require(:order).permit(:name, components_attributes: [:name])
end
end
You should include accepts_nested_attributes_for :components in the Order model.
class Order < ApplicationRecord
has_many :component_orders, dependent: :restrict_with_exception
has_many :components, through: :component_orders
accepts_nested_attributes_for :components
end
And change
<%= fields_for :components do |builder| %>
to
<%= f.fields_for :components do |builder| %>
to get the desired params. accepts_nested_attributes_for :components creates a method namely components_attributes
More Info here
Related
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.
I have a little project management app.
In the app I have a Project, Item and Delivery Model.
class Project < ApplicationRecord
has_many :locations, dependent: :destroy
has_many :items, dependent: :destroy
has_many :deliveries, dependent: :destroy
end
class Item < ApplicationRecord
belongs_to :project
belongs_to :location, optional: true
has_many :delivery_items, dependent: :destroy
has_many :deliveries, through: :delivery_items
enum status: [:unscheduled, :scheduled, :delivered]
end
class Delivery < ApplicationRecord
belongs_to :project
has_many :delivery_items, dependent: :destroy
has_many :items, through: :delivery_items
enum status: [ :unapproved, :approved, :scheduled ]
end
I also have a delivery_item join table
class DeliveryItem < ApplicationRecord
belongs_to :delivery
belongs_to :item
end
I have added a new Model called location, which is a way of classifying the items into a group on the project.
class Location < ApplicationRecord
belongs_to :project
has_many :items
has_many :part_numbers, through: :items
def bulkadd(delivery)
self.items.each do |row|
batch << Product.new(row)
end
end
end
At the moment the user individually adds items to deliveries via a form on the page
<h6>Add to Delivery</h6>
<%= form_for #delivery_item, html: {class: 'form-inline'} do |form| %>
<div class="form-group">
<%= form.collection_select :delivery_id, #project.deliveries.all, :id, :date, placeholder: 'Add to Delivery', class: 'form-control' %>
</div>
<%= form.hidden_field :item_id, value: item.id %>
<div class="form-group">
<%= form.submit "Add",class: 'btn btn-primary' %>
</div>
<% end %>
I would like to simplify the process by adding a bulk add button to each location which would add all of the associated items to the delivery selected has many items.
I know that I will need the delivery_item(delivery, item).
I just cant seem to get the final part to work in my brain
When you create a has_many or has_and_belongs_to_many assocation the macro creates an others_ids setter/getter. In this case item_ids= which will automatically add/remove rows from the join table.
Its really easy to use this together with the form option helpers to create a select where the user can choose multiple records:
<%= form_for(#delivery) do |form| %>
<div class="field">
<%= f.label :item_ids, 'Select the items' %>
<%= f.collection_select :item_ids, #items, :id, :name, multiple: true %>
</div>
<% end %>
Or if you prefer checkboxes:
<%= form_for(#delivery) do |form| %>
<div class="field">
<%= f.label :item_ids, 'Select the items' %>
<%= f.collection_check_boxes :item_ids, #items, :id, :name %>
</div>
<% end %>
Replace :name with whatever attribute you want to use for the option text.
class DeliveriesController < ApplicationController
before_action :set_delivery, only: [:show, :edit, :update, :destroy]
# This avoids a database query in the view
before_action :set_items, only: [:new, :edit]
# POST /deliveries
def create
#delivery = Delivery.new(delivery_params)
if #delivery.save
redirect_to #delivery, notice: 'Delivery created'
else
set_items
render :new
end
end
# PUT|PATCH /deliveries/1
def update
if #delivery.update(delivery_params)
redirect_to #delivery, notice: 'Delivery updated'
else
set_items
render :edit
end
end
private
def set_delivery
#delivery = Delivery.find(params[:id])
end
def set_items
#items = Item.all
end
def delivery_item_params
# Passing the hash `item_ids: []` allows an array of permitted scalar types.
params.require(:delivery)
.permit(:foo, :bar, :baz, item_ids: [])
end
end
I have a rails 4 application that has a params block that looks like:
def store_params
params.require(:store).permit(:name, :description, :user_id, products_attributes: [:id, :type, { productFields: [:type, :content ] } ])
end
but I'm getting the error:
ActiveRecord::AssociationTypeMismatch in StoresController#create
ProductField expected, got Array
The parameters I'm trying to insert look like:
Parameters: {"utf8"=>"✓", "store"=>{"name"=>"fdsaf", "description"=>"sdfd","products_attributes"=>{"0"=>{"productFields"=>{"type"=>"", "content"=>""}}}}, "type"=>"Magazine", "commit"=>"Create store"}
My models are
Store (has a has_many :products)
Product (has a has_many :productFields and belongs_to :store)
ProductField (has a belongs_to :product)
My view looks like:
<%= f.fields_for :products do |builder| %>
<%= render 'product_fields', f: builder %>
<% end %>
and then the product_fields partial:
<%= f.fields_for :productFields do |builder| %>
<%= builder.text_field :type%>
<%= builder.text_area :content %>
<% end %>
Make sure your Product and Store models have:
accepts_nested_attributes_for
inside them.
Then, if your calling nested fields_for like that, make sure you build them (in the controller), something like:
product = #store.products.build
product.productFields.build
Firstly you should have set accepts_nested_attributes_for in your models like this
class Store < ActiveRecord::Base
has_many :products
accepts_nested_attributes_for :products
end
class Product < ActiveRecord::Base
has_many :product_fields
belongs_to :store
accepts_nested_attributes_for :product_fields
end
class ProductField < ActiveRecord::Base
belongs_to :products
end
Secondly, your store_params should look like this
def store_params
params.require(:store).permit(:name, :description, :user_id, products_attributes: [:id, :type, { product_fields_attributes: [:type, :content ] } ])
end
I'm trying to setup the following: A User has many Groups through Memberships, a Group has many Events, and an Event has many Posts.
On my view to show a group with all of its events, I want a user to be able to write a new post by selecting the correct group from a drop down, writing a comment and submit. I'm currently using a collection_select to create the post, but the event_id is not getting passed to ActiveRecord, i.e. posts are created, but they do not have event_ids (or even comments):
class User < ActiveRecord::Base
has_many :memberships
has_many :groups, through: :memberships
has_many :posts
end
class Membership < ActiveRecord::Base
belongs_to :group
belongs_to :user
end
class Group < ActiveRecord::Base
has_many :memberships
has_many :events, dependent: :destroy
has_many :users, through: :memberships
end
class Event < ActiveRecord::Base
belongs_to :group
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :event
belongs_to :user
end
class GroupsController < ApplicationController
def show
#define new post
#new_post = Post.new
end
end
class PostsController < ApplicationController
def create
if #post = Post.create(params[post_params])
flash[:success] = "Post Created!"
else
redirect_to group_url
end
end
private
def post_params
params.require(:post).permit(:event_id, :comment)
end
end
<h1>New Post:</h1>
<%=form_for([#new_post]) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class = "field">
<%= f.label :event_name %>
<%= f.collection_select(:event_id, Event.all, :id, :title) %>
</div>
<div class = "field">
<%= f.text_area :comment, placeholder: "New Post..." %>
</div>
<%=f.submit "Submit", class: "btn btn-large btn-primary" %>
<%end%>
I have a feeling that because the routes are nested, group_id never is passed to the Posts controller, and so can never be set. But I'm sure there's a lot more wrong than that...
can you try to pass Post.create(post_params) instead of Post.create(params[post_params])
post_params is actually a full hash extracted from the params so you should not pass it to params again
If you want to add user_id
you should add to your view something like this
<%= f.hidden_field :user_id, value: current_user.id %>
I'm new to both programming and Ruby on Rails. I'm just trying with a sample 2 level deep nesting. When I followed Ryan's Scraps (http://ryandaigle.com/articles/2009/2/1/what-s-new-in-edge-rails-nested-attributes) for 1 level deep nesting everything well and good but when I extended for 2 level deep I'm getting
NameError in ParentsController#new
uninitialized constant Child::Grandchild
My models are like this
class Parent < ActiveRecord::Base
has_many :children
accepts_nested_attributes_for :children, :allow_destroy => true
end
class Child < ActiveRecord::Base
belongs_to :parent
has_many :grandchildren
accepts_nested_attributes_for :grandchildren
end
class GrandChild < ActiveRecord::Base
belongs_to :child
end
And my controller : new method for parent is ->
def new
#parent = Parent.new
2.times do
child = #parent.children.build
2.times {child.grandchildren.build}
end
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => #parent }
end
end
Don't know what's the error , when I modified the models into
class Parent < ActiveRecord::Base
has_many :children, :through => :grandchildren
has_many :grandchildren
accepts_nested_attributes_for :children, :allow_destroy => true
accepts_nested_attributes_for :grandchildren, :allow_destroy => true
end
class Child < ActiveRecord::Base
belongs_to :parent
has_many :grandchildren
accepts_nested_attributes_for :grandchildren
end
class GrandChild < ActiveRecord::Base
belongs_to :parent
belongs_to :child
end
then I'll get following error-- uninitialized constant Parent::Grandchild..
I Don't know its a silly mistake or what...
Thanks
I have edited my question which is my real requirement. Instead of creating parent, children and grandchildren at a time as mentioned earlier I want to create parent first then child and grandchild together. I have edited the above code as mentioned below,
My model:
class Parent < ActiveRecord::Base
has_many :children
has_many :grand_children
accepts_nested_attributes_for :children, :allow_destroy => true
accepts_nested_attributes_for :grand_children, :allow_destroy => true
end
class Child < ActiveRecord::Base
belongs_to :parent
has_many :grand_children
accepts_nested_attributes_for :grand_children
end
class GrandChild < ActiveRecord::Base
belongs_to :parent
belongs_to :child
end
My children controller -new method:
def new
#parent = Parent.find(params[:parent_id])
child = Child.new
child.grand_children.build
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => #child }
end
end
My children _form template is
<%= form_for([#parent, #parent.children.build]) do |form| %>
<div>
<%= form.label :name %><br />
<%= form.text_field :name %>
</div>
<div class="field">
<%= form.label :sex %><br />
<%= form.text_field :sex %>
</div>
<div>
<%= form.fields_for :grand_children do |grand_child_form| %>
<%= render :partial => "grand_children/form", :locals => { :form => grand_child_form} %>
<% end %>
</div>
<% end %>
Here I'm not getting any error as such but when I select new child the grand_child is not appearing,
<%= form.fields_for :grand_children do |grand_child_form| %>
<%= render :partial => "grand_children/form", :locals => { :form => grand_child_form} %>
<% end %>
is not getting reflected at all .
Thanks in advance
I believe your error is your association name, but I don't have a rails terminal to try it on.
has_many :grandchildren
should be
has_many :grand_children
GrandChild is camel cased, and would be grand_child underscored, and grand_children when tableized.
I believe rails is using the underscore/tableize methods when doing it's associations, if you're ever having this problem again with an association, take your class name in a console and do
"GrandChild".tableize
"GrandChild".underscore
That will give you your association names.