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.
Related
I am trying to make a player character generator. I have a form that hopefully will allow me to attach skills with their values to a character sheet model. I made models like this:
class CharacterSheet < ApplicationRecord
has_many :character_sheet_skills, dependent: :destroy
has_many :skills, through: :character_sheet_skills
belongs_to :user
accepts_nested_attributes_for :skills
end
class Skill < ApplicationRecord
has_many :character_sheet_skills, dependent: :destroy
has_many :character_sheets, through: :character_sheet_skills
attr_reader :value
end
class CharacterSheetSkill < ApplicationRecord
belongs_to :skill
belongs_to :character_sheet
end
Character sheet model holds data about player character and skill model has all skills available in game. In CharacterSheetSkill I'd like to store the skills that the player chooses for his character together with an integer field setting the skill value.
When opening form, I already have a full list of skills in database. All I want to do in form is create a character sheet that has all of these skills with added value. I tried using "fields_for" in form, but I couldn't really get that to work. Right now it looks like this:
<%= simple_form_for [#user, #sheet] do |f| %>
<%= f.input :name %>
<%= f.input :experience, readonly: true, input_html: {'data-target': 'new-character-sheet.exp', class: 'bg-transparent'} %>
...
<%= f.simple_fields_for :skills do |s| %>
<%= s.input :name %>
<%= s.input :value %>
<% end %>
<% end %>
How can I make that form so it saves character sheet together with CharacterSheetSkills?
A better idea here is to use skills as a normalization table where you store the "master" definition of a skill such as the name and the description.
class CharacterSheetSkill < ApplicationRecord
belongs_to :skill
belongs_to :character_sheet
delegate :name, to: :skill
end
You then use fields_for :character_sheet_skills to create rows on the join table explicitly:
<%= f.fields_for :character_sheet_skills do |cs| %>
<fieldset>
<legend><%= cs.name %></legend>
<div class="field">
<%= cs.label :value %>
<%= cs.number_field :value %>
</div>
<%= cs.hidden_field :skill_id %>
</fieldset>
<% end %>
Instead of a hidden fields you could use a select if you want let the user select the skills.
Of course nothing will show up unless you "seed" the inputs:
class CharacterSheetController < ApplicationController
def new
#character_sheet = CharacterSheet.new do |cs|
# this seeds the association so that the fields appear
Skill.all.each do |skill|
cs.character_sheet_skills.new(skill: skill)
end
end
end
def create
#character_sheet = CharacterSheet.new(character_sheet_params)
if #character_sheet.save
redirect_to #character_sheet
else
render :new
end
end
private
def character_sheet_params
params.require(:character_sheet)
.permit(
:foo, :bar, :baz,
character_sheet_skill_attributes: [:skill_id, :value]
)
end
end
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
I am using Rails 5.1 and im having some issues saving params on an n:n relationship.
I have three models:
class Course < ApplicationRecord
belongs_to :studio
has_many :reviews, dependent: :destroy
has_many :has_category
has_many :categories, through: :has_category
validates :name, presence: true
end
class Category < ApplicationRecord
has_many :has_category
has_many :courses, through: :has_category
end
class HasCategory < ApplicationRecord
belongs_to :category
belongs_to :course
end
and a simple form to create a new course with different categories using check_box_tag (not sure if using it correctly though)
<%= simple_form_for [#studio, #course] do |f| %>
<%= f.input :name %>
<%= f.input :description %>
<% #categories.each do |category| %>
<%= check_box_tag "course[category_ids][]", category.id, true %>
<%= category.name%>
<% end %>
<%= f.button :submit %>
<% end %>
And all is permitted and created on the courses controller:
def new
#studio = Studio.find(params[:studio_id])
#course = Course.new
#course.studio = #studio
#categories = Category.all
end
def create
#studio = Studio.find(params[:studio_id])
#course = Course.new(course_params)
#course.studio = #studio
#categories = params[:category_ids]
if #course.save
redirect_to course_path(#course)
else
render :new
end
end
def course_params
params.require(:course).permit(:studio_id, :name, :description, :category_ids)
end
With better_errors i know the categories are being requested, here the request info:
"course"=>{"name"=>"Course test", "description"=>"testing", "category_ids"=>["2", "3"]}, "commit"=>"Create Course", "controller"=>"courses", "action"=>"create", "studio_id"=>"16"}
but the categories are not saved on course_params, HasCategory instance or on the Course, i´ve tried with #course.categories = params[:category_ids] and other solutions without success.
How do i save the categories to the courses?
Try the following
Change the strong parameter with category_ids: []
def course_params
params.require(:course).permit(:studio_id, :name, :description, category_ids: [])
end
Comment out this line #categories = params[:category_ids]
Hope it helps
I have three model classes related to each other.
class Student < ActiveRecord::Base
has_many :marks
belongs_to :group
accepts_nested_attributes_for :marks,
reject_if: proc { |attributes| attributes['rate'].blank?},
allow_destroy: true
end
This class describes a student that has many marks and I want to create a Student record along with his marks.
class Mark < ActiveRecord::Base
belongs_to :student, dependent: :destroy
belongs_to :subject
end
Marks are related both to the Subject and a Student.
class Subject < ActiveRecord::Base
belongs_to :group
has_many :marks
end
When I try to create the nested fields of marks in loop labeling them with subject names and passing into in it's subject_id via a loop a problem comes up - only the last nested field of marks is saved correctly, whilst other fields are ignored. Here's my form view code:
<%= form_for([#group, #student]) do |f| %>
<%= f.text_field :student_name %>
<%=f.label 'Student`s name'%><br>
<%= f.text_field :student_surname %>
<%=f.label 'Student`s surname'%><br>
<%=f.check_box :is_payer%>
<%=f.label 'Payer'%>
<%= f.fields_for :marks, #student.marks do |ff|%>
<%#group.subjects.each do |subject| %><br>
<%=ff.label subject.subject_full_name%><br>
<%=ff.text_field :rate %>
<%=ff.hidden_field :subject_id, :value => subject.id%><br>
<%end%>
<% end %>
<%= f.submit 'Add student'%>
<% end %>
Here`s my controller code:
class StudentsController<ApplicationController
before_action :authenticate_admin!
def new
#student = Student.new
#student.marks.build
#group = Group.find(params[:group_id])
#group.student_sort
end
def create
#group = Group.find(params[:group_id])
#student = #group.students.new(student_params)
if #student.save
redirect_to new_group_student_path
flash[:notice] = 'Студента успішно додано!'
else
redirect_to new_group_student_path
flash[:alert] = 'При створенні були деякі помилки!'
end
end
private
def student_params
params.require(:student).permit(:student_name, :student_surname, :is_payer, marks_attributes: [:id, :rate, :subject_id, :_destroy])
end
end
How can I fix it?
#student.marks.build
This line will reserve an object Mark.
If you want multi marks, May be you need something like this in new action :
#group.subjects.each do |subject|
#student.marks.build(:subject=> subject)
end
Hope useful for you.
I'm trying to create an order form that also serves as a list for all the available products. I have the models with the has_many through associations, and I managed to create collection_select fields to create the records on the join model which works fine, but I cannot figure out how to build a list showing each product and accepting additional attributes for the join records.
My models (somewhat simplified, I also have images and such):
class Supply < ActiveRecord::Base
attr_accessible :available, :name
has_many :order_lines
has_many :orders, through: :order_lines
accepts_nested_attributes_for :order_lines
end
class Order < ActiveRecord::Base
attr_accessible :month, :user_id, :order_lines_attributes
belongs_to :user
has_many :order_lines
has_many :supplies, through: :order_lines
accepts_nested_attributes_for :order_lines
end
class OrderLine < ActiveRecord::Base
attr_accessible :amount, :supply_id, :order_id
belongs_to :order
belongs_to :supply
validates_presence_of :amount
end
Order controller:
def new
#order = Order.new
#supplies = Supply.available
#supplies.count.times { #order.order_lines.build }
respond_to do |format|
format.html # new.html.erb
format.json { render json: #supply_order }
end
end
Order_line field from Order form:
<%= f.fields_for :order_lines do |order_line| %>
<%= order_line.label :amount %>
<%= order_line.number_field :amount %>
<%= order_line.label :supply_id %>
<%= order_line.collection_select(:supply_id, #supplies, :id, :name) %>
<% end %>
In place of this I would like to have a list with one order_line per supply (with name and other attributes) which gets saved if there's an amount defined. Any help appreciated!
Just add an order_item for each supply. Instead of this:
#supplies.count.times { #order.order_lines.build }
Do this:
#supplies.each { |supply| #order.order_lines.build(supply: supply) }
Then in your order form you don't need the collection_select for supply_id, just show the name:
<%= order_line.object.supply.name %>
Make sure you ignore nested attributes with an empty amount:
accepts_nested_attributes_for :order_lines, reject_if: proc { |attr| attr.amount.nil? || attr.amount.to_i == 0 }
You don't need the accepts_nested_attributes_for in the Supply model.