Rails simple_form nested attributes - ruby-on-rails

I have 2 models:
class Client < ActiveRecord::Base
has_many :contact_people
accepts_nested_attributes_for :contact_people
end
class ContactPerson < ActiveRecord::Base
belongs_to :client
end
I cand add a new Client or a new ContactPerson separately with no problem.
I want to create a form where I will be able to add them both with a nested form. What would be the recommended way to do this, create a new controller to do this and create new and create action for that, or use the ClientsController and create a new method there?
If a new controller is recommended, how can I access the params? Also, will validations will work here?
Thanks!

You should be using the new and create methods of your ClientsController to do something like this
Class ClientsController < ApplicationController
def new
#client = Client.new
#client.contact_people.build #this is very important
end
def create
#client = Client.new(client_params)
if #client.save
redirect_to #client
else
render 'new'
end
end
private
def client_params
params.require(:client).permit(:client_attr1, :client_attr2,.., contact_people_attributes: [:id, :contact_people_attr1,:contact_people_attr2,..])
end
end
And the view code would be something like this
<%= simple_form_for #client, :html => { :multipart => true } do |f| %>
<% if #client.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(#client.errors.count, "error") %> prohibited this client from being saved:</h2>
<ul>
<% #client.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
#your client attributes code here
<%= f.simple_fields_for :contact_people do |cp| %>
#your contact_people attributes code here
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
And the validations you are talking about can be set normally in models.

Related

Ruby on Rails: how to make a form for associated models (nested)

First of all I have this:
https://polar-scrubland-30279.herokuapp.com/ - my project which is deployed on heroku (Captain Obvious)
I've got projects and todos inside them.
For this moment I show all projects using this way:
------index.html.erb------
<%= render #projects %>
------_project.html.erb-----
<div class="project">
<div class="project-header">
<h2><%= project.title %></h2>
</div>
<div class="project-todos">
<% project.todos.all.each do |todo| %>
<p><%= check_box('tag', todo.__id__, {class: 'icheckbox_square-blue', checked: todo.isCompleted}) %> <%= content_tag :todotext, todo.text %></p>
<% end %>
</div>
</div>
And as you understand it doesn't allow me to change my todo's status when checkbox is checked. So that's why I need a form that will allow me to track all the checkboxes. Also I wanna make text-decoration: line-through when checkbox is pressed, but don't get how to.
Is there a way to creat a form which will satisfy my needs? Please can you help me, Any information will be appreciated.
ADDITIONAL INFORAMTION:
GitHub - https://github.com/NanoBreaker/taskmanager
project.rb
class Project < ActiveRecord::Base
has_many :todos
end
todo.rb
class Todo < ActiveRecord::Base
belongs_to :project
end
Lets start with the models:
class Project < ApplicationRecord
has_many :todos
accepts_nested_attributes_for :todos
end
class Todo < ApplicationRecord
belongs_to :project
end
accepts_nested_attributes_for lets you create or modify several nested Todo records at once when creating or updating a Project.
# will update 2 todos at once
#project.update(
todos_attributes: [ { id: 1, isComplete: true }, { id: 2, isComplete: false }]
)
We can use fields_for to create nested inputs for todos:
<%= f.form_for(#project) do |f| %>
<%= f.fields_for(:todos) do |tf| %>
<%= tf.check_box :isCompleted %>
<% end %>
<% end %>
This generates fields for todos nested under the key todos_attributes. We can whitelist them by using a hash key containing a array of permitted attributes.
class ProjectsController < ApplicationController
before_action :set_project, only: [:show, :edit, :update, :destroy]
def new
#project = Project.new
# this seeds the project with 3 empty tasks
# otherwise we don't have any inputs.
3.times { #project.todos.new }
end
def create
#project = Project.new(project_params)
if #project.save
# ...
else
# ...
end
end
def update
if #project.update(project_params)
# ...
else
# ...
end
end
private
def set_project
#project = Project.find(params[:id])
end
def project_params
params.require(:project)
.permit(:foo, :bar,
todos_attributes: [:isCompleted, :text]
)
end
end
You can create a form for each project by creating a partial which uses a local instead of an instance variable:
# app/views/projects/_form.html.erb
<%= f.form_for(local_assigns[:project] || #project) do |f| %>
<%= f.fields_for(:todos) do |tf| %>
<%= tf.check_box :isCompleted %>
<% end %>
<% end %>
# app/views/projects/index.html.erb
<% #projects.each do |project| %>
<%= render partial: 'projects/form', project: project %>
<% end %>
You can reuse the same partial for the other views as well:
# app/views/projects/new.html.erb
<%= render partial: 'projects/form' %>
# app/views/projects/edit.html.erb
<%= render partial: 'projects/form' %>

Form Objects in Rails

The example code below is a contrived example of an attempt at a form object where it is probably overkill to utilize a form object. Nonetheless: it shows the issue I am having:
I have two models: a User and an Email:
# app/models/user.rb
class User < ApplicationRecord
has_many :emails
end
# app/models/user.rb
class Email < ApplicationRecord
belongs_to :user
end
I want to create a form object which creates a user record, and then creates three associated email records.
Here are my form object classes:
# app/forms/user_form.rb
class UserForm
include ActiveModel::Model
attr_accessor :name, :email_forms
validates :name, presence: true
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
puts "The Form is VALID!"
puts "I would proceed to create all the necessary objects by hand"
user = User.create(name: name)
email_forms.each do |email|
Email.create(user: user, email_text: email.email_text)
end
end
end
# app/forms/email_form.rb
class EmailForm
include ActiveModel::Model
attr_accessor :email_text, :user_id
validates :email_text, presence: true
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
puts "The Form is VALID!"
# DON'T THINK I WOULD PERSIST DATA HERE
# INSTEAD DO IT IN THE user_form
end
end
Notice: the validations on the form objects. A user_form is considered to be invalid if it's name attribute is blank, or if the email_text attribute is left blank for any of the email_form objects inside it's email_forms array.
For brevity: I will just be going through the new and create action of utilizing the user_form:
# app/controllers/user_controller.rb
class UsersController < ApplicationController
def new
#user_form = UserForm.new
#user_form.email_forms = [EmailForm.new, EmailForm.new, EmailForm.new]
end
def create
#user_form = UserForm.new(user_form_params)
if #user_form.save
redirect_to users_path, notice: 'User was successfully created.'
else
render :new
end
end
private
def user_form_params
params.require(:user_form).permit(:name, {email_forms: [:_destroy, :id, :email_text, :user_id]})
end
end
Lastly: the form itself:
# app/views/users/new.html.erb
<h1>New User</h1>
<%= render 'form', user_form: #user_form %>
<%= link_to 'Back', users_path %>
# app/views/users/_form.html.erb
<%= form_for(user_form, url: users_path) do |f| %>
<% if user_form.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(user_form.errors.count, "error") %> prohibited this user from being saved:</h2>
<ul>
<% user_form.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %>
<%= f.text_field :name %>
</div>
# MESSY, but couldn't think of a better way to do this...
<% unique_index = 0 %>
<% user_form.email_forms.each do |email_form| %>
<div class="field">
<%= label_tag "user_form[email_forms][#{unique_index}][email_text]", "Email Text" %>
<%= text_field_tag "user_form[email_forms][#{unique_index}][email_text]" %>
</div>
<% unique_index += 1 %>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
The form does render:
And here is the form's html:
I go to submit the form. Here is the params hash:
Parameters: {"utf8"=>"✓", "authenticity_token"=>”abc123==", "user_form"=>{"name"=>"neil", "email_forms"=>{"0"=>{"email_text"=>"test_email_1"}, "1"=>{"email_text"=>"test_email_2"}, "2"=>{"email_text"=>""}}}, "commit"=>"Create User form"}
What should happen is the form should be re-rendered and nothing persisted because the form_object is invalid: All three associated emails must NOT be blank. However: the form_object thinks it is valid, and it blows up in the persist! method on the UserForm. It highlights the Email.create(user: user, email_text: email.email_text) line and says:
undefined method `email_text' for ["0", {"email_text"=>"test_email_1"}]:Array
Clearly there are a couple things going on: The nested validations appear to not be working, and I am having trouble rebuilding each of the emails from the params hash.
Resources I have already examined:
This Article seemed promising but I was having trouble getting it to work.
I have attempted an implementation with the virtus gem and the reform-rails gem. I have pending questions posted for both of those implementations as well: virtus attempt here and then reform-rails attempt here.
I have attempted plugging in accepts_nested_attributes, but was having trouble figuring out how to utilize that with a form object, as well as a nested form object (like in this code example). Part of the issue was that has_many and accepts_nested_attributes_for do not appear to be included in ActiveModel::Model.
Any guidance on getting this form object to do what is expected would be very much appreciated! Thanks!
Complete Answer
Models:
#app/models/user.rb
class User < ApplicationRecord
has_many :emails
end
#app/models/email.rb
class Email < ApplicationRecord
belongs_to :user
end
Controller:
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
#users = User.all
end
def new
#user_form = UserForm.new
#user_form.emails = [EmailForm.new, EmailForm.new, EmailForm.new]
end
def create
#user_form = UserForm.new(user_form_params)
if #user_form.save
redirect_to users_path, notice: 'User was successfully created.'
else
render :new
end
end
private
def user_form_params
params.require(:user_form).permit(:name, {emails_attributes: [:email_text]})
end
end
Form Objects:
#app/forms/user_form.rb
class UserForm
include ActiveModel::Model
attr_accessor :name, :emails
validates :name, presence: true
validate :all_emails_valid
def emails_attributes=(attributes)
#emails ||= []
attributes.each do |_int, email_params|
email = EmailForm.new(email_params)
#emails.push(email)
end
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
user = User.new(name: name)
new_emails = emails.map do |email_form|
Email.new(email_text: email_form.email_text)
end
user.emails = new_emails
user.save!
end
def all_emails_valid
emails.each do |email_form|
errors.add(:base, "Email Must Be Present") unless email_form.valid?
end
throw(:abort) if errors.any?
end
end
app/forms/email_form.rb
class EmailForm
include ActiveModel::Model
attr_accessor :email_text, :user_id
validates :email_text, presence: true
end
Views:
app/views/users/new.html.erb
<h1>New User</h1>
<%= render 'form', user_form: #user_form %>
<%= link_to 'Back', users_path %>
#app/views/users/_form.html.erb
<%= form_for(user_form, url: users_path) do |f| %>
<% if user_form.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(user_form.errors.count, "error") %> prohibited this User from being saved:</h2>
<ul>
<% user_form.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %>
<%= f.text_field :name %>
</div>
<%= f.fields_for :emails do |email_form| %>
<div class="field">
<%= email_form.label :email_text %>
<%= email_form.text_field :email_text %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>

Rails 4 fields_for not displaying or updating

I have a nested relationship where dashboard has many rewards, and I am trying to add a fields_for to the page in order to edit the rewards. Unfortunately, it doesn't seem to be working and I don't know why.
Here's what I have.
Dashboard model:
class Dashboard < ActiveRecord::Base
belongs_to :manager
has_many :rewards
accepts_nested_attributes_for :rewards, allow_destroy: true
end
Rewards model:
class Reward < ActiveRecord::Base
belongs_to :dashboard
end
Dashboard controller:
class DashboardsController < ApplicationController
before_action :authenticate_manager!
# Requires user to be signed in
def index
#dashboards = Dashboard.all
end
def new
#dashboard = Dashboard.new
end
def edit
#dashboard = Dashboard.find(params[:id])
end
def create
#dashboard = Dashboard.new(dashboard_params)
#dashboard.save
if #dashboard.save
redirect_to dashboard_path(#dashboard)
else
render :action => new
end
end
def update
#dashboard = Dashboard.find(params[:id])
if #dashboard.update(dashboard_params)
redirect_to :action => :show
else
render 'edit'
end
end
def show
#dashboard = Dashboard.find(params[:id])
end
def destroy
#dashboard = Dashboard.find_by_id(params[:id])
if #dashboard.destroy
redirect_to dashboards_path
end
end
private
def dashboard_params
args = params.require(:dashboard).permit(:title, :description, :rewards, {rewards_attributes: [ :id, :title, :referralAmount, :dashboardid, :selected, :_destroy] } )
args
end
end
Form in dashboards view:
<%= form_for :dashboard, url: dashboard_path(#dashboard), method: :patch do |f| %>
<% if #dashboard.errors.any? %>
<div id="error_explanation">
<h2>
<%= pluralize(#dashboard.errors.count, "error") %> prohibited
this dashboard from being saved:
</h2>
<ul>
<% #dashboard.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<p>
<%= f.label :title %><br>
<%= f.text_field :title %>
</p>
<p>
<%= f.label :description %><br>
<%= f.text_field :description %>
</p>
<%= f.fields_for :rewards do |reward| %>
<%= reward.label :title %><br>
<%= reward.text_field :title %>
<%= reward.check_box :_destroy %>
<%= reward.label :_destroy, "Remove reward" %>
<% end %>
<p>
<%= f.submit %>
</p>
<% end %>
I went ahead and manually added rewards to the database through the rails console and it worked beautifully, but they are not showing up on the page. They will show up if I iterate through them like so
<% if #dashboard.rewards.any? %>
<ul>
<% #dashboard.rewards.each do |reward| %>
<li><%= reward.title %></li>
<li><%= reward.referralAmount %></li>
<% end %>
</ul>
<% else %>
<p>no rewards</p>
<% end %>
However the fields_for does not display the rewards or their content and resultingly allow one to edit them.
Let me know if you need further information/code.
Try to modify your:
View:
<% if #dashboard.errors.any? %>
<div id="error_explanation">
<h2>
<%= pluralize(#dashboard.errors.count, "error") %> prohibited
this dashboard from being saved:
</h2>
<ul>
<% #dashboard.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<%= form_for #dashboard, url: dashboard_path(#dashboard) do |f| %>
........
<% end %>
Controller (has_many relationship):
def new
#dashboard = Dashboard.new
#dashboard.rewards.build
end
private
def dashboard_params
params.require(:dashboard).permit(:title, :description,
rewards_attributes: [
:id,
:title,
:referralAmount,
:dashboardid,
:selected,
:_destroy
])
end
You don't have to set the method: patch if form.
Once you got in edit page, Rails will use the update action in controller when form submission.
To check it, run rake routes,
you will see somsthing like this:
PATCH /dashboards/:id(.:format) dashboards#update
PUT /dashboards/:id(.:format) dashboards#update
In controller you need to give build
def new
#dashboard = Dashboard.new
#dashboard.rewards.build
end
"build" is just create a new object in memory so that the view can take this object and display something, especially for a form.
Hope it helps for you
You should build object before nested form. You can add whatever you want that object.
Try it in controller;
def new
#dashboard = Dashboard.new
3.times do
#dashboard.build_reward
end
end
Try setting an "#rewards" instance variable in your dashboards edit method (where #rewards = #dashboard.rewards). Then replace :rewards with #rewards.
Edit:
I believe my initial answer is inapproriate for your exact question (while it would be helpful on say the page to show a specific dashboard and its rewards). The answers above are on the right track re:
refining your params method per #aldrien.h;
Adding #santosh dadi's suggestion of
#dashboard.rewards.build
(assuming you only want one rewards fields on a form for "new")
Finally though, to avoid making fake information for a new rewards form, adding to the top of your Dashboards model:
accepts_nested_attributes_for :rewards, reject_if: lambda {|attributes| attributes['title'].blank?}
http://guides.rubyonrails.org/form_helpers.html#nested-forms

Single table Inheritance : type defined as a constant

I'm trying tu use Single Table inheritance to represent that employees can be managers or consultants.
So here's my employees model:
class Employee < ActiveRecord::Base
belongs_to :communaute
self.inheritance_column = :fonction
scope :manager, -> { where(fonction: 'Manager') }
scope :consultant, -> { where(fonction: 'Consultant') }
end
and here are my subclasses :
class Consultant < Employee
end
class Manager < Employee
end
When i'm oppening the new view to create an employee. I have undefined method `fonction' error when i'm trying to list the types on employees.
I don't understand why because i defined it in the employee controller.
Could you please help me on this error please.
Below my new form
<%= form_for(#employee) do |f| %>
<% if #employee.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(#employee.errors.count, "error") %> prohibited this employee from being saved:</h2>
<ul>
<% #employee.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %><br>
<%= f.text_field :name %>
</div>
<div class="field">
<%= f.label :fonction %><br>
<%= f.select :fonction, Employee.fonction.map {|r| [r.humanize, r.camelcase]}, {}, disabled: #fonction != "Employee" %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
And my employee controller
class EmployeesController < ApplicationController
before_action :set_employee, only: [:show, :edit, :update, :destroy]
before_action :set_fonction
def index
#employees = Employee.all
##employees = fonction_class.all
end
def show
end
def new
##employee = fonction_class.new
#employee = Employee.new
end
def edit
end
def create
#employee = Employee.new(employee_params)
if #employee.save
redirect_to #employee, notice: "#{fonction} was successfully created."
else
render action: 'new'
end
end
def update
end
def destroy
end
private
def fonction
params[:type] || "Employee"
end
def set_fonction
#fonction = fonction
end
# def fonction_class
# fonction.constantize
#end
def set_animal
#employee = fonction_class.find(params[:id])
end
def employee_params
params.require(fonction.downcase.to_sym).permit(:name, :fonction)
end
end
You need to define fonction in your model (app/models/employee.rb)
The way you're using it implies it's defined as both a class method, and an instance method. It also looks like it could be an array? Employee.fonction.map {|r| ... or an attribute (since you have a select to set it on an employee.)
However, you define fonction as a string...
def fonction
params[:type] || "Employee"
end
So I'm not really sure what you're trying to achieve here... All I know is Employee.fonction looks like a model class method...
def self.fonction
...
end
and f.select :fonction looks like an instance method or attribute?
EDIT I think I've got this... (Sorry, I missed fonction reference in your model definition)
Try adding this to your Employee model:
def self.fonction
["Manager", "Consultant", "Employee"]
end
EDIT #2
You can also define a constant in your model
FONCTIONS = ["Manager", "Consultant", "Employee"]
def fonction
FONCTIONS
end
Did you create fonction column?, create it by entering in your terminal, when in your app directory:
rails g migration add_fonction_to_employees fonction
rake db:migrate
Then check your DB has the column fonction in employees table.

Nested model validation - errors don't show

There have been many questions about this, but none of them seem to help. And yes, I have watched this rails cast.
I have an Author who has many Books, like so:
Author:
class Author < ActiveRecord::Base
attr_accessible :name
has_many :books, dependent: :destroy
accepts_nested_attributes_for :books, allow_destroy: true
validates :name, presence: true
validates :name, length: { minimum: 3 }
end
Book:
class Book < ActiveRecord::Base
attr_accessible :name, :year
belongs_to :author
validates :name, :year, presence: true
validates :year, numericality: { only_integer: true, less_than_or_equal_to: Time.now.year }
end
I created the following form to add a book to an author in authors#show:
<%= form_for([#author, #book], html: { class: "well" }) do |f| %>
<% if #book.errors.any? %>
<div class="alert alert-block">
<ul>
<% #author.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
#labels and buttons...
<% end %>
...with the following authors_controller method:
def show
#author = Author.find(params[:id])
#book = #author.books.build
end
...and the following books_controller method:
def create
#author = Author.find(params[:author_id])
if #author.books.create(params[:book])
redirect_to author_path(#author)
else
render action: :show
end
end
I cannot seem to figure out why the form does not display any error messages. I followed the example from railscasts where they say there should be an instance variable of books in the form instead of #author.books.build, so I put the latter in the controller and #book in the form - still to no avail.
Thanks for any help!
Let's step through it.
You submit the create, and that enters your create action
def create
#author = Author.find(params[:author_id])
if #author.books.create(params[:book])
redirect_to author_path(#author)
else
render action: :show
end
end
(Side note, what if #author is not found. You are not handling that case.)
Now, the Author is found, but #author.books.create fails (returns false), so you render the show action.
This uses the show template, but does not call the show action code. (Side note, maybe the new page would be a better choice, so the user can try to create again.)
At this point #author is instantiated with the Author you found, but not #book. So #book, if called will be nil.
Your show template does
if #book.errors.any?
which will not be true, so the rest of the template inside the if will be skipped. That's why there are no errors.
You don't need a form_for to display error messages. If you switch to using the new template, then there will be a form to try again.
So let's switch to rendering new.
Class BooksController < ApplicationController
def new
#author = Author.find(params[:author_id])
#book = #author.books.build
end
def create
#author = Author.find(params[:author_id])
#book = #author.books.build(params[:book])
if #author.save
redirect_to author_path(#author)
else
render action: :new
end
end
Your new template will be
<% if #author.errors.any? %>
<div class="alert alert-block">
<ul>
<% #author.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<% if #book.errors.any? %>
<div class="alert alert-block">
<ul>
<% #book.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<%= form_for([#author, #book], html: { class: "well" }) do |f| %>
#labels and buttons...
<% end %>
In Books controller
/books_controller.rb
def new
#author = Author.find_by_id(params[:author_id])
#book = #author.books.build
end
def create
#author = Author.find_by_id(params[:author_id])
if #author
#book = #author.books.build(params[:book])
if #book.save
flash[:notice] = "Book saved successfully"
redirect_to author_path(#author)
else
render :new
end
else
flash[:notice] = "Sorry no author found"
redirect_to author_path
end
end
If author is not present redirect to authors index page with error message dont render the new form as you'll not be able to build the books form as author is nil.
And in your books new form you can have the error listed for books
/books/new.html.erb
<% if #book.errors.any? %>
<div class="alert alert-block">
<ul>
<% #books.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>

Resources