This question already has an answer here:
Rails -- how to populate parent object id using nested attributes for child object and strong parameters?
(1 answer)
Closed 6 years ago.
I'm trying to build a nested form that will create a new user and subscribe them to a plan.
When I hit "Enroll" I get the following error:
Validation failed: Plan subscriptions user can't be blank
I've double and triple checked everything below and am not sure what's wrong at this point. Any idea why the subscription is not being associated to the new user record?
Here's my code:
User.rb
class User < ActiveRecord::Base
attr_accessible ..., :plan_subscriptions_attributes
has_many :plan_subscriptions, dependent: :destroy
accepts_nested_attributes_for :plan_subscriptions
PlanSubscriptions.rb
class PlanSubscription < ActiveRecord::Base
belongs_to :user
Plan_Subscriptions#new
def new
#plan = Plan.find(params[:plan_id])
#user = User.new
#user.plan_subscriptions.build
end
Plan_Subscriptions/New.html
<%= form_for #user do |f| %>
<fieldset>
<%= f.text_field :first_name, :label => false, :placeholder => 'First Name', :required => false %>
<%= f.text_field :last_name, :label => false, :placeholder => 'Last Name',
<%= f.fields_for :plan_subscriptions do |builder| %>
<%= builder.hidden_field :plan_id, :value => #plan.id %>
<% end %>
<%= f.submit 'Enroll', :error => false %>
</fieldset>
<% end %>
Plan wont have an id at this point in the execution so I wouldn't use that. Try removing the line:
<%= builder.hidden_field :plan_id, :value => #plan.id %>
#plan.id will be nil, so it will overwrite the automatically built object and thus fail the validation.
Then attempt to submit the form again. Try adding a valid form element for the plan subscription if you want the user to set something in the subscription.
Related
RAILS 6
Hey, I'm working on a class system that uses units and assignments as a many-to-many relationship. When I submit a new assignment form with a dropdown collection for units, the unit is not being received by the controller, but no error log is displayed. When I use byebug, the following error is displayed:
Unpermitted parameter: :unit_ids
Even though it has been permitted. Here's my controller for assignments.
class AssignmentsController < ApplicationController
def new
#assignment = Assignment.new
end
def create
debugger
#assignment = Assignment.new(assignment_params)
#assignment.save
if #assignment.save
flash[:success] = "The unit was successfully submitted."
redirect_to units_path
else
render 'new'
end
end
def show
end
private
def assignment_params
params.require(:assignment).permit(:name, :description, :duedate, user_ids: [])
end
end
Using byebug, I know the unit_id is being correctly received, from this form:
<%= form_for(#assignment, :html => {class: "form-horizontal", role: "form"}) do |f| %>
<div class="form-group">
<div>
<%= f.collection_select(:unit_ids, Unit.all, :id, :name, placeholder: "Units" )%>
</div>
<div>
<%= f.text_field :name, class:"form-control", placeholder: "Title of Assignment", autofocus: true %>
</div>
<div>
<%= f.text_area :description, class:"form-control materialize-textarea", placeholder: "Assignment Description", autofocus: true %>
</div>
<div>
<%= f.text_field :duedate, class: "datepicker", placeholder: "Due Date"%>
</div>
<div class="form-group" id="submitbutton">
<div align = "center">
<%= f.submit class: "btn waves-effect waves-light" %>
</div>
</div>
</div>
<%end%>
Here are the relevant models just to be safe. Note that I added the nested lines to both after I received this error because I saw it on another thread, but it doesn't seem to be fixing it.
class Unit < ApplicationRecord
has_and_belongs_to_many :users
has_and_belongs_to_many :assignments
accepts_nested_attributes_for :assignments
end
And the Assignment model:
class Assignment < ApplicationRecord
has_and_belongs_to_many :units
has_many :users, :through => :units
accepts_nested_attributes_for :units
end
The answer was a mix of a couple things, as Rockwell pointed out I was using User instead of Units, but that still didn't fix it. My collection had multiple choices set to false, so my controller wanted simply
params.require(:assignment).permit(:name, :description, :duedate, :unit_ids)
However, when I set multiple to true, that didn't work. Then, it wanted
params.require(:assignment).permit(:name, :description, :duedate, unit_ids[])
My solution was to leave multiple as true, and use the unit_ids[].
You have to update the permitted parameters
def assignment_params
params.require(:assignment).permit(:name, :description, :duedate, user_ids: [], unit_ids: [])
end
You mentioned that is was permitted, but I do not see unit_ids in the permitted params, I do see user_ids. Is there a spelling error? Or do you just need to include the unit_ids in there?
unit_ids is not a column name. You can use accept_nested_attribute or form object to solve this problem.
I have 2 tables, landslides and sources (maybe doesn't relate to each other). I want a form which lets user to fill in information and then submit to both tables. Here's my current form without sources fields:
= form_for :landslide, :url => {:controller => 'landslides', :action => 'create'} do |f|
.form-inputs
%form#landslideForm
.form-group.row
%label.col-sm-2.col-form-label{for: "textinput"}Date
.col-sm-10
= f.date_select :start_date, :class => "form-control"
#Some fields
.form-actions
= f.button :submit, class: "btn btn-lg btn-primary col-sm-offset-5", id: "submitButton"
And parameters:
def landslide_params
params.require(:landslide).permit(:start_date, :continent, :country, :location, :landslide_type, :lat, :lng, :mapped, :trigger, :spatial_area, :fatalities, :injuries, :notes)
end
def source_params
params.require(:source).permit(:url, :text, :landslide_id)
end
Also there's a column in sources calls landslide_id which take the landslide ID from table landslides. So when a user submits a new landslide, how can I take the upcoming landslide ID (which is auto increment, user doesn't need to fill in)?
Thanks!
HTML does not allow nested <form> elements and you can't pass the id of record that has not been persisted yet through a form (because it does not have an id).
To create a nested resource in the same request you use accepts_nested_attributes_for:
class Landslide
# or has_many
has_one :source
accepts_nested_attributes_for :source
end
class Source
belongs_to :landslide
end
This means that you can do Landslide.create(source_attributes: { foo: 'bar' }) and it will create both a Landslide and a Source record and will automatically link them through sources.landslide_id.
To create the form inputs use fields_for:
# use convention over configuration
= form_for #landslide do |f|
.form-inputs
.form-group.row
# use the form builder to create labels instead
= f.label :start_date, class: 'col-sm-2 col-form-label'
.col-sm-10
= f.date_select :start_date, class: "form-control"
%fieldset
%legend Source
= f.fields_for :sources do |s|
.form-group.row
= s.label :url, class: 'col-sm-2 col-form-label'
.col-sm-10
= s.text_field :url, class: "form-control"
# ...
class LandslidesController
# ...
def new
#landslide = Landslide.new
# this is needed to seed the form with inputs for source
#landslide.source.new
end
def create
#landslide = Landslide.new(landslide_params)
if #landslide.save
redirect_to #landslide
else
#landslide.source.new unless #landslide.source.any?
render :new
end
end
private
def landslide_params
params.require(:landslide).permit(
:start_date, :continent, :country,
:location, :landslide_type,
:lat, :lng, :mapped, :trigger, :spatial_area,
:fatalities, :injuries, :notes,
source_attributes: [ :url, :text ]
)
end
end
You need to use accept_nested_attributes_for and nest your form accordingly:
(With reservation in regards to what form should be nested in which, I use the example of Sources submitted via landslide-form.)
in landslide.rb
accept_nested_attributes_for :sources
In your view (I don't know haml but anyways)
<%= form_for :landslide do |f|%>
<%= f.select :start_date %>
<%= fields_for :sources do |s| %>
<%= s.input :your_column %>
<% end %>
<%= f.button :submit %>
<% end %>
Btw, there are a lot of questions on this already, it's called 'Nested Forms'
Nested forms in rails - accessing attribute in has_many relation
Rails -- fields_for not working?
fields_for in rails view
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
I looked for two days on the web but I am still blocked to update a child object of a parent object.
My parent:
class Pass < ActiveRecord::Base
has_many :fields
attr_accessor :fields_attributes
accepts_nested_attributes_for :fields, :allow_destroy => true, :update_only => true
My child:
class Field < ActiveRecord::Base
belongs_to :pass
My form view:
<%= f.fields_for :fields do |field|%>
<div class="control-group">
<%= field.label :id, :class => 'control-label' %>
<%= field.label :value, :class => 'control-label' %>
<div class="controls">
<%= field.text_field :value, :class => 'text_field' %>
</div>
</div>
<% end %>
I also define permitted parameters thanks to:
def pass_params params.require(:pass).permit(:pass,
:description,
:organization_name,
:logo_upload,
:icon_upload,
:strip_upload,
fields_attributes: [:id,:value])
#params.require(:pass).permit!
end
I have no problem to create a pass with 5 fields in my passes_controller
def new
#pass = Pass.new
5.times {#pass.fields.build}
##fields = #pass.fields
end
My problem happens is that child fields of my pass are not updated after an edit of the pass. I always get initial values (at the creation of the pass) of fields.
I tried to update using different ways without success
if #pass.update_attributes(pass_params)
if #pass.update_attributes[params[:pass][:fields_attributes]]
if #pass.update_attributes(params[:fields_attributes])
When I update my pass, the pass_params looks like this:
{"description"=>"Test22gg", "organization_name"=>"Toto", "fields_attributes"=>{"0"=>{"id"=>"30", "value"=>"testf"}, "1"=>{"id"=>"29", "value"=>"test"}, "2"=>{"id"=>"28", "value"=>"test"}, "3"=>{"id"=>"27", "value"=>"test"}, "4"=>{"id"=>"26", "value"=>"test"}}}
I don't see which requirement or thing I forget to update these fields!
I'm attempting to build a recipe-keeper app with three primary models:
Recipe - The recipe for a particular dish
Ingredient - A list of ingredients, validated on uniqueness
Quantity - A join table between Ingredient and Recipe that also reflects the amount of a particular ingredient required for a particular recipe.
I'm using a nested form (see below) that I constructed using an awesome Railscast on Nested Forms (Part 1, Part 2) for inspiration. (My form is in some ways more complex than the tutorial due to the needs of this particular schema, but I was able to make it work in a similar fashion.)
However, when my form is submitted, any and all ingredients listed are created anew—and if the ingredient already exists in the DB, it fails the uniqueness validation and prevents the recipe from being created. Total drag.
So my question is: Is there a way to submit this form so that if an ingredient exists whose name matches one of my ingredient-name fields, it references the existing ingredient instead of attempting to create a new one with the same name?
Code specifics below...
In Recipe.rb:
class Recipe < ActiveRecord::Base
attr_accessible :name, :description, :directions, :quantities_attributes,
:ingredient_attributes
has_many :quantities, dependent: :destroy
has_many :ingredients, through: :quantities
accepts_nested_attributes_for :quantities, allow_destroy: true
In Quantity.rb:
class Quantity < ActiveRecord::Base
attr_accessible :recipe_id, :ingredient_id, :amount, :ingredient_attributes
belongs_to :recipe
belongs_to :ingredient
accepts_nested_attributes_for :ingredient
And in Ingredient.rb:
class Ingredient < ActiveRecord::Base
attr_accessible :name
validates :name, :uniqueness => { :case_sensitive => false }
has_many :quantities
has_many :recipes, through: :quantities
Here's my nested form that displays at Recipe#new:
<%= form_for #recipe do |f| %>
<%= render 'recipe_form_errors' %>
<%= f.label :name %><br>
<%= f.text_field :name %><br>
<h3>Ingredients</h3>
<div id='ingredients'>
<%= f.fields_for :quantities do |ff| %>
<div class='ingredient_fields'>
<%= ff.fields_for :ingredient_attributes do |fff| %>
<%= fff.label :name %>
<%= fff.text_field :name %>
<% end %>
<%= ff.label :amount %>
<%= ff.text_field :amount, size: "10" %>
<%= ff.hidden_field :_destroy %>
<%= link_to_function "remove", "remove_fields(this)" %><br>
</div>
<% end %>
<%= link_to 'Add ingredient', "new_ingredient_button", id: 'new_ingredient' %>
</div><br>
<%= f.label :description %><br>
<%= f.text_area :description, rows: 4, columns: 100 %><br>
<%= f.label :directions %><br>
<%= f.text_area :directions, rows: 4, columns: 100 %><br>
<%= f.submit %>
<% end %>
The link_to and link_to_function are there to allow the addition and removal of quantity/ingredient pairs on the fly, and were adapted from the Railscast mentioned earlier. They could use some refactoring, but work more or less as they should.
Update: Per Leger's request, here's the relevant code from recipes_controller.rb. In the Recipes#new route, 3.times { #recipe.quantities.build } sets up three blank quantity/ingredient pairs for any given recipe; these can be removed or added to on the fly using the "Add ingredient" and "remove" links mentioned above.
class RecipesController < ApplicationController
def new
#recipe = Recipe.new
3.times { #recipe.quantities.build }
#quantity = Quantity.new
end
def create
#recipe = Recipe.new(params[:recipe])
if #recipe.save
redirect_to #recipe
else
render :action => 'new'
end
end
You shouldn't put the logic of ingredients match into view - it's duty of Recipe#create to create proper objects before passing 'em to Model. Pls share the relevant code for controller
Few notes before coming to code:
I use Rails4#ruby2.0, but tried to write Rails3-compatible code.
attr_acessible was deprecated in Rails 4, so strong parameters are used instead. If you ever think to upgrade your app, just go with strong parameters from the beginning.
Recommend to make Ingredient low-cased to provide uniform appearance on top of case-insensitivity
OK, here we go:
Remove attr_accessible string in Recipe.rb, Quantity.rb and Ingredient.rb.
Case-insensitive, low-cased Ingredient.rb:
class Ingredient < ActiveRecord::Base
before_save { self.name.downcase! } # to simplify search and unified view
validates :name, :uniqueness => { :case_sensitive => false }
has_many :quantities
has_many :recipes, through: :quantities
end
<div id='ingredients'> part of adjusted form to create/update Recipe:
<%= f.fields_for :quantities do |ff| %>
<div class='ingredient_fields'>
<%= ff.fields_for :ingredient do |fff| %>
<%= fff.label :name %>
<%= fff.text_field :name, size: "10" %>
<% end %>
...
</div>
<% end %>
<%= link_to 'Add ingredient', "new_ingredient_button", id: 'new_ingredient' %>
We should use :ingredient from Quantity nested_attributes and Rails will add up _attributes-part while creating params-hash for further mass assignment. It allows to use same form in both new and update actions. For this part works properly association should be defined in advance. See adjusted Recipe#new bellow.
and finally recipes_controller.rb:
def new
#recipe = Recipe.new
3.times do
#recipe.quantities.build #initialize recipe -> quantities association
#recipe.quantities.last.build_ingredient #initialize quantities -> ingredient association
end
end
def create
#recipe = Recipe.new(recipe_params)
prepare_recipe
if #recipe.save ... #now all saved in proper way
end
def update
#recipe = Recipe.find(params[:id])
#recipe.attributes = recipe_params
prepare_recipe
if #recipe.save ... #now all saved in proper way
end
private
def prepare_recipe
#recipe.quantities.each do |quantity|
# do case-insensitive search via 'where' and building SQL-request
if ingredient = Ingredient.where('LOWER(name) = ?', quantity.ingredient.name.downcase).first
quantity.ingredient_id = quantity.ingredient.id = ingredient.id
end
end
end
def recipe_params
params.require(:recipe).permit(
:name,
:description,
:directions,
:quantities_attributes => [
:id,
:amount,
:_destroy,
:ingredient_attributes => [
#:id commented bc we pick 'id' for existing ingredients manually and for new we create it
:name
]])
end
In prepare_recipe we do the following things:
Find ID of ingredient with given name
Set foreign_key quantity.ingredient_id to ID
Set quantity.ingredient.id to ID (think what happens if you don't do that and change ingredient name in Recipe)
Enjoy!