Rails nested fields creation in loop - ruby-on-rails

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.

Related

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

Request parameters not saving on model_params on n:n relationship with check_box_tag

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

Editing associated models the rails way

I have a model "Student" and every student has_many parents (a mother and a father in the parents table). In my UI I want to be able to add the parents and the student on the same page. So when I click on "Add student" the view 'students/new' is rendered. In this view I have the regular stuff for adding a student (<% form_for #student....) so far so good. But now I also want to provide the form for adding a mother and a father for this student on the same page. I know I could place a link to 'parents/new' somewhere but that is not really user-friendly in my opinion.
What are my options and what would you recommend?
Your best bet would be using nested_forms with accepts_nested_attributes_for like below
#student.rb
Class Student < ActiveRecord::Base
has_many :parents
accepts_nested_attributes_for :parents
end
#students_controller.rb
def new
#student = Student.new
#student.parents.build
end
def create
#student = Student.new(student_params)
if #student.save
redirect_to #student
else
render 'new'
end
private
def student_params
params.require(:student).permit(:id, :student_attr_1, :student_attrr_2, parents_attributes: [:id, :father, :mother])
end
#students/new
<%= form_for #student do |f| %>
---student code here ---
<%= f.fields_for :parents do |p| %>
<%= p.label :father, :class => "control-label" %>
<%= p.text_field :father %>
<%= p.label :fmother, :class => "control-label" %>
<%= p.text_field :mother %>
<% end %>
Inside your form you can add fields_for helper
<%= fields_for #student.father do |father| %>
<% father.text_field :name %> # will be appropriate father name
....
<% end %>
Check also rails fields_for
I'd use the ObjectForm concept:
Here is one good article about this pattern.
Here's an introduction to the implementation:
Class Student < ActiveRecord::Base
has_many :parents
end
class CompleteStudentForm
include ActiveModel::Model
attr_acessor :name, :age #student attributes
attr_accessor :father_name, :mother_name #assuming that Parent model has only the :name attribute
validates_presence_of :name, :age
# simply add custom validation messages for fields
validates_presence_of :father_name, message: 'Fill your father name'
validates_presence_of :mother_name, message: 'Fill your mother name'
def save
persist! if valid?
end
private
def persist!
student = Student.new(name: #name, age: #age)
student.parents << Parent.new(name: #father_name)
student.parents << Parent.new(name: #mother_name)
student.save!
end
end
class StudentController
def create
#student = CompleteStudentForm.new(params[:complete_student_form])
if #student.save
redirect_to :show, #student
else
render :new
end
end
end

Ruby on Rails: create records for multiple models with one form and one submit

I have a 3 models: quote, customer, and item. Each quote has one customer and one item. I would like to create a new quote, a new customer, and a new item in their respective tables when I press the submit button. I have looked at other questions and railscasts and either they don't work for my situation or I don't know how to implement them.
quote.rb
class Quote < ActiveRecord::Base
attr_accessible :quote_number
has_one :customer
has_one :item
end
customer.rb
class Customer < ActiveRecord::Base
attr_accessible :firstname, :lastname
#unsure of what to put here
#a customer can have multiple quotes, so would i use has_many or belongs_to?
belongs_to :quote
end
item.rb
class Item < ActiveRecord::Base
attr_accessible :name, :description
#also unsure about this
#each item can also be in multiple quotes
belongs_to :quote
quotes_controller.rb
class QuotesController < ApplicationController
def index
#quote = Quote.new
#customer = Customer.new
#item = item.new
end
def create
#quote = Quote.new(params[:quote])
#quote.save
#customer = Customer.new(params[:customer])
#customer.save
#item = Item.new(params[:item])
#item.save
end
end
items_controller.rb
class ItemsController < ApplicationController
def index
end
def new
#item = Item.new
end
def create
#item = Item.new(params[:item])
#item.save
end
end
customers_controller.rb
class CustomersController < ApplicationController
def index
end
def new
#customer = Customer.new
end
def create
#customer = Customer.new(params[:customer])
#customer.save
end
end
my form for quotes/new.html.erb
<%= form_for #quote do |f| %>
<%= f.fields_for #customer do |builder| %>
<%= label_tag :firstname %>
<%= builder.text_field :firstname %>
<%= label_tag :lastname %>
<%= builder.text_field :lastname %>
<% end %>
<%= f.fields_for #item do |builder| %>
<%= label_tag :name %>
<%= builder.text_field :name %>
<%= label_tag :description %>
<%= builder.text_field :description %>
<% end %>
<%= label_tag :quote_number %>
<%= f.text_field :quote_number %>
<%= f.submit %>
<% end %>
When I try submitting that I get an error:
Can't mass-assign protected attributes: item, customer
So to try and fix it I updated the attr_accessible in quote.rb to include :item, :customer but then I get this error:
Item(#) expected, got ActiveSupport::HashWithIndifferentAccess(#)
Any help would be greatly appreciated.
To submit a form and it's associated children you need to use accepts_nested_attributes_for
To do this, you need to declare it at the model for the controller you are going to use (in your case, it looks like the Quote Controller.
class Quote < ActiveRecord::Base
attr_accessible :quote_number
has_one :customer
has_one :item
accepts_nested_attributes_for :customers, :items
end
Also, you need to make sure you declare which attributes are accessible so you avoid other mass assignment errors.
If you want add info for diferent models i suggest to apply nested_model_form like this reference: http://railscasts.com/episodes/196-nested-model-form-part-1?view=asciicast.
This solution is very simple and cleanest.

Rails has_many through form with checkboxes and extra field in the join model

I'm trying to solve a pretty common (as I thought) task.
There're three models:
class Product < ActiveRecord::Base
validates :name, presence: true
has_many :categorizations
has_many :categories, :through => :categorizations
accepts_nested_attributes_for :categorizations
end
class Categorization < ActiveRecord::Base
belongs_to :product
belongs_to :category
validates :description, presence: true # note the additional field here
end
class Category < ActiveRecord::Base
validates :name, presence: true
end
My problems begin when it comes to Product new/edit form.
When creating a product I need to check categories (via checkboxes) which it belongs to. I know it can be done by creating checkboxes with name like 'product[category_ids][]'. But I also need to enter a description for each of checked relations which will be stored in the join model (Categorization).
I saw those beautiful Railscasts on complex forms, habtm checkboxes, etc. I've been searching StackOverflow hardly. But I haven't succeeded.
I found one post which describes almost exactly the same problem as mine. And the last answer makes some sense to me (looks like it is the right way to go). But it's not actually working well (i.e. if validation fails). I want categories to be displayed always in the same order (in new/edit forms; before/after validation) and checkboxes to stay where they were if validation fails, etc.
Any thougts appreciated.
I'm new to Rails (switching from CakePHP) so please be patient and write as detailed as possible. Please point me in the right way!
Thank you. : )
Looks like I figured it out! Here's what I got:
My models:
class Product < ActiveRecord::Base
has_many :categorizations, dependent: :destroy
has_many :categories, through: :categorizations
accepts_nested_attributes_for :categorizations, allow_destroy: true
validates :name, presence: true
def initialized_categorizations # this is the key method
[].tap do |o|
Category.all.each do |category|
if c = categorizations.find { |c| c.category_id == category.id }
o << c.tap { |c| c.enable ||= true }
else
o << Categorization.new(category: category)
end
end
end
end
end
class Category < ActiveRecord::Base
has_many :categorizations, dependent: :destroy
has_many :products, through: :categorizations
validates :name, presence: true
end
class Categorization < ActiveRecord::Base
belongs_to :product
belongs_to :category
validates :description, presence: true
attr_accessor :enable # nice little thingy here
end
The form:
<%= form_for(#product) do |f| %>
...
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name %>
</div>
<%= f.fields_for :categorizations, #product.initialized_categorizations do |builder| %>
<% category = builder.object.category %>
<%= builder.hidden_field :category_id %>
<div class="field">
<%= builder.label :enable, category.name %>
<%= builder.check_box :enable %>
</div>
<div class="field">
<%= builder.label :description %><br />
<%= builder.text_field :description %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
And the controller:
class ProductsController < ApplicationController
# use `before_action` instead of `before_filter` if you are using rails 5+ and above, because `before_filter` has been deprecated/removed in those versions of rails.
before_filter :process_categorizations_attrs, only: [:create, :update]
def process_categorizations_attrs
params[:product][:categorizations_attributes].values.each do |cat_attr|
cat_attr[:_destroy] = true if cat_attr[:enable] != '1'
end
end
...
# all the rest is a standard scaffolded code
end
From the first glance it works just fine. I hope it won't break somehow.. :)
Thanks all. Special thanks to Sandip Ransing for participating in the discussion. I hope it will be useful for somebody like me.
use accepts_nested_attributes_for to insert into intermediate table i.e. categorizations
view form will look like -
# make sure to build product categorizations at controller level if not already
class ProductsController < ApplicationController
before_filter :build_product, :only => [:new]
before_filter :load_product, :only => [:edit]
before_filter :build_or_load_categorization, :only => [:new, :edit]
def create
#product.attributes = params[:product]
if #product.save
flash[:success] = I18n.t('product.create.success')
redirect_to :action => :index
else
render_with_categorization(:new)
end
end
def update
#product.attributes = params[:product]
if #product.save
flash[:success] = I18n.t('product.update.success')
redirect_to :action => :index
else
render_with_categorization(:edit)
end
end
private
def build_product
#product = Product.new
end
def load_product
#product = Product.find_by_id(params[:id])
#product || invalid_url
end
def build_or_load_categorization
Category.where('id not in (?)', #product.categories).each do |c|
#product.categorizations.new(:category => c)
end
end
def render_with_categorization(template)
build_or_load_categorization
render :action => template
end
end
Inside view
= form_for #product do |f|
= f.fields_for :categorizations do |c|
%label= c.object.category.name
= c.check_box :category_id, {}, c.object.category_id, nil
%label Description
= c.text_field :description
I just did the following. It worked for me..
<%= f.label :category, "Category" %>
<%= f.select :category_ids, Category.order('name ASC').all.collect {|c| [c.name, c.id]}, {} %>

Resources