Rails - Issue with permitting params & require - ruby-on-rails

I'm having an issue with submitting my params,
portrait and portrait_tag are passed through my form and are siblings,
how would I go permitting both of these?
Output
{"utf8"=>"✓", "_method"=>"patch", "authenticity_token"=>"", "portrait"=>{"artist_image"= "", #original_filename="rubytraining.jpg", #content_type="image/jpeg"}, "portrait_tag"=>{"tag_ids"=>["", "1", "2", "3", "4"]}, "commit"=>"Save changes", "controller"=>"admin/portraits", "action"=>"update", "id"=>"72"}
I've tried the following
private
def portrait_params
params.require(:portrait).permit(:id, :artist_image)
params.require(:portrait_tag).permit(:id, :tag => [])
end
These work separately but overwrite one another when added together
controller
def update
#portrait = Portrait.where(artist_id: 33, id: params[:id]).take
if #portrait.update(portrait_params)
redirect_to :edit_admin_portrait, flash: {notice: "Successfully updated your information"}
else
flash[:system] = #portrait.errors.full_messages
p #portrait.errors.full_messages
render :edit
end
end
private
def portrait_params
params.require(:portrait).permit(:id, :artist_image)
params.require(:portrait_tag).permit(:id, :tag => [])
end
Edit Form
%h1 Edit Portrait
= form_for [:admin, #portraits] do |f|
- if flash[:system].present?
- flash[:system].each do |e|
%div= e
- if flash[:notice].present?
%div= flash[:notice]
= f.file_field :artist_image
= collection_select :portrait_tag, :tag_ids, Tag.all, :id, :name, {:selected => #portraits.tag_ids}, { :multiple => true}
= f.submit "Save changes", class: "btn btn-primary"

the tags are an association of the portrait (portrait_tags).
Finally, the core of the problem. In this case, you should be using accepts_nested_attributes_for. It will allow you to post attributes for tags inside your :portrait params.
So your strong params method would look like this:
def portrait_params
params.require(:portrait).permit(:id, :artist_image, portrait_tags_attributes: [:id, :tag])
end
Of course, amend your forms accordingly.

You are returning only last line of portrait_params.
params.require(:portrait_tag).permit(:id, :tag => [])
I'd do it like this:
Make two private methods with params(portrait, portrait_tag) , they will return you Hash'es, then you can merge them. It is not clear to have method portrait_params which return you
portrait_tag params if they are not in nested attributes.
If they are you can use but you have to add this to model
method accepts_nested_attributes_for
and then you can do it like this
def portrait_params
params.require(:portrait).permit(:id, :artist_image, portrait_tags_attributes: [:id, :tag]))
end

You can use nested_attributes to update parent and child same time. But you have to made some changes, for instance you need to change params structure.
Ex:
params.require(:portrait).permit(:attr1,:att2,..., portrait_attributes: [:attr1, :attr2])
Ref: http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html

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.

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

Select box not keeping param value when validation errors occur - Rails 3

The problem I have is the select box in my view does not maintain the value selected if a validation error occurs.
I have business and category relationship which has a many to many association
category.rb
class Category < ActiveRecord::Base
has_and_belongs_to_many :businesses
business.rb
class Business < ActiveRecord::Base
has_and_belongs_to_many :categories
In my view I have a select field
<%= f.select(:category_tokens, Category.all.collect {|c| [ c.name, c.id ] }, { :include_blank => "Select Category", :selected => params['category'] }}) %>
I can access the business categories in the console using business.categories which returns and array.
In my view to troubleshoot the params I've added.
<%= #business.categories %>
<%= #business.attributes.inspect %>
<%= #business.user.attributes.inspect %>
The output for these shows provides the following
[#<Category id: 2, name: "kitchen renovations", created_at: "2012-10-19 14:16:52", updated_at: "2012-10-19 14:16:52">]
{"id"=>nil, "business_name"=>"", ... additional attributes}
{"id"=>nil, "email"=>"", ... additional attributes}
The params hash looks like this
Processing by BusinessesController#create as HTML
Parameters: {"utf8"=>"✓",
"authenticity_token"=>"fVz1mJZI8QT/HYKRph8YTvzRec0IVV0RS55v5QD5SL0=",
"business"=>{"category_tokens"=>"2",
"location"=>"", "service_area"=>"20",
"business_name"=>"", "abn_number"=>"",
"business_description"=>"",
"address"=>"", "suburb"=>"",
"notification_type"=>"",
"user_attributes"=>{"first_name"=>"", "phone"=>"", "email"=>"", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}}, "commit"=>"Sign up my Business"}
So the category is being set but I'm not sure how to add this to my select in the view to use the category as the select value when a validation error occurs.
EDIT -- adding controller code ----
class BusinessesController < ApplicationController
def new
#business = Business.new
#business.build_user
end
def create
#business = Business.new(params[:business])
respond_to do |format|
if #business.save
cookies[:auth_token] = #business.user.auth_token
format.html { redirect_to jobs_users_path, notice: 'Your business was successfully created.' }
else
format.html { render action: "new" }
end
end
end
I fixed this issue by making a few simple changes.
in my controller I added the following
def create
#...
category = (params[:business][:category_tokens])
#category = category
#...
end
Then used this in my view for the selected option.
<%= f.select(:category_tokens, Category.all.collect {|c| [ c.name, c.id ] }, { :include_blank => "Select Category", :selected => #category }}) %>
Updating as of 2019, If your f.select looks like this :
<%= f.select(:sector , options_for_select([["Please select", "Please select"], ["Energy", "Energy"], ["Metals","Metals"], ["Agriculture","Agriculture"], ["Renewables","Renewables"], ["Natural Ressources","Natural Ressources"], ["Other","Other"]])) %>
All you have to do is remove options_for_select( ... ) and the page will "remember" what you selected if a validation error occurs.
<%= f.select(:sector , [["Please select", "Please select"], ["Energy", "Energy"], ["Metals","Metals"], ["Agriculture","Agriculture"], ["Renewables","Renewables"], ["Natural Ressources","Natural Ressources"], ["Other","Other"]]) %>
I do not know how this works with multi-select inputs. I also used my own example for better readibility.

Nested Attributes not being saved

I have a form for creating/editing an event. There is a Client drop down which renders some custom questions and answers dependent on the selected Client.
I'm running Rails 2.3.9 with Ruby 1.8.6 and Mongrel on Windows.
Here is the relevant code:
Event form
- form_for #event do |f|
....
= f.collection_select(:client_id, Client.all, :id, :name)
#custom_client_fields
= render 'client_fields' if #client
= observe_field :event_client_id, :url => {:action => 'client_fields'},
:with => "'client_id=' + encodeURIComponent(value)"
....
_client_fields.html.haml
- fields_for "event[client_attributes]", #client do |client|
- client.fields_for :questions do |question|
%label.question= question.object.content
- question.fields_for :answers, Answer.new do |answer|
= answer.text_field:content, :maxlength => 150, :size => 40
Event Controller
def client_fields
if params[:client_id].blank?
render_no_client_fields #self explanatory
else
respond_to do |format|
format.js {
render :update do |page|
page[:custom_client_fields].replace_html :partial => 'client_fields', :layout => false
end
}
end
end
end
Parameter hash
Parameters: {
"event"=>{
"id"=>"2",
"client_attributes"=>{
"questions_attributes"=>{
"0"=>{
"id"=>"4",
"answers_attributes"=>{
"0"=>{
"content"=>"fjhkghjkk"
}
}
}
}
}
}
}
Basically the form passes the validations and everything except the nested attributes. Nothing is inserted into the database table.
Looking at the parameter hash my client_attributes doesn't have an id... hmmm...
In the client_fields partial I had to add the following code to set the client_attributes_id:
<input id="event_client_attributes_id" name="event[client_attributes][id]" type="hidden" value="#{#client.id}">
Note to self: When you don't use Rails' magic form helpers you have to build the rest of the form as well.

How can I get form_for to autopopulate fields based upon a non-model hash?

I'm building a multi-step form in rails. It's not javascript driven, so each page has its own controller action like "step1" "step2" etc. I know how to do multi-step wizards through JQuery but I don't know how to keep rails validations per page without getting into javascript, hence this way.
Anyways, my model is a User object but I'm storing all my variables in an arbitrary Newuser variable and using the following in the view:
<% form_for :newuser, :url => { :action => "step3" } do |u| %>
In the controller, I merge the current page's info with the overall hash using:
session[:newuser].merge!(params[:newuser])
This works great except that if the user clicks back to a previous page, the fields are no longer populated. How do I keep them populated? Do I need to change the object in the form_for to somehow refer to the session[:newuser] hash?
EDIT:
I guess I'm looking for more info on how form_for autopopulates fields within the form. If it's not built around a model but an arbitrary hash (in this case, session[:newuser]), how do I get it to autopopulate?
This is how we did a multi-step form with validations
class User < ActiveRecord::Base
attr_writer :setup_step
with options :if => :is_step_one? do |o|
o.validates_presence_of :name
end
with options :if => :is_step_two? do |o|
o.validates_presence_of :email
end
def setup_step
#setup_step || 1
end
def is_step_one?
setup_step == 1
end
def is_step_two?
setup_step == 2
end
def last_step?
is_step_two? #change this to what your last step is
end
end
Then in the controller:
UsersController
SETUP_STEPS{1 => 'new', 2 => 'step_two'}
def new
#user = User.new
end
def step_two
end
def create
#user = User.new(params[:user])
if !#user.valid?
render :action => SETUP_STEPS[#user.setup_step]
elsif #user.last_step?
#user.save
#do stuff
else
render :action => SETUP_STEPS[#user.setup_step]
end
end
end
And then in your forms, they are like like any other rails form with one exception, you will need a hidden field to hold the values from your previous steps.
- form_for #user, :url => users_path do |f|
- [:login, :password].each do field
= f.hidden_field field
What about still using a class for your population?
class User
attr_accessor :credit_card, :name, :likes_fried_chicken
def initialize(options = {})
options.each do |key, value|
self.send("#{key}=", value)
end
end
end
you could use some tableless model functions here if you wanted to include some validations.
Then in your controller steps:
def step_one
#user = User.new(session[:new_user])
end
your forms should continue to work.
Another option is just to set the value of the form objects directly from your session hash
- form_for :user, :url => step_2_path do |f|
= f.text_field :name, :value => session[:new_user][:name]

Resources