Nested fields causing rollback - ruby-on-rails

So I am facing this problem where nested field is causing a rollback on submit. I am using rails 5.
Here is the new and create actions of the controller
def new
#match = Match.new
#match.heros.build
end
def create
#match = cur_user.matches.build(matches_params)
#match.save
end
Here are the params
def matches_params
params.require(:match).permit(:map, heros_attributes: [:id, :hero])
end
Simplified form_for
= form_for(#match) do |f|
= f.label :map, value: "Map Played:"
= f.select "map",
[["Select Map", 0]
= f.label :heros, value: "Hero Played:"
= f.fields_for :heros do |h|
= h.select "hero",
[["Select Hero", 0]
= f.submit "Submit"
In match.rb I have
has_many :heros, dependent: :destroy
accepts_nested_attributes_for :heros
and in hero.rb I have
belongs_to :match
I get a rollback on pressing submit and on running #match.errors.full_messages I get ["Heros match must exist"]
Any help would be greatly appreciated.
Edit: Views are in haml.

The plural of hero is heroes, and not heros. Change your code so that it specifies heroes instead of heros, and retry.

Related

Unpermitted parameters nested attributes - rails

I'm trying to submit a form to 2 tables but somehow I got this syntax error unexpected '\n' at this line joins: ['sources'], :landslide_id and found unpermitted parameter: sources in landslide params. Here's all the files
Models
class Landslide < ApplicationRecord
has_many :sources, dependent: :destroy
accepts_nested_attributes_for :sources
class Source < ApplicationRecord
belongs_to :landslide
end
landslides_controller.rb
def new
#landslide = Landslide.new
#landslide.sources.build
end
# POST /landslides
def create
#landslide = Landslide.new(landslide_params)
#landslide.save
end
private
# Use callbacks to share common setup or constraints between actions.
def set_landslide
render json: Landslide.find(params[:total_id]),
joins: ['sources'], :landslide_id
end
# Only allow a trusted parameter "white list" through.
def landslide_params
params.require(:landslide).permit(:start_date, :continent, :country, :location, :landslide_type, :lat, :lng, :mapped, :trigger, :spatial_area, :fatalities, :injuries, :notes, sources_attributes: [ :url, :text ])
end
sources_controller.rb
def set_source
#source = Source.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def source_params
params.require(:source).permit(:url, :text)
end
_form.html.haml
= form_for :landslide, :url => {:controller => 'landslides', :action => 'create'} do |f|
#something
%fieldset
%legend Source
= f.fields_for :sources do |s|
.form-group.row
%label.col-sm-2.col-form-label{for: "textinput"}URL
.col-sm-10
= s.text_field :url, class: "form-control"
.form-group.row
%label.col-sm-2.col-form-label{for: "textinput"}Text
.col-sm-10
= s.text_field :text, class: "form-control"
Request
{"utf8"=>"✓",
"authenticity_token"=>"W3m2dLTGyuPCbP6+pStWDfgpIbPzGdl4tvf01vMAbyozzkimqlXH4B/RtwBcsLb+iiBqms7EHagY+Anbpo4zNg==",
"landslide"=>
{"start_date(3i)"=>"27",
"start_date(2i)"=>"4",
"start_date(1i)"=>"2017",
"continent"=>"Africa",
"country"=>"Country",
"location"=>"Location",
"landslide_type"=>"1",
"lat"=>"1",
"lng"=>"1",
"mapped"=>"False",
"spatial_area"=>"1",
"fatalities"=>"1",
"injuries"=>"1",
"notes"=>"1",
"trigger"=>"1",
"sources"=>{"url"=>"url", "text"=>"text"}},
"button"=>""}
found unpermitted parameter: sources
Based on your form, it looks like sources are inside a param called sources rather than sources_attributes. Edit your landslide_params, changing sources_attributes to sources.
May I ask what set_landslide is trying to render, or correct me if I am wrong below? Placing joins on a new line causes the error. I am thinking you're trying to do something like:
landslide = Landslide.find(params[:total_id])
render json: landslide.to_json(:include => { :sources => { landslide_params[:sources] }})
Which would give you a json with the landslide object and a sources array. The landslide id should be within the landslide object. This of course assumes that's what you were going for.

Rails 4 Has One Update Pb

I have a bit of a problem with a "has one" association on my app.
What I want to achieve is to be able to attach an optional quote to the topic. The quote can only be used once (in other words, if it's used for topic 1, it can't be used for any other topics).
I have a Topic model and a Quote model.
Topic has one quote.
Quote belongs to topic.
I also want to be able to attach a quote to other models (ex. Profile Model).
I'm really confused on what to do on my "edit topic" view as well as in the controller. I thought it would work like a "one to many" association, which I had no problem configuring. Somehow the "has one" is more complicated (for me!)
What I'd like is to have in the "edit topic" view a radio list of the available quotes which I can freely update. (Same for the "new topic" view).
My current controller:
def edit
#topic = Topic.find(params[:id])
#quote = #topic.quote
#packages = #topic.packages
#books = #topic.books
#tasklists = #topic.tasklists
#links = #topic.links
#terms = #topic.terms
end
def update
#topic = Topic.find(params[:id])
if #topic.update_attributes(topic_params)
flash[:success] = t('helpers.success-update', model: "topic")
redirect_to backend_topics_url
else
render partial: 'edit'
end
end
def topic_params
params.require(:topic).permit(:topic_id, :theme_id, :cover, :topic_status, :topic_access, :slug, *Topic.globalize_attribute_names, :quote_attributes => [:id, :topic_id], :package_ids => [], :book_ids => [], :link_ids => [], :tasklist_ids => [], :term_ids => [])
end
My current Topic model:
has_one :quote
accepts_nested_attributes_for :quote
My current Quote model:
belongs_to :topic
And my "Edit Topic" view:
<h4>Quote</h4>
<% if #quote %>
<h5>Current quote</h5>
<%= #quote.quote %> <%= link_to('[change]', '#') %>
<% end %>
<%= f.input :quote, :collection => Quote.all, :label_method => :quote, :label_value => :id, :checked => #quote.id, as: :radio_buttons %>
I'm sure there is something obvious that I'm missing but I can't figure out what.
Any ideas?
Thanks!
- Vincent
First off, if you want to have quote belong to multiple models, you will need a polymorphic association. Otherwise, you would need to add multiple foreign ids to quote like this: topic_id, profile_id etc and that will get messy fast. You can view a screencast on polymorphism here: http://railscasts.com/episodes/154-polymorphic-association-revised
has_one and belongs_to is basically the exact same as has_many and belongs_to except you are only dealing with 1 record instead of a collection of records.
For your current setup - in your edit action you need to fetch all the quotes that are not associated to any Topics. You can do that like this:
#available_quotes = Quote.where(topic_id: nil)
and then:
<%= f.input :quote, :collection => #available_quotes, :label_method => :quote, :label_value => :id, :checked => #quote.id, as: :radio_buttons %>
instead of
Quote.all in your form which is returning all quotes.
If you move to a polymorphic model, watch the screencast, and you would replace "commentable_id" in the screencast with something like "quotable_id" and then in your edit action to find the unassigned quotes you would do this:
#quotes = Quote.where(quotable_id: nil)

has_one nested attributes not saving

I have two models Project and ProjectPipeline.
I want to create a Project form that also has fields from the ProjectPipeline model. I have created the form successfully but when I hit save the values aren't stored on the database.
project.rb
class Project < ActiveRecord::Base
has_one :project_pipeline
accepts_nested_attributes_for :project_pipeline
self.primary_key = :project_id
end
projectpipeline.rb
class ProjectPipeline < ActiveRecord::Base
belongs_to :project, autosave: :true
validates_uniqueness_of :project_id
end
I don't always want a project pipeline but under the right conditions based on a user viewing a project. I want the project pipeline fields to be built, but only saved if the user chooses to save/populate them.
So when the project is shown I build a temporary project pipeline using the project_id: from params[:id] (not sure if I really need to do this). Then when the project is saved I use the create_attributes. But if it has already been created or built I just want to let the has_one and belongs_to association kick in and then use update_attributes.
My issue is when I am trying to save, I am either hitting a 'Forbidden Attribute' error if I use params[:project_pipeline] or having nothing saved at all if I used project_params. I have checked and rechecked all my fields are in project_params and even tried using a project_pipeline_params but that didn't feel right.
It is driving me nuts and I need to sleep.
projects_controller.rb
def show
#project = Project.find(params[:id])
if #project.project_pipeline
else
#project.build_project_pipeline(project_id: params[:id])
end
autopopulate
end
def update
#project = Project.find(params[:id])
if #project.project_pipeline
else
#project.build_project_pipeline(project_id: params[:id], project_type: params[:project_pipeline][:project_type], project_stage: params[:project_pipeline][:project_stage])
end
if #project.update_attributes(project_params)
flash[:success] = "Project Updated"
redirect_to [#project]
else
render 'edit'
end
end
def project_params
params.require(:project).permit(:user_id, project_pipeline_attributes:[:project_id,:project_type,:project_stage,
:product_volume,:product_value,:project_status,:outcome, :_destroy])
end
show.html.haml
- provide(:title, "Show Project")
%h1= #project.project_title
= simple_form_for(#project) do |f|
= f.input :id, :as => :hidden, :value => #project, :readonly => true
= f.input :user_id, label: 'Assigned to Account Manager', :collection => #account_managers, :label_method => lambda { |r| "#{r.first_name} #{r.last_name}" }
= f.input :project_id, :readonly => true
= f.input :status, :readonly => true
= f.input :project_stage, :readonly => true
- if #project.project_codename = "project pipeline"
= simple_fields_for #project.project_pipeline do |i|
%h2 Project Pipeline
- if #project.user_id == current_user.id
= i.input :project_volume, label: 'Project Status', collection: #project_status
= i.input :project_value, label: 'Project Status', collection: #project_status
= i.input :project_status, label: 'Project Status', collection: #project_status
= i.input :outcome, label: 'Outcome', collection: #outcome
= f.submit 'Save'
If you've gotten this far I sincerely thank you.
Solution
You need to change few things here. Firstly:
= simple_fields_for #project.project_pipeline do |i|
When you pass the object, rails have no idea it is to be associated with the parent object and as a result will create a field named project[project_pipeline] instead of project[project_pipeline_attributes]. Instead you need to pass the association name and call this method on the form builder:
= f.simple_fields_for :project_pipeline do |i|
This will check find out that you have defined project_pipeline_attributes= method (using accept_nested_attributes_for` and will treat it as association. Then in your controller change your show action to:
def update
#project = Project.find(params[:id])
#project.assign_attributes(project_params)
if #project.save
flash[:success] = "Project Updated"
redirect_to #project
else
render 'edit'
end
end
And all should work. As a separate note, since you are allowing :_destroy attribute in nested params, I am assuming you want to be able to remove the record using nested attributes. If so, you need to add allow_destroy: true to your accepts_nested_attributes_for call.
Now a bit of styling:
You can improve your show action a bit. First of all, I've noticed you are building an empty pipeline in every single action if none has been declared yet. That mean that you probably should move this logic into your model:
class Project < AR::Base
after_initalize :add_pipeline
private
def add_pipeline
project_pipeline || build_project_pipeline
end
end
You also have the mysterious method prepopulate - most likely it should be model concern as well.
Another point: This syntax:
if something
else
# do sth
end
is somehow quite popular and makes the code unreadable as hell. Instead, use:
if !something
# do something
end
or (preferred)
unless something
# do something
end
I'm not sure from your description if this is the problem, but one the thing is that a update_attributes with a has_one, by default, will rebuild the children(!), so you would lose the attributes you initialised. You should provide de update_only: true option to accepts_nested_attributes_for.
You can find more on this here, in the rails docs. The line would be this:
accepts_nested_attributes_for :project_pipeline, update_only: true
Considering the after_initialize, that would result in every project always having a pipeline. While that could be desirable, it isn't necessarily, depending on your domain, so I'd be a bit careful with that.
Cheers,
Niels

Trouble setting param values in controller for form

I have a form that lets users add a new blocked tv show to their list of blocked shows. The form is not taking the param values (:user_id, :title, :image) that I tried to set in the controller. I'm a beginner, so I'm guessing the syntax is the problem.
Also I am getting a Couldn't find Tvshow without Id error when trying to use the #tvshow instance variable to set the param values of :title and :image. Each Blocked show should have the same title and image as the tvshow that the user selects in the collection_select. Is there an easier way to do this?
View
<%= form_for #blockedshow do |b| %>
<%= b.label :tvshow_id, "Add a Blocked TV Show " %><br/>
<%= collection_select(:blockedshow, :tvshow_id, Tvshow.all, :id, :title, prompt: true) %>
<%= submit_tag 'Add' %>
<% end %>
Controller
class BlockedshowsController < ApplicationController
def new
#blockedshow = Blockedshow.new
end
def create
#tvshow = Tvshow.find params[:blockedshow][:id]
#blockedshow = Blockedshow.new(safe_blockedshow)
params[:user_id] = current_user.id
params[:title] = #tvshow.title
params[:image] = #tvshow.image
if #blockedshow.save
flash[:notice] = "New Blocked TV Show added successfully"
redirect_to tv_show_index_path
else
render 'new'
end
end
private
def safe_blockedshow
params.require(:blockedshow).permit(:title, :user_id, :tvshow_id, :image)
end
end
Blockedshow model
class Blockedshow < ActiveRecord::Base
has_many :phrases
has_many :tvshows
belongs_to :user
end
Tvshow model
class Tvshow < ActiveRecord::Base
has_many :phrases
belongs_to :blockedshow
def self.search_for (query)
where('title LIKE :query', query: "%#{query}%")
end
end
Routes
resources :blockedshows
post 'blockedshows', to:'blockedshows#create#[:id]'
you are getting the issue because params[:blockedshow][:id] is not passed, if your trying to access the Tvshow id selected by from the drop-list you can do the following
#tvshow = Tvshow.find params[:blockedshow][:tvshow_id]
Just fixed by changing the controller to this:
def create
#tvshow = Tvshow.find params[:blockedshow][:tvshow_id]
#blockedshow = Blockedshow.new(
:user_id =>current_user.id,
:title=> #tvshow.title,
:image=> #tvshow.image,
:tvshow_id=>#tvshow.id
)

Passing params to a new template on a failed create action when using accepts_nested_attributes_for

I may just be missing something simple, but I am relatively inexperienced so it is likely. I've searched extensively for a solution without success.
I am using the fields_for function to build a nested form using the accepts_nested_attributes_for function. If the submit on the form fails the params are passed to the render of the new template only for the parent model. How do I pass the nested params for the child model so that fields that have been filled out previously remain filled. Note that I am using simple_form and HAML but I assume this shouldn't impact the solution greatly.
My models:
class Account < ActiveRecord::Base
attr_accessible :name
has_many :users, :dependent => :destroy
accepts_nested_attributes_for :users, :reject_if => proc { |a| a[:email].blank? }, :allow_destroy => true
end
class User < ActiveRecord::Base
attr_accessible :email, :password, :password_confirmation
belongs_to :account
end
My accounts controller:
def new
#account = Account.new
#account.users.build
end
def create
#account = Account.new(params[:account])
if #account.save
flash[:success] = "Welcome."
redirect_to #account
else
#account.users.build
<- I suspect I need something here but unsure what
render :new
end
end
The key part of the accounts/new view:
= simple_form_for #account do |f|
= f.input :name
= f.simple_fields_for :users do |u|
= u.input :email
= u.input :password
= u.input :password_confirmation
= f.button :submit, :value => "Sign up"
My params on a failed save are:
:account {"name"=>"In", "users_attributes"=>{"0"=>{"email"=>"u#e.com", "password"=>"pass", "password_confirmation"=>"pass"}}}
As you can see, the key information, in the users_attributes section, is stored but I can't seem to have the email address default into the new form. Account name on the other hand is filled automatically as per Rails standard. I'm not sure if the solution should live in the accounts controller or in the accounts/new view, and have not had any luck with either.
Answers with .erb are, of course, fine.
I'm fairly new to Ruby and Rails so any assistance would be much appreciated.
The problem lies with attr_accessible, which designates the only attributes allowed for mass assignment.
I feel a bit silly in that I actually stated the problem in a comment last night and failed to notice:
accepts_nested_attributes_for :users will add a users_attributes= writer to the account to update the account's users.
This is true, but with attr_accessible :name, you've precluded every attribute but name being mass-assigned, users_attributes= included. So when you build a new account via Account.new(params[:account]), the users_attributes passed along in params are thrown away.
If you check the log you might note this warning:
WARNING: Can't mass-assign protected attributes: users_attributes
You can solve your original problem by adding :users_attributes to the attr_accessible call in the account class, allowing it to be mass-assigned.
Amazingly, after reading a blog post this evening, and some more trial and error, I worked this out myself.
You need to assign an #user variable in the 'new' action so that the user params are available for use in the 'create' action. You then need to use both the #account and #user variables in the view.
The changes look like this.
Accounts Controller:
def new
#account = Account.new
#user = #account.users.build
end
def create
#account = Account.new(params[:account])
#user = #account.users.build(params[:account][:user]
if #account.save
flash[:success] = "Welcome."
redirect_to #account
else
render :new
end
end
The accounts/new view changes to:
= simple_form_for #account do |f|
= f.input :name
= f.simple_fields_for [#account, #user] do |u|
= u.input :email
= u.input :password
= u.input :password_confirmation
= f.button :submit, :value => "Sign up"
In this case the params remain nested but have the user component explicitly defined:
:account {"name"=>"In", "user"=>{"email"=>"user#example.com", "password"=>"pass", "password_confirmation"=>"pass"}}
It has the additional side effect of removing the #account.users.build from within the else path as #numbers1311407 suggested
I am not certain whether their are other implications of this solution, I will need to work through it in the next few days, but for now I get the information I want defaulted into the view in the case of a failed create action.
#Beerlington and #numbers1311407 I appreciate the help in guiding me to the solution.

Resources