Editing associated models the rails way - ruby-on-rails

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

Related

Rails nested fields creation in loop

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.

Permitting params in rails when form references multiple models

I have three models. The two I am having trouble with (recipe and ingredient) each have a has_and_belongs_to_many relationship with the other. The form seems to be getting all the information I ask for, but I can't seem to get the name attribute of the ingredient into my permitted params.
Form:
<%= form_for(#recipe, :url => create_path) do |f| %>
<%= f.label :category %>
<%= f.select :category_id, options_for_select(Category.all.map{|c|[c.title, c.id]}) %>
<%= f.label :title %>
<%= f.text_field :title%>
<%= f.label :instruction %>
<%= f.text_area(:instruction, size: "50x10") %>
<%= f.fields_for :indgredient do |i| %>
<%= i.label :name %>
<%= i.text_field :name %>
<% end %>
<%= f.submit "Submit" %>
Relevant action in Recipes Controller:
def create
safe_params = params.require(:recipe).permit(:title, :instruction,
:category_id, {ingredient: :name})
#recipe = Recipe.new(safe_params)
#recipe.save
#recipe.ingredients.create(name: safe_params[:name])
render body: YAML::dump(safe_params)
end
What the YAML dump gives me:
--- !ruby/hash:ActionController::Parameters
title: foo
instruction: bar
category_id: '1'
Code for models:
class Category < ActiveRecord::Base
has_many :recipes
end
class Recipe < ActiveRecord::Base
has_and_belongs_to_many :ingredients
accepts_nested_attributes_for :ingredients
belongs_to :category
end
class Ingredient < ActiveRecord::Base
has_and_belongs_to_many :recipes
end
the create method does create a new ingredient, but the name is nil. Thanks in advance for the help.
Did you add accepts_nested_attributes_for :ingredients in the model of Recipe ?
Moreover there is a gem to handle nested forms called cocoon.
You can read this article which is explaining exactly what you are trying to do.
https://hackhands.com/building-has_many-model-relationship-form-cocoon/
Firstly, change <%= f.fields_for :indgredient do |i| %> to <%= f.fields_for :ingredients do |i| %>.
And change the new and create actions like below
def new
#recipe = Recipe.new
#recipe.ingredients.build
end
def create
#recipe = Recipe.new(safe_params)
if #recipe.save
redirect_to #recipe
else
render 'new'
end
end
private
def safe_params
params.require(:recipe).permit(:title, :instruction, :category_id, ingredients_attributes: [:name])
end
To add to #Pavan's answer, you have to remember that Ruby is building objects (it's an object orientated language), and as such, whenever you pass associated data, you have to refer to the objects Ruby has in memory.
In your case, you're trying to create new Ingredient objects through Recipe:
#app/models/recipe.rb
class Recipe < ActiveRecord::Base
has_and_belongs_to_many :ingredients
accepts_nested_attributes_for :ingredients
end
... thus, you need to reference ingredients:
<%= f.fields_for :ingredients do ... %>
--
You also want to make sure you're only processing the Recipe object in your create action:
def create
#recipe = Recipe.new safe_params
#recipe.save
end
private
def safe_params
params.require(:recipe).permit(:title, :instruction, :category_id, ingredients_attributes: [:name] )
end

How to validate multiple models in a single transaction?

In my rails (4.1.6) app, I have a contact model that has_one :address, :email
I construct a contact and related address and email in a single form using fields_for:
views/contacts/new.html.erb
<%= form_for #contact, ... %>
...
<%= fields_for :address do |address_fields| %>
<%= address_fields.text_field :street, ... %>
<%= address_fields.text_field :city, ... %>
...
<% end %>
<%= fields_for :email do |email_fields| %>
<%= email_fields.text_field :display_name, ... %>
<%= email_fields.text_field :mail_id, ... %>
<% end %>
...
<% end %>
I want email to be required, while address is optional. In other words, if email is not provided, none of the 3 models should be created, but if only email is provided, the email and contact must be created.
One way that does work is to validate the params manually in the contacts_controller#create before constructing anything, and flash[:error] and return without saving if email is not specified, or save it if all is well:
contacts_controller.rb
def create
#contact = Contact.new
if(params_email_valid? params)
#contact.save!
#email = Email.create(...)
#email.save!
...
else
flash[:error] = 'Email must be specified to save a contact'
redirect_to :root
end
end
private:
def params_email_valid? params
!(params[:email][:display_name].blank? || params[:email][:mail_id].blank?)
end
Another way that may work is to drop down to SQL and validate everything through direct SQL calls in a transaction.
However, both of these are not 'the rails way', since validations belong in the models. So, I am trying to use some combination of validates_presence_of, validates_associated and custom validators to validate this scenario. The problem here is that model level validation of associated models requires either self to be already saved in the database, or the associated model to be already saved in the database. Is there a way to validate all these models in a single transaction?
Considering you have appropriate validations in the models:
class Contact <
has_many :addresses
has_many :emails
#add
accepts_nested_attributes_for :addresses, :emails #you can add some validations here to like reject_all if blank? see the docs
end
class Address <
belongs_to :contact
end
class Email <
belongs_to :contact
end
In your CompaniesController
def new
#contact = Contact.new
#contact.addresses.new
#contact.emails.new
end
def create
#contact = Contact.new(contact_params)
if #contact.save
#redirect add flash
else
#add flash
#render action: new
end
protected
def contact_params
#permit(#contact_fields, address_attributes: [#address_fields], email_attributes: [#email_fields])
end
And you would like to modify your form like this
<%= form_for #contact, ... do|f| %>
...
<%= f.fields_for :address do |address_fields| %>
<%= address_fields.text_field :street, ... %>
<%= address_fields.text_field :city, ... %>
...
<% end %>
<%= f.fields_for :email do |email_fields| %>
<%= email_fields.text_field :display_name, ... %>
<%= email_fields.text_field :mail_id, ... %>
<% end %>
...
<% end %>
So accepts_nested_attributes helps you validate the child as well as the parent and adds [child]_attributes getters and setters, So normally in your form what was contact[email][display_name] will become contact[email_attributes][display_name]

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.

Associating two records after create in Rails

I'm working on an association between two models:
class Person < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_one :person
end
Many person records exist in the system that don't necessarily correspond to a user, but when creating a user you need to either create a new person record or associate to an existing one.
What would be the best way to associate these two models when the person record already exists? Do I need to manually assign the user_id field, or is there a Rails way of doing that?
Where #user is a recently created user, and #person is an existing person.
#user.person = #person
#user.save
Alternately:
User.new :person => #person, ... #other attributes
or in params form:
User.new(params[:user].merge({person => #person}))
As far as forms go:
<% form_for #user do |f| %>
...
<% fields_for :person do |p| %>
<%= p.collection_select, :id, Person.all, :id, :name, :include_blank => "Use fields to create a person"%>
<%= p.label_for :name%>
<%= p.text_field :name %>
...
<% end %>
<% end %>
And in the user controller:
def create
#user = User.create(params[:user])
#person = nil
if params[:person][:id]
#person = Person.find(params[:person][:id])
else
#person = Person.create(params[:person])
end
#user.person = #person
...
end
If you don't want to create/alter a form for this, you can do
#person_instance.user = #user_instance
For has_many relationships, it would be:
#person_instance.users << #user_instance
You first have to do a nested form :
<% form_for #user do |user| %>
<%= user.text_field :name %>
<% user.fields_for user.person do |person| %>
<%= person.text_field :name %>
<% end %>
<%= submit_tag %>
<% end %>
In your User model :
class User < ActiveRecord::Base
accepts_nested_attributes_for :person
end
If you want the person deleted when the user is :
class User < ActiveRecord::Base
accepts_nested_attributes_for :person, :allow_destroy => true
end
And in your controller do nothing :
class UserController < ApplicationController
def new
#user = User.new
#find the person you need
#user.person = Person.find(:first)
end
def create
#user = User.new(params[:user])
#user.save ? redirect_to(user_path(#user)) : render(:action => :new)
end
end

Resources