Nested model form not submitting everything - ruby-on-rails

I heard about this community while listening to Hypercritical and I am excited to join in with my first question. I am working on my first rails App and I have run into an issue that I cannot seem to crack. I have been watching Railscast, Lynda.com, and Googling for days but I still cannot comprehend how to create a form that that will update my has_many :through associations at once. Allow me to try an explain what I am doing.
My Goal:
The firm I work for provides many "Service Offerings" and I want to be able to create a new service offering on one page and have it create the contacts and other information that is associated with it. The additional information such as "contacts" will live in their own tables because they may need to be referenced by many "Service Offerings."
Problem:
When I submit the form the "Service Offering" fields submit and are entered into the database, but the fields for the "Business Developer" do not. Obviously, I would like everything to be entered into its appropriate table and the for the IDs to be linked in the join table. I would really appreciate any insight that you could provide.
What I Have So Far: What you see below is Service Offerings and Business Developers. Eventually I will be adding Contacts, Photos, and Files but I thought I would start simply and work my way up.
Models:
class ServiceOffering < ActiveRecord::Base
attr_accessible :name, :description
has_many :business_developer_service_offerings
has_many :business_developers, :through => :business_developer_service_offerings
accepts_nested_attributes_for :business_developer_service_offerings
end
class BusinessDeveloper < ActiveRecord::Base
attr_accessible :first_name, :last_name
has_many :business_developer_service_offerings
has_many :service_offerings, :through => :business_developer_service_offerings
end
class BusinessDeveloperServiceOffering < ActiveRecord::Base
belongs_to :business_developer
belongs_to :service_offering
end
Controller:
def new
#service_offering = ServiceOffering.new
#service_offering.business_developers.build
end
def create
#service_offering = ServiceOffering.new(params[:service_offering])
if #service_offering.save
redirect_to(:action => 'list')
else
render('new')
end
end
View:
<%= form_for((#service_offering), :url => {:action => 'create'}) do |f|%>
<p>
<%= f.label :name%>
<%= f.text_field :name %>
<%= f.label :description%>
<%= f.text_field :description %>
</p>
<%= f.fields_for :business_developer do |builder| %>
<p>
<%= builder.label :first_name%>
<%= builder.text_field :first_name %>
<%= builder.label :last_name%>
<%= builder.text_field :last_name %>
</p>
<%end%>
<%= f.submit "Submit" %>
<%end%>

I figured it out. It turns out a few things were wrong and needed to be changed in addition to the two suggestions #Delba made.
The Form:
I took a look at RailsCasts #196 again and noticed that my form looked different than the one used there, so I tried to match it up:
<%= form_for #service_offering do |f|%>
<p>
<%= f.label :name%>
<%= f.text_field :name %>
<%= f.label :description %>
<%= f.text_field :description %>
</p>
<%= f.fields_for :business_developers do |builder| %>
<p>
<%= builder.label :first_name %>
<%= builder.text_field :first_name %>
<%= builder.label :last_name %>
<%= builder.text_field :last_name %>
</p>
<%end%>
<%= f.submit "Submit" %>
<%end%>
Initially, this presented an error:
undefined method `service_offerings_path'
Routes:
This lead me to learn about RESTful Routes because I was using the old routing style:
match ':controller(/:action(/:id(.:format)))'
So I updated my routes to the new RESTful Routes style:
get "service_offerings/list"
resource :service_offerings
resource :business_developers
attr_accessible:
That got the form visible but it was still not working. So I did some searching around on this site and found this post that talked about adding "something_attributes" to your parent objects model under attr_accessible. So I did:
class ServiceOffering < ActiveRecord::Base
has_many :business_developer_service_offerings
has_many :business_developers, :through => :business_developer_service_offerings
accepts_nested_attributes_for :business_developers
attr_accessible :name, :description, :business_developers_attributes
end
That change along with #Delba's suggestion shown in the above model and controller listed below solved it.
def new
#service_offering = ServiceOffering.new
#business_developer = #service_offering.business_developers.build
end

You just forgot to assign #business_developper.
def new
#service_offering = ServiceOffering.new
#business_developper = #service_offering.business_developpers.build
end
-
#business_developer = #service_offering.business_developers.build
initializes an instance of biz_dev which is then available in the view.
fields_for :biz_dev isn't really tied to this instance but to the many-to-many relationship btw serv_off and biz_dev.
In this way, you can add multiple input for additional biz_dev if you initialize another biz_dev instance in your controller. For instance:
5.times { #service_offering.biz_dev.build }
will add additional fields in your form without you having to declare them in your view.
I hope it helped.

Related

Rails - How to avoid using hidden_fields in the view to pass values to controller?

Is there a way I can avoid the hidden_field method of passing values in the view to a controller? I would prefer a controller method for security reasons. Unfortunately value pairing #variables is not supported in strong_parameters.
EDIT 6/18 1:00 PM EST
I've renamed my garages controller to appointments
cars_controller no longer creates a new appointment (formally garages). A new appointment is created in the
appointments_controller
My current structure
routes
Rails.application.routes.draw do
resources :techs, only: [:index, :show], shallow: true do
resources :cars, only: [:new, :create]
end
resources :appointments
#For searchkick
resources :cars, only: [:show] do
collection do
get 'search'
end
end
root "home#index"
end
models
tech.rb
class Tech < ActiveRecord::Base
searchkick
has_many :appointments
has_many :customers, :through => :appointments
has_many :service_menus
has_many :services
has_many :cars
end
service.rb
class Service < ActiveRecord::Base
belongs_to :tech
belongs_to :service_menu
has_many :cars, dependent: :destroy
accepts_nested_attributes_for :cars, :reject_if => :all_blank, :allow_destroy => true
end
car.rb
class Car < ActiveRecord::Base
belongs_to :service
belongs_to :tech
has_many :appointments
end
appointment.rb
class Garage < ActiveRecord::Base
belongs_to :customer
belongs_to :tech
belongs_to :car
end
controllers
cars_controller
def new
#car = Car.find(params[:id])
#tech = Tech.find(params[:tech_id])
#appointment = Garage.new
end
appointments_controller
def create
#appointment = current_customer.appointments.build(appointment_params)
if #appointment.save
redirect_to appointments_path, notice: "You car has been added to this appointment."
else
redirect_to appointments_path, notice: "Uh oh, an error has occured."
end
end
private
def appointment_params
params.require(:appointment).permit(:tech_id, :service_id, :car_id, ...and a bunch of other keys here)
end
views
cars.new.html
Please note this form passes hidden values to the appointment_controller.
Value from #car.name and other alike are not from a text_field but rather a pre-defined value based on selections from a previous page which is store in the cars db.
<%= simple_form_for(#appointment, { class: 'form-horizontal' }) do |f| %>
<%= f.hidden_field :tech_id, value: #tech.id %>
<%= f.hidden_field :car_id, value: #car.id %>
<%= f.hidden_field :service_id, value: #car.service.id %>
<%= f.hidden_field :customer_car, value: current_customer.car %>
<%= f.hidden_field :customer_street_address, value: current_customer.street_address %>
<%= f.hidden_field :customer_city, value: current_customer.city %>
<%= f.hidden_field :customer_state, value: current_customer.state %>
<%= f.hidden_field :customer_zip_code, value: current_customer.zip_code %>
<%= f.hidden_field :service_name, value: #car.service.service_menu.name %>
<%= f.hidden_field :car_name, value: #car.name %>
<%= **And a bunch of other hidden values here which are too long to list** %>
<%= f.submit "Add to appointment", class: 'btn btn-default' %>
<% end %>
service.html
<%= render 'form' %>
_form.html
<%= simple_form_for #service do |f| %>
<div class="field">
<%= f.label "Select service category" %>
<br>
<%= collection_select(:service, :service_menu_id, ServiceMenu.all, :id, :name, {:prompt => true }) %>
<%= f.fields_for :cars do |task| %>
<%= render 'car_fields', :f => task %>
<% end %>
</div>
<div class="links">
<%= link_to_add_association 'Add New Car', f, :cars, class: 'btn btn-default' %>
</div><br>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
_car_fields.html
<div class="nested-fields">
<div class="field">
<%= f.label :name %><br>
<%= f.text_field :name %><br>
<%= f.label :hours %>
<%= f.select :hours, '0'..'8' %>
<%= f.label :minutes %>
<%= f.select :minutes, options_for_select( (0..45).step(15), selected: f.object.minutes) %><br>
<%= f.label :price %><br>
<%= f.text_field :price, :value => (number_with_precision(f.object.price, :precision => 2) || 0) %> <br>
<%= f.label :details %><br>
<%= f.text_area :details %></div>
<%= link_to_remove_association "Remove Car", f, class: 'btn btn-default' %>
<%= f.hidden_field :tech_id, value: current_tech.id %>
<br>
<hr>
</div>
> Edit 7/14 1:30 pm EST
Brief Synopsis on this specific function of the application
A customer clicks through a list of services a tech has to offer
The customer selects a service for example brakes which is a service a tech has listed in his profile.
The attributes for brakes are listed in the cars db
cars belongs_to to techs
The customer can save brakes which is an attribribute of a techs car to a appointment
A good number of predefined values from tech, the customer's street address, etc..., and the car are pre-loaded in the form for storing in the appointments table.
appointment acts as a histories table. So if the tech decides to modify any one of his services in this example brakes, the appointments tables will remain untouched for the brakes entry.
Once the customer selects the Add to appointment button, it will save all of the predefined values from tech, customer, and car attributes (in this example brakes) to the appointments db.
Another approach to this would be to get rid of the strong parameters altogether and do the following:
def create
#appointment = Garage.create(tech_id: #car.service.tech.id,
customer_id: current_customer.id,
customer_street_address: current_customer.street_address,
customer_city: current_customer.city,
customer_state: current_customer.state,
customer_zip_code: current_customer.zip_code,
customer_phone_number: current_customer.phone_number,
customer_location_type: "WILL ADD LATER",
customer_latitude: current_customer.latitude,
customer_longitude: current_customer.longitude,
service_id: #car.service.id,
service_name: #car.service.name,
car_id: #car.id,
car_name: #car.name,
car_time_duration: #car.time_duration,
price: #car.price,
car_details: #car.details)
if #appointment.save
redirect_to techs_path, notice: "This service has been saved."
elsif
redirect_to tech_path, notice: "Uh oh, an error has occurred."
end
end
Please let me know if you require further details.
I can think of some methods you could use to avoid this form bloated with hidden_fields:
Share data between controllers in the user's session, pretty much like a shopping cart in an e-commerce application.
If you prefer to preserve the statelessness of the application, create a model to temporarily store these informations; this way you'll only need to include one hidden_field in the form.
Use JavaScript to make the requests, storing the data in local objects and passing them as JSON when needed (this is trivial using AngularJS).
Whichever method you choose, keep in mind that storing a lot of state in a web application usually is a code smell. You can always rethink your application so you don't need to keep so much context.
To resolve my issue, my latest edit from my initial post stated the following:
EDIT 6/18 1:00 PM EST
I've renamed my garages_controller to appointments_controller
cars_controller no longer creates a new appointment (formally garages). A new appointment is created in the appointments_controller
Only hidden_field i'm passing is the car_id in the appointments view /new.html.erb <%= f.hidden_field :car_id, value: #car.id %>.
In the appointments_controller, I'm assigning all the car attributes doing the following.
def create
#appointment = current_customer.appointments.build(appointment_params)
#appointment.tech_id = #appointment.car.service.tech.id
#appointment.price = #appointment.car.price
#appointment.car_name = #appointment.car.name
#appointment.car_details = #appointment.car.details
if #appointment.save
redirect_to appointments_path, notice: "Thank you booking your appointment."
else
redirect_to appointments_path, notice: "Uh oh, an error has occurred. Please try again or contact us for further assistance"
end
end
Thank you all for your responses.
I should've known better. :(
You could move that stuff into a callback and only pass the customer_id and car_id with the form. This way garage instance will know about it's customer and car parents and you can do something like:
class Garage < ActiveRecord::Base
before_create :copy_stuff
private
def copy_stuff
self.customer_street_address = customer.street_address
self.car_name = car.name
# and so on
end
end
Is there a way I can avoid the hidden_field method of passing values
in the view to a controller?
You can disable those fields in the HTML/view by adding attribute disabled: true to the hidden input field tags to achieve what you asked for.
Not sure about the syntax exactly, but should be something like this for example
f.hidden_field :tech_id, value: #tech.id, disabled: true

Correct usage of simple_form collection_check_boxes

I have set up 2 models as following :
class Sector < ActiveRecord::Base
attr_accessible :summary, :title, :sector_ids
belongs_to :platform
end
and
class Platform < ActiveRecord::Base
attr_accessible :name, :url
has_many :sectors
end
and a form which tries to use example from here as follows :
<%= simple_form_for #platform, :html => { :class => 'form-vertical'} do |f| %>
<%= f.error_notification %>
<div class="form-inputs">
<div class="row-fluid">
<div class="span12">
<div class="span6">
<%= field_set_tag "General" do %>
<%= f.input :name %>
<%= f.input :url %>
<%= f.collection_check_boxes :sector_ids, Sector.all, :title, :title %>
<% end %>
</div>
</div>
</div>
</div>
<div class="form-actions">
<%= f.button :submit %>
</div>
<% end %>
nevertheless, after I try to submit the form I get the following error :
Can't mass-assign protected attributes: sector_ids
What am I missing here? I successfully migrated the database after adding appropriate associations, but it seems like Rails doesnt really know what to do now wit the sector ids that are selected.
Solution :
class Sector < ActiveRecord::Base
attr_accessible :summary, :title
belongs_to :platforms
end
and
class Platform < ActiveRecord::Base
attr_accessible :name, :url, :platform_attributes, :sector_ids
has_many :sectors
end
and in the view :
<%= f.association :sectors, :as => :check_boxes %>
Ofcourse, do not forget to run "rake db"migrate", in case you havent done it yet. I was also required to restart the server in order for the changes to apply.
I hope this helps someone.
Try adding { :sector_ids => [] } to your Platform permits list.
https://github.com/rails/strong_parameters#nested-parameters
First I think you should remove :sectors_ids from the attr_accessible of Sector and try.
If the above doesn't works (which probably may not work), see the documentation of simple_form, specially read https://github.com/plataformatec/simple_form#associations and https://github.com/plataformatec/simple_form#collection-check-boxes; the last one makes what you are trying to do, but, in the associations they use has_and_belongs_to_many associations.
Finally, check this out https://github.com/plataformatec/simple_form/issues/341
Update
The solution is to make a has_and_belongs_to_many relationship, there is no way out, because, you want to associate some sectors to one platform, but, you want to associate the same sectors to other platforms, otherwise you won't need checkbox, only nested forms to keep adding new records. You have to make this change in your database design and structure.

Update fails for nested attributes when nested object is ActiveRecord subclass

I have ActiveRecord with a subclass and its associated with another ActiveRecord object.
I am able to create my object with nested attributes with a form with nested attributes no problem for a new object (following Ryan Bates rails cast - Thanks by the way :)). However when i do an update it fails to save the changes to either the main object or the related object when submitted
I have the following Activerecord classes and sub class.
class Room < ActiveRecord::Base
attr_accessible :name, :type, room_headers_attributes
has_many :room_headers, dependent: :destroy
accepts_nested_attributes_for :room_headers , :allow_destroy => true
end
And the sub class is
class BigRoom < Room
end
And the related class is
class RoomHeader < ActiveRecord::Base
attr_accessible :key, :room_id, :value
belongs_to :room
end
In my room controller I created the nested objects. note that i'm using :type to specify the subclass type
def new
#room = current_user.passes.build(params[:room])
#room.type = params[:type]
3.times do
room_header = #room.room_headers.build
end
....
end
....
def edit
#room = Room.find(params[:id])
end
def update
#room = Room.find(params[:id])
if #room.update_attributes(params[:room])
...
The form used for creating and editing is the same
<%= form_for(#room) do |f| %>
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name %>
</div>
<%= f.fields_for :room_headers do |builder| %>
<%= render 'room_header_fields', f: builder %>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end &>
And the _room_headers.html.erb partial is
<p class="fields">
<%= f.label :key, "Key" %>
<%= f.text_field :key %>
<%= f.label :value, "Value" %>
<%= f.text_field :value %>
<%= f.check_box :_destroy %>
<%= f.label :_destroy, "Remove Header" %>
</p>
To recap on the problem. I can successfully create a new BigRoom. In the new form when i create the BigRoom and I can successfully set values for the RoomHeader class and these are all saved successfully.
However when i Edit the the record and submit changes for update, nothing is saved. Either for changes for the Bigroom attributes or to the associated RoomHeader records.
first try by
if #room.update_attribute(params[:room])
rather
if #room.update_attributes(params[:room])
if this works then their are some errors with your validdations
Ok, nested attributes were a red herring. The problem is with STI
The Rails form helper guide says you can’t rely on record identification with STI.
In the form_for we need to coearce the ids to be the base type id otherwise the edit fails
so
<%= form_for(#room) do |f| %>
should be
<%= form_for(#room.becomes(Room) do |f| %>
if you look at the difference in the html output
the problem html would create ids like big_room_fieldname when in edit mode
when using .becomes we get ids like room_fieldname. in whihc case it saves and updates ok

Rails 3. How can I only display one nested model form?

I have these two models: Company and CompanyContact.
So I have the usual...
companies_controller.rb
def edit
#company = Company.find(params[:id])
student = #company.students.build
company_contact = #company.company_contacts.build
end
company.rb
has_many :company_contacts, :dependent => :destroy
accepts_nested_attributes_for :company_contacts, :reject_if => :reject_company_contacts, :allow_destroy => true
company_contact.rb
belongs_to :company
form.html.erb
<%= f.fields_for :company_contacts do |builder| %>
<%= render "company_contact_fields", :f => builder %>
<% end %>
_company_contacts.html.erb
<p style="margin:5px 0;">
<%= f.label :first_name %><br />
<%= f.text_field :first_name, :class => 'text_field' %>
</p>
<p style="margin:5px 0;">
<%= f.label :email %><br />
<%= f.text_field :email, :class => 'text_field' %>
</p>
In the edit form, if I already have a company contact for a company, it gives me that existing record plus another empty company contact form ready to be filled out. Which is ok because that it's supposed to happen.
What I need to do is only have ONE company contact, so if there is already a company contact, I don't want to display another form to add an extra company contact. I do not want to setup a has_one relationship because my client might want to add extra company contacts in the future and also when I tried a has_one relationship I got bunch of errors.
Ok so to only have ONE company contact per company, I tried a counter solution, you know in the loop set counter = 0 and then check if counter > 0 but that "solution" didn't work. What would you suggest?
If I got you correct, then
#company.company_contacts.build unless #company.company_contacts.present?
is what you are looking for.
This way, if a company already has a contact, then no more contacts are built. Likewise, if a company has no contact, this will build up contacts which shall then be used by f.fields_for :company_contacts to render in the form.

Rails updating multiple models on a single form

Im writing a form which uses formtastic to manage the BusinessUnit model, however when creating a new BusinessUnit it also has to create a number of other record types. The associations between the models are as below:
class BusinessUnit < ActiveRecord::Base
has_many :business_unit_sites
has_many :locations
class BusinessUnitSite < ActiveRecord::Base
belongs_to :site
belongs_to :business_unit
class Site < ActiveRecord::Base
has_many :locations
has_many :business_unit_sites
class Location < ActiveRecord::Base
belongs_to :business_unit
belongs_to :site
When a BusinessUnit is created, a Site must also be created using BusinessUnitSite as a join table. In addition a Location record should be created which must hold a foreign key to the new Site record and this is where Im having problems.
I can create a new Location using a nested form (below) but the Site will have to be created manually.
<%= semantic_form_for #business_unit do |f| %>
<%= f.inputs do %>
<%= f.input :name %>
<%= f.input :business_unit_id %>
<%= f.input :business_unit_group, :include_blank => false %>
<%= f.input :business_unit_type %>
<%= f.input :tax_region, :include_blank => false %>
<%= f.semantic_fields_for :locations do |l| %>
<%= l.input :name, :label => "Location Name" %>
<% end %>
<% end %>
<%= f.buttons %>
<% end %>
What is the best way to create the Location, Site records and ensure that Location holds the foreign key of the newly created Site?
You probably want to do something like using the "fields_for" approach for the sub-objects in your form.
See this related answer:
Multiple objects in a Rails form
More info about fields_for:
http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html
http://apidock.com/rails/ActionView/Helpers/FormHelper/fields_for

Resources