I am having some trouble saving arrays in Rails.
Rails version: 4.2.3 | Ruby version: 2.2.1 | DB: PostgreSQL
In my view, I have a collection of check boxes that shows the conferences that my member attended.
<%= f.fields_for :conferences_member do |conference| %>
<%= collection_check_boxes(:conferences_member, :conference_id, #all_conferences, :id, :name)%>
<% end %>
I put a break point (binding.pry) after the create action in my MembersController, and surprisingly, it shows the selected check boxes:
Processing by Database::MembersController#create as HTML
Parameters: {"utf8"=>"✓","authenticity_token"=>"XYZ==",
[...] "conferences_member"=> {"conference_id"=>["3", "5", ""]}, [...]
Now, if I go to rails c, and type ConferencesMember.last to check what was saved, I get:
pry(main)> ConferencesMember.last
ConferencesMember Load (0.5ms) SELECT "conferences_members".* FROM
"conferences_members" ORDER BY "conferences_members"."id" DESC LIMIT 1
=> nil
These are my associations:
#=> member.rb
has_one :conferences_member
accepts_nested_attributes_for :conferences_member, allow_destroy: true, reject_if: :all_blank
#=> conferences_member.rb
serialize :conference_id, Array
belongs_to :member
#=> members_controller.rb
params.require(:member).permit( [...]
:conference_member_attributes => [:id, :member_id, :conference_id => []],
[...])
I want to thank you in advance. I've tried almost everything here on StackOverflow, but I don't see my error.
Thank you again.
EDIT:
More of my MembersController:
def new
#member = Member.new
#member.build_conferences_member
end
def create
#member = Member.new(member_params)
binding.pry
end
The log doesn't show any error, it just shows that conferences were not saved at all.
First, your field needs to be renamed to nest the :conference_id in :conferences_member_attributes (not in :conferences_member as you do now). Take advantage of the form object yielded by fields_for:
<%= f.fields_for :conferences_member do |conference| %>
<%= conference.collection_check_boxes :conference_id, #all_conferences, :id, :name %>
<% end %>
You also need to actually save the record in the create action: Member.new builds the record but does not save it. Typically, the create action branches based on whether the record saved or did not (due to validations). So you might rewrite this method like so:
def create
#member = Member.new(member_params)
# when #member.save returns true, it saved to the db successfully
if #member.save
redirect_to members_path, notice: "Member #{#member.id} saved!"
# otherwise, it didn't save because of a validation error, so we render the error
# to the user and give them a chance to fix it
else
flash[:error] = "Member didn't save: #{#member.errors.full_messages.to_sentence}"
render :new
end
end
Lastly, to make sure your data gets through your strong parameters, check your logs for any messages that parameters were filtered out. The messages look like:
Unpermitted parameters: your_favorite_attribute
Related
I struggled to digest this into a title.
I'm using SimpleForm to construct a bulk-edit page with one or more fieldsets - one for each record in a collection of ActiveRecord models that have been built but not yet saved.
My form looks like this:
= simple_form_for :courses, method: :patch do |f|
- #courses.each do |course|
= field_set_tag do
= f.simple_fields_for 'courses[]', course do |c|
= c.input :title
.row
.medium-6.columns
= c.input :start_date, as: :string, input_html: { class: 'input-datepicker' }
.medium-6.columns
= c.input :end_date, as: :string, input_html: { class: 'input-datepicker' }
= f.submit 'Save', class: 'primary button'
The params hash for one record looks like this:
"courses"=>{"courses"=>[{"title"=>"Course Y", "start_date"=>"2017-09-26", "end_date"=>"2017-07-31"}]}
with an array, while for two records it looks like this:
"courses"=>{"courses"=>{"1"=>{"title"=>"Course X", "start_date"=>"2018-01-16", "end_date"=>"2018-07-30"}, "2"=>{"title"=>"Course Y", "start_date"=>"2017-09-26", "end_date"=>"2018-07-30"}}}
with a stringy-integer-keyed hash.
This becomes a problem when I try and use strong parameters. After much hacking, I ended up with this piece of code, which works for multiple records but fails when only one is submitted:
ActionController::Parameters
.new(courses: params[:courses][:courses].values)
.permit(courses: [:title, :start_date, :end_date])
.require(:courses)
It fails with param is missing or the value is empty: courses highlighting the .require(:courses) line above.
The problem is "solved" by harmonising the single-record case with the multiple-record case:
if params[:courses][:courses].is_a?(Array)
params[:courses][:courses] = { '1': params[:courses][:courses][0] }
end
but it feels like there should be a simpler way of doing it.
Is there a better way to write the form for this use-case? Am I missing a trick with strong parameters?
I'm using rails 5.0.5 and simple_form 3.5.0.
"but it feels like there should be a simpler way of doing it."
Yes, use ajax to send individual create/update requests. This can be done transparently to the user and provides simpler code and a far better user experience.
Rails has fields_for and accepts_nested_attributes that can be used to create/update multiple child records and the parent record in a single request. But it really requires a association that groups the records together and even at this can get really hacky and convoluted when it comes to validations.
You want to set it up so that you have a seperate form for each record:
- courses.each do |c|
= render partial: 'courses/_form', course: c
There is really nothing to the form:
# courses/_form.haml.erb
= simple_form_for course, remote: true, html: { 'data-type' => 'json', class: 'course_form'} do |f|
= c.input :title
.row
.medium-6.columns
= c.input :start_date, as: :string, input_html: { class: 'input-datepicker' }
.medium-6.columns
= c.input :end_date, as: :string, input_html: { class: 'input-datepicker' }
= f.submit 'Save', class: 'primary button'
Instead of using a js.erb template we use 'data-type' => 'json' and write our own handler as its easier to target the correct form:
$(document).on('ajax:success', '.course_form', function(event, xhr, status){
var $form = $(this);
alert('Course created');
if (this.method.post) {
// changes form to update instead.
this.method = 'patch';
this.action = xhr.getResponseHeader('Location');
}
});
$(document).on('ajax:error', '.course_form', function(event, xhr, status){
var $form = $(this);
// #todo display errors
});
Creating the controller is very straight forward:
class CoursesController
def create
#course = Course.new(course_params)
respond_to do |format|
if #course.save(course_params)
format.json { head :created, location: #course }
else
format.json do
render json: {
errors: #course.errors.full_messages
}
end
end
end
end
def update
#course = Course.find(params[:id])
respond_to do |format|
if #course.update(course_params)
format.json { head :ok }
else
render json: {
errors: #course.errors.full_messages
}
end
end
end
end
Keep your form, change strong params to this:
params.require(:courses).permit(
courses: [
:id,
:title,
:start_date,
:end_date
]
)
With this code params should be without index key, #courses is just an array:
# CoursesController
def new
#courses = []
# creating 3 items for example
3.times do
#courses << Course.new
end
end
def create
errors = false
#courses= []
# keep courses in the array for showing errors
courses_params[:courses].each do |params|
course = Course.new(params)
#courses << course
unless course.valid?
errors = true
end
end
if errors
render :new
else
# if no errors save and redirect
#courses.each(&:save)
redirect_to courses_path, notice: 'courses created'
end
end
It turns out that the f.simple_fields_for 'courses[]' ... method only gives that fieldset an ID if the form is populated by an existing record, and the params structure of a string ID mapping to a course hash is only used in this case. For "fresh" records, there is no ID and the course hashes are placed in a plain array.
This bit of code was running in the context of "rolling over" courses from one year to another - copying a previous course and changing the dates. This meant that each fieldset had the ID of the original course.
When the form was submitted, a new record was created and validated with the new attributes, and it was this fresh record with no ID that repopulated the form. The "it only happens when one course is submitted" thing was a red herring - a product of the test scenario.
So worth noting: f.simple_fields_for 'courses[]' ... creates an array for new records and a hash mapping IDs to attributes for existing records.
I'm stuck on this... Asked a few questions already, but can't get my head around this.
I have a form for adding bibliography (model Biblio) that has a simple validation field on title of the bibliography.
Validation always fails, even when valid data is added.
MODEL
class Biblio < ApplicationRecord
# validates_presence_of :auteurs => there's a nested form too but
# I commented it out in order to isolate the problem
accepts_nested_attributes_for :auteurs
validates :titre, presence: true
CONTROLLER
(full text and I didn't translate in order to avoid typos)
def new
#biblio = Biblio.new(params_biblio)
#biblio.auteurs.build
end
def nouveau
# this method renders 'nouveau.html.erb',
# that contains the form allowing the addition of bibliography
#biblio = Biblio.new
if params[:id] # id is an optional parameter
#auteur = Auteur.find(params[:id])
#idauteur = #auteur.id
end
end
def ajouter
# is the method that treats the post form that was sent
#biblio = Biblio.new
if #biblio.save
# the 4 following lines are irrelevant here as they only add the
# second and subsequent authors to the join table.
# No validation and works fine.
b = auteurs_devises(params[:biblio][:auteurs])
aut = b.map do |var|
lett = Auteur.find(var)
lett.biblios << #biblio
end
redirect_to voir_biblio_url(Biblio.last)
else
if params[:id]
#auteur = Auteur.find(params[:id])
#idauteur = #auteur.id
end
render 'nouveau'
end
end
THE VIEW:
<%= form_for :biblio, url: administration_ajoute_biblio_url do |f| %>
<%= f.fields_for :auteurs do |aut| %>
<%= aut.label t('auteur') %>
<%= aut.text_field :nom , :name =>"biblio[auteurs][nom]", data: {autocomplete_source: auteurs_enum_path} %>
<% end %>
<%= f.label t('titre').capitalize %>
<%= f.text_field :titre %>
These are the params that are sent to the method nouveau:
Started POST "/administration/biblios/nouveau" for ::1 at 2017-02-07 21:28:28 +0100
Processing by Administration::BibliosController#ajouter as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"+354h4M0Tg+BX21XAuQ6YMKS0BGQ8UjET8paKjkGCBsS1up1lB131KsoaCy563X4juDz0EJy46WgXbHcu51Kgw==", "biblio"=>{"auteurs"=>{"nom"=>"Godding"}, "titre"=>"Test Tuesday Evening", "soustitre"=>"", "recueil"=>"", "editeur"=>"", "annee"=>"", "isbn"=>"", "genre"=>"source", "revue_id"=>"", "revue_no"=>"", "revue_page"=>"", "lieu"=>"", "commentaire"=>""}, "commit"=>"Enregistrer"}
(0.2ms) BEGIN
(0.1ms) ROLLBACK
Rendering administration/biblios/nouveau.html.erb within layouts/messources
CONTROLLER AGAIN
def params_biblio
params.require(:biblio).permit(
:titre,
:soustitre,
:editeur,
:isbn,
:recueil,
:genre,
:revue_id,
:revue_no,
:revue_page,
:annee,
:lieu,
:commentaire,
auteurs: [:nom] )
end
For the sake of completeness, here's my routes.rb:
# ADMINISTRATION => BIBLIOGRAPHIE
get 'biblios/nouveau(/:id)' => 'biblios#nouveau', as: 'nouvelle_biblio'
post 'biblios/nouveau(/:id)' => 'biblios#ajouter', as: 'ajoute_biblio'
delete 'biblios/supprime/:id' => 'biblios#supprime', as: 'supprime_biblio'
get 'biblios/maj/:id' => 'biblios#cherche_maj', as: 'maj_biblio'
patch 'biblios/maj/:id' => 'biblios#maj', as: 'patch_maj_biblio'
I must be blind. I'm doing something wrong... I put a title to this bibliography ('Test Tuesday Evening'), this is the only field on which I left a validation, and despite this, validation always fails.
I am using Cocoon gem to do nested forms.
I have models like that:
# request.rb
has_many :filled_cartridges, inverse_of: :request, dependent: :destroy
accepts_nested_attributes_for :filled_cartridges, :reject_if => :all_blank, allow_destroy: true
#filled_cartridge.rb
belongs_to :request
Inside of my form_for #request i have nested form like that:
<div id="filled_cartridges">
<%= f.fields_for :filled_cartridges do |filled_cartridge| %>
<%= render 'filled_cartridge_fields', f: filled_cartridge %>
<% end %>
<div class="links">
<%= link_to_add_association 'add', f, :filled_cartridges %>
</div>
Where filled_cartridge_fields partial is like that:
<fieldset>
<%= f.text_field :cartridge_id %>
<%= f.hidden_field :_destroy %>
<%= link_to_remove_association "remove", f %>
</fieldset>
When i click on "add" it adds one more . When clicking on "remove" it removes that .
When i submit form the params for nested form look like that:
filled_cartridges_attributes: !ruby/hash:ActionController::Parameters
'0': !ruby/hash:ActionController::Parameters
cartridge_id: '12'
_destroy: 'false'
'1429260587813': !ruby/hash:ActionController::Parameters
cartridge_id: '2'
_destroy: 'false'
How do i access these params, and how to save them. How to traverse over these params and save them, or do Cocoon gem has some built in functionality? And finally how to check if these params are set? Since it is nested, it tricks me.
EDIT: My request_controllers#create:
def create
#request = Request.new( request_params )
# code for handling Request model
# here i want to handle nested model too (filled_cartridge)
#request.save
if #request.save
flash[:success] = "Заявка была добавлена"
redirect_to #request
else
render 'new'
end
end
EDIT2: my strong params:
def request_params
params.require(:request).permit(:name, :address, :phone, :mobile, :type, :description, :priority, :responsible, :price, :payed, :date, filled_cartridges_attributes: [:cartridge_id, :_destroy], :stype_ids => [], :social_media =>[])
end
In a recent project using cocoon I had to access the params of the attributes about to be saved. I figured a code in my create action in my controller. The trick is to understand how to retrieve the key of the hash of the attribute that is about to be saved. The key of the hash is that number '1429260587813' that is in your params
...
'1429260587813': !ruby/hash:ActionController::Parameters
cartridge_id: '2'
_destroy: 'false'
So you need to create a loop in your create action to retrieve this key using ruby hash method "keys". I do a loop because when using cocoon dynamic nested field I might create more than one nested attributes at once so it means more than one key to retrieve.
Here is a the code that worked for me, read my comments which explains the different steps of this code. I hope it will help you to adapt it to your needs.
#Here I just initialize an empty array for later use
info_arr = []
#First I check that the targeted params exist (I had this need for my app)
if not params[:recipe]["informations_attributes"].nil?
#z variable will tell me how many attributes are to be saved
z = params[:recipe]["informations_attributes"].keys.count
x = 0
#Initiate loop to go through each of the attribute to be saved
while x < z
#Get the key (remember the number from above) of the first hash (params) attribute
key = params[:recipe]["informations_attributes"].keys[x]
#use that key to get the content of the attribtue
value = params[:recipe]["informations_attributes"][key]
#push the content to an array (I had to do this for my project)
info_arr.push(value)
#Through the loop you can perform actions to each single attribute
#In my case, for each attributes I creates a new information association with recipe
#recipe.informations.new(title: info_arr[x]["title"]).save
x = x +1
end
end
This work to access cocoon nested attribute content and apply actions based on your need. This worked for me so you should be able to use this sample code and adapt it to your need.
Issue: Instead of updating nested attributes, they are being created on top of the existing nested attributes when I hit the #update action of the associated features_controller.rb
Likely Cause: I think the problem lies in my lack of understanding in Rails' form_for. I think the breakdown is in my views, how I render the persisting nested attributes, and/or how I fail to specify the nested attribute's id, causing it to simply create a new one
feature.rb
class Feature < ActiveRecord::Base
...
has_many :scenarios
accepts_nested_attributes_for :scenarios,
allow_destroy: true,
reject_if: :all_blank
...
end
features_controller.rb
def update
...
project = Project.find(params[:project_id])
#feature = Feature.find(params[:id])
if #feature.update_attributes(feature_params)
# checking feature_params looks good...
# feature_params['scenarios'] => { <correct object hash> }
redirect_to project
else
render :edit
end
end
...
private
def feature_params
params.require(:feature).permit(:title, :narrative, :price, :eta, scenarios_attributes[:description, :_destroy])
end
_form.html.haml (simplified)
= form_for [#project, #feature] do |f|
...
- if #feature.new_record? -# if we are creating new feature
= f.fields_for :scenarios, #feature.scenarios.build do |builder|
= builder.label :description, "Scenario"
= builder.text_area :description, rows: "3", autocomplete: "off"
- else -# if we are editing an existing feature
= f.fields_for :scenarios do |builder|
= builder.label :description, "Scenario"
= builder.text_area :description, rows: "3", autocomplete: "off"
I'm sure there's a nicer way to achieve the if #feature.new_record? check. I'm also using a few Javascript hooks to create dynamic nested attribute forms (which I've left out), heavily influenced by Railscast #196 Nested Model Form (revised)
I would love a really nice Rails-y implementation of dealing with these sorts of nested forms.
Try adding :id to the :scenario_attributes portion of your feature_params method. You only have the description field and the ability to allow a destroy.
def feature_params
# added => before nested attributes
params.require(:feature).permit(:id, :title, :narrative, :price, :eta, scenarios_attributes => [:id, :description, :_destroy])
end
As #vinodadhikary suggested, you no longer need to check if feature is a new record, since Rails, specifically using the form_for method, will do that for you.
Update:
You don't need to define if #feature.new_record? ... else in your form. It will be taken care by Rails when you use form_for. Rails checks if the action is going to be create or update based on object.persisted?, so, you can update your form to:
= form_for [#project, #feature] do |f|
...
= f.fields_for :scenarios, #feature.scenarios.build do |builder|
= builder.label :description, "Scenario"
= builder.text_area :description, rows: "3", autocomplete: "off"
As #Philip7899 mentioned as a comment in the accepted answer, allowing the user to set the id means that they could "steal" children records belonging to another user.
However, Rails accepts_nested_attributes_for actually checks the id and raises:
ActiveRecord::RecordNotFound:
Couldn't find Answer with ID=5 for Questionnaire with ID=5
Basically the ids are looked for in the children association (again, as said by #glampr). Therefor, the child record belonging to another user is not found.
Ultimately, 401 is the response status (unlike the usual 404 from ActiveRecord::RecordNotFound)
Follows some code I used to test the behaviour.
let :params do
{
id: questionnaire.id,
questionnaire: {
participation_id: participation.id,
answers_attributes: answers_attributes
}
}
end
let :evil_params do
params.tap do |params|
params[:questionnaire][:answers_attributes]['0']['id'] = another_participant_s_answer.id.to_s
end
end
it "doesn't mess with other people's answers" do
old_value = another_participant_s_answer.value
put :update, evil_params
expect(another_participant_s_answer.reload.value).to eq(old_value) # pass
expect(response.status).to eq(401) # pass
end
In conclusion, adding the id to the permitted params as stated above is correct and safe.
Fascinating Rails.
I have a company model which can have many tags. It works fine, but in one occasion it does not work. The occasion is when company model validation fails. After :render => 'edit' it does not show tags in the view. I suspect the data-pre is not taking the data correctly. I would also like for tags to be preserved when solving validations.
I got this idea from here: http://railscasts.com/episodes/167-more-on-virtual-attributes
I use Input token control: http://loopj.com/jquery-tokeninput/
This is what I have in Company model regarding the tag_tokens:
before_save :save_tag_tokens
attr_writer :tag_tokens
attr_accessible :tag_tokens
def tag_tokens
#tag_tokens || tags.to_json(:only => [:id, :name])
end
def save_tag_tokens
if #tag_tokens
#tag_tokens.gsub!(/CREATE_(.+?)_END/) do
Tag.create!(:name => $1.strip.downcase).id
end
self.tag_ids = #tag_tokens.split(",")
end
end
Here is the code from the view:
<div class="input text no-border">
<% Tag.include_root_in_json = false %>
<%= company_form.label :tag_tokens, t('form.account.company.edit.company_tags_html')%>
<%= company_form.text_field :tag_tokens, :id => 'company_tag_tokens', "data-pre" => #company.tag_tokens%>
<p class="tip"><%= t('form.account.company.edit.tag_tip') %></p>
</div>
EDIT:
OK, so I see what is the problem with the above code.
When i load edit page data-pre contains this: data-pre="[{"id":1704,"name":"dump truck"}]". when I submit the form with validation error the data-pre contains: data-pre="1704".
if i change the code to this:
def tag_tokens
tags.to_json(:only => [:id, :name])
end
new tags that were not yet save to the company model are removed, because they are read from the DB everytime. How can I preserve the entered data between form transitions?
OK, I've written a solution, it might not be the nicest one, but it works to me! It parses the input token value to JSON format (when validation fails), which is used when loading the page. Under page load it just loads tags from DB.
def tag_tokens
if #tag_tokens
#if there is user info, parse it to json format. create an array
array = #tag_tokens.split(",")
tokens_json = []
#loop through each tag and check if it's new or existing
array.each do |tag|
if tag.to_s.match(/^CREATE_/)
#if new generate json part like this:
tag.gsub!(/CREATE_(.+?)_END/) do
tokens_json << "{\"id\":\"CREATE_#{$1.strip.downcase}_END\",\"name\":\"Add: #{$1.strip.downcase}\"}"
end
else
#if tag is already in db, generate json part like this:
tokens_json << "{\"id\":#{tag},\"name\":\"#{Tag.find_by_id(tag).name}\"}"
end
end
#encapsulate the value for token input with [] and add all tags from array
"[#{tokens_json.to_sentence(:last_word_connector => ',', :words_connector => ',', :two_words_connector => ',')}]"
else
#if there is no user input already load from DB
tags.to_json(:only => [:id, :name])
end
end