Nested Attributes not being saved - ruby-on-rails

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.

Related

Rails - Issue with permitting params & require

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

Rails - how to change <select> to be just a hidden field when called with an id?

I have a form with a <select> element for the Group.
[The application stores bookmark links and groups for them]
When doing a 'new', everything work correctly. The new form works, the <select> dropdown has the list of groups and has their ID's.
The problem now is that I want to call the 'new link' from a different place that will already know the group. In this case I don't want an option to select the group, I just want to use the group id passed to it.
The form itself has:
= f.select :group_id, #groups
My routes include:
resources :groups do
resources :links # Added so that I can do group/:id/link/new ...
collection do
post 'order_links'
end
end
match 'search' => 'links#index'
match 'advanced_search' => 'links#advanced_search'
resources :links do
collection do
get 'groups'
end
end
The controller that's showing the form for this "new link" has:
def new
#link = Link.new
#groups = Group.all.collect { |g| [g.group_name, g.id] }
#group_name =
if params[:group_id]
'for the '+Group.find(params[:group_id]).group_name + ' group.'
else
''
end
respond_to do |format|
format.html
end
end
I tried changing the view to have this:
-if params[:group_id]
= f.hidden_field :group_id, :value => params[:group_id]
-else
= f.select :group_id, #groups
but it didn't work, I still got the <select> element, defaulting to its first <option>.
I actually had this working as some point in the past but has broken since, so hopefully I'm fairly close.
The indentation in your HAML template is wrong. It should be:
- if params[:group_id]
= f.hidden_field :group_id, :value => params[:group_id]
- else
= f.select :group_id, #groups
(If that was just the way it came out when you entered the question, I will delete this answer.)

Run All Validations Except Any of "kind" :presence

Is there a way to run only validations of a specific type?
I have an application that updates multiple class instances in a single form. Validations are performed by creating an instance of building and validating on that.
The problem: if an attribute isn't being updated, the form field is left blank and the form submits an empty string. You can see an example here where params[:building][:name] is an empty string.
params = {:building=>{:name=>"", :short_name=>"", :code=>"test"}, :commit=>"Update Buildings", :building_ids=>["2", "5", "7"], :action=>"update_multiple", :controller=>"buildings"}
How can I run all the validations excluding those that check for the presence of an attribute?
def update_multiple
#building = Building.new(params[:building].reject {|k,v| v.blank?})
respond_to do |format|
if #building.valid?
Building.update_all( params[:building].reject {|k,v| v.blank?}, {:id => params[:building_ids]} )
format.html { redirect_to buildings_path, notice: 'Buildings successfully updated.' }
else
#buildings = Building.all
format.html { render action: 'edit_multiple' }
end
end
end
I've spent quite a bit of time working on this, and here's what I've found so far:
To retrieve a models validations
$ Building.validators
=> [#<ActiveModel::Validations::PresenceValidator:0x007fbdf4d6f0b0 #attributes=[:name], #options={}>]
To get a validators kind
$ Building.validators[0].kind
=> :presence
This is the method used by rails to run validations:
https://github.com/rails/rails/blob/master/activesupport/lib/active_support/callbacks.rb
line 353
# This method runs callback chain for the given kind.
# If this called first time it creates a new callback method for the kind.
# This generated method plays caching role.
#
def __run_callbacks(kind, object, &blk) #:nodoc:
name = __callback_runner_name(kind)
unless object.respond_to?(name, true)
str = object.send("_#{kind}_callbacks").compile
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{name}() #{str} end
protected :#{name}
RUBY_EVAL
end
object.send(name, &blk)
end
If there is a way to run validations directly? If so, I could iterate over the Building.validators and only run those with kind != :presence.
I'd love to hear any ideas you have.
I think circumventing specific validations is an interesting idea, but I think there's an easier way. I'd handle this by writing a custom method that validates batch updates, something like this:
def valid_for_batch?(params)
#building = Building.new(params[:building].reject {|k,v| v.blank?})
#building.name = "Foo" if #building.name.blank?
#building.shortname = "Bar" if #building.shortname.blank?
# etc...
#building.valid?
end
Just make sure that "Foo" and "Bar" up there are values that will pass your validation - all that code is doing is looking to see if the values are empty and if so, replacing them with a temporary value that will pass validation. That way, the only way that #building.valid? will return false at the end is if there were existing bad data.
Wow, after working on this issue for many hours, it looks to be a very difficult problem.
Create a class instance #building and set placeholders for attributes whose presence is validated.
I've tried many different approaches and this is the best I can come up with so far.
def update_multiple
valid = true
#building = Building.new(params[:building])
set_bulk_edit_placeholders_for_presence_validators(#building)
building_valid = #building.valid?
# Check if buildings were selected to edit
if params[:building_ids].blank?
valid = false
#building.errors.add(:base, 'You must select at least one Building')
end
# Check if all form fields are blank
if params[:building].values.delete_if {|v| v.blank?}.blank?
valid = false
#building.errors.add(:base, 'The edit form must not be empty')
end
respond_to do |format|
if valid && building_valid
#buildings = Building.find(params[:building_ids])
#buildings.each do |building|
building.update_attributes!(params[:building].reject { |k,v| v.blank? })
end
format.html { redirect_to buildings_path, notice: 'Buildings were successfully updated.' }
else
#buildings = Building.all
format.html { render edit_multiple_buildings_path }
end
end
end
This is a general function to set the placeholders. It can be used for any model from any controller.
application_controller.rb
private
def set_bulk_edit_placeholders_for_presence_validators(class_instance, hash={})
model_name = hash[:model_name] || self.class.name.sub("Controller", "").singularize.downcase
klass = self.class.name.sub("Controller", "").singularize.constantize
klass.send(:validators).each { |validator|
if (validator.kind == :presence)
validator.attributes.each { |attribute|
if params[model_name][attribute].blank?
class_instance.send("#{attribute}=", 'placeholder')
end
}
end
}
end
In order to display the errors properly on the form it has to be a form_for #buildings.
Since the placeholders are set for the #buildings object, we have to specify that the form values come directly from params.
edit_multiple.haml
= bootstrap_form_for #building, :url => update_multiple_buildings_path, :method => :put, :html => {:class => 'form-horizontal'} do |f|
= f.text_field :name, :value => params[:building].try(:send, :[], :name)
= f.text_field :short_name, :value => params[:building].try(:send, :[], :short_name)
= f.text_field :code, :value => params[:building].try(:send, :[], :code)
= f.submit 'Update Buildings', :name => nil
%table.table.table-striped.table-bordered
%thead
%tr
%th.check_box_column= check_box_tag 'select_all'
%th.sortable= sortable :name
%th Abbr Name
%th.sortable= sortable :code
%th
%tbody
- #buildings.each do |building|
%tr
%td.check_box_column= check_box_tag 'building_ids[]', building.id, (params[:building_ids].include?(building.id.to_s) if params[:building_ids])
%td= building.name
%td= building.short_name
%td= building.code
%td.links.span2
= link_to 'Show', building
= link_to 'Edit', edit_building_path(building)
= link_to 'Delete', building, :method => :delete, :confirm => 'Are you sure you want to delete this building?'
I hope this can be of help to others working on similar "bulk update" validation issues.

Ajax Posting via jQuery to custom controller method not working

I'm having an issue posting data to my custom controller methods via a jquery ajax post.
I have the following rails myPlayer.erb.html:
<% form_for #playersonline, :url => game_index_path, :html => { :method => :put, :class => "block", :id => "playersOnline_form" } do |f| %>
<% #playersonline.each do |member| %>
<li>
<div class="playerDetails">
<div class="playerNickname"><%= member.nickname %></div>
<div class="playerPlayLink">
<input type="button" class="submit_button playPlayer" id="<%= member.nickname %>" value="Play" />
</div>
</div>
</li>
<% end %>
<p class="morePlayers">
View more players >>
</p>
<% end %>
The forms html looks like this when rendered:
<form accept-charset="UTF-8" action="/game" class="block" id="playersOnline_form" method="post">
And the following jQuery code that attaches event handllers to each button.
SITE.Game = function() {
$('.playPlayer').live('click',
function() {
SITE.Game.sendGameRequest()
}
);
}
SITE.Game.sendGameRequest = function() {
$.post('/game/test,{"testData": "One", "testDataAgain": "Two"}, function(result) { console.log("data returned = " + result); }, "json")
}
So when I click a button, and the event fires making the ajax request I get the following error back:
Started POST "/game/test" for 127.0.0.1 at 2011-03-13 00:05:59 +0000
ActionController::RoutingError (No route matches "/game/test"):
My controller looks like this, and I can see from the server output that its not going into my test method:
class GameController < ApplicationController
def test
puts "---------------------------"
puts "GAME TEST WORKING"
puts "---------------------------"
end
def index
#returnMember = ["Tom","Pete"]
respond_to do |format|
format.json { render :json => #returnMember.to_json }
end
end
end
I have the following in my routes also:
resources :game
Any ideas where I'm going wrong?
EDIT
In response to nzifnab reply. I added:
resource :game do
member do
post 'test'
end
end
But am now getting:
ActionController::RoutingError (uninitialized constant GamesController):
EDIT 2
Fixed above by changing controller to GamesControllor from GameController.
Now getting the error:
undefined local variable or method `games_index_path'
<% form_for #playersonline, :url => games_index_path, :html => { :method => :put, :class => "block", :id => "playersOnline_form" } do |f| %
>
See my controller at beginning of issue.
EDIT 3
Fix for Edit 2 is quite simple! Changed url to games_path
resources :game sets up a RESTful route for your controller which automatically enables the show, create, new, edit, update, and destroy actions. If you want it to hit the test action you need to do this in your routes file:
resource :game do
member do
post :test
end
end
Or if you don't need all of those other RESTful routes you can do this:
match 'game/test' => 'game#test'
That's for rails 3. In rails 2.3.x the resourceful way would be like this:
resource :game, :members => {:test => :post}
I believe
Rails also expects your controllers to be named in the plural format. So a file in controllers/games_controller.rb should have the class GamesController < ApplicationController as it's class contents.
I would juse rename the file and/or controller to match the convention. But if you really need to keep it named singular for some reason you can add this to your routes file:
resource :game, :controller => 'game' do
# Other stuff here
end
I think that would fix it. But it's more usual for people to just pluralize their controller names.
It's because of your data type. It depends what kinda datatype you want but this is how you could do html and json:
JSON example:
SITE.Game.sendGameRequest = function() {
$.post('/game/test.json,{"testData": "One", "testDataAgain": "Two"}, function(result) {
console.log("data returned = " + result); }, "json")
}
And HTML:
SITE.Game.sendGameRequest = function() {
$.post('/game/test.html,{"testData": "One", "testDataAgain": "Two"}, function(result) { console.log("data returned = " + result); }, "html")
}
then in your controller you need to check the format:
class GameController < ApplicationController
def test
responds_to |format|
format.html #need for ajax with html datatype
format.json #need for ajax with json datatype
end
end
end
Change log:
[changed] the paths for your ajax request
[added] responds_to helper in your controller
And don't forget to declare the route in your routes.rb:
map.resources :games

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