accepts_nested_attributes_for and nested_form plugin - ruby-on-rails

I've the following code in a _form.html.haml partial, it's used for new and edit actions.
(fyi I use the Ryan Bates' plugin nested_form)
.fields
- f.fields_for :transportations do |builder|
= builder.collection_select :person_id, #people, :id, :name, {:multiple => true}
= builder.link_to_remove 'effacer'
= f.link_to_add "ajouter", :transportations
works fine for the new action...
for the edit action, as explain in the doc, I've to add the :id of already existing associations, so, I've to add something like
= builder.hidden_field :id, ?the value? if ?.new_record?
How can I get the value?
Here is the doc of accepts_nested_attributes_for for reference (source: http://github.com/rails/rails/blob/master/activerecord/lib/active_record/nested_attributes.rb#L332)
# Assigns the given attributes to the collection association.
#
# Hashes with an <tt>:id</tt> value matching an existing associated record
# will update that record. Hashes without an <tt>:id</tt> value will build
# a new record for the association. Hashes with a matching <tt>:id</tt>
# value and a <tt>:_destroy</tt> key set to a truthy value will mark the
# matched record for destruction.
#
# For example:
#
# assign_nested_attributes_for_collection_association(:people, {
# '1' => { :id => '1', :name => 'Peter' },
# '2' => { :name => 'John' },
# '3' => { :id => '2', :_destroy => true }
# })
#
# Will update the name of the Person with ID 1, build a new associated
# person with the name `John', and mark the associatied Person with ID 2
# for destruction.
#
# Also accepts an Array of attribute hashes:
#
# assign_nested_attributes_for_collection_association(:people, [
# { :id => '1', :name => 'Peter' },
# { :name => 'John' },
# { :id => '2', :_destroy => true }
# ])
Thanks for your help.

I found my error, here is what i learned fyi:
When you use accepts_nested_attributes_for with many to many associations, keep the :id primary key for the association table.
Cheers

Mine works when using ":_delete" instead of ":_destroy". I am on rails 2.3.4. Ruby 1.8.7
Check out this: http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#M001605

Nested forms are officially supported with Rails. What you are doing (specifically with the fields_for method) may be conflicting with RAils' built-in way to render fields_for.
Here's the documentation for the Rails way to do fields_for...it's very thorough:
http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#M001605
I highly recommend you try the built-in way instead of the plugin, as that will continue to be supported almost indefinitely.
Hope this helps!

Related

Rails. Has_many :through and form_for params for a checkbox field

So I have this kind of association:
class FirstModel
has_many :merged_models
has_many :second_models, :through => :merged_models
end
class SecondModel
has_many :merged_models
has_many :first_models, :through => :merged_models
end
class MergedModel
belongs_to :first_model
belongs_to :second_model
end
Now my problem is to understand this trick that helps check_box_tag helper to recognise elements in HTML from a passed collection in my form:
form_for(first_model) do |f|
<% SecondModel.all.each do |s| -%>
<div>
<%= check_box_tag 'second_model_ids[]', s.id, first_model.second_models.include?(s), :name => 'first_model[second_model_ids][]'-%>
<%= label_tag :second_model_ids, s.first_name -%>
</div>
<% end -%>
What I do not understand is this:
first_model.second_models.include?(s), :name => 'first_model[second_model_ids][]'
I believe that this:
first_model.second_models.include?(s)
checks if SecondModel's object id is already in FirstModel's second_model_ids array. In this case I would expect something like an if statement - if this id is there then do that, etc.
And this part makes me even more confused:
:name => 'first_model[second_model_ids][]'
Where that :name came from? Why first_model[second_model_ids][] have two square brackets - how they work in Rails syntax? To merge this newly checked id to the second_model_ids array?
I will appreciate all info. Thanks!
So check_box_tag has this signature:
check_box_tag(name, value = "1", checked = false, options = {})
In your case:
check_box_tag 'second_model_ids[]', s.id, first_model.second_models.include?(s), :name => 'first_model[second_model_ids][]'
The first parameter (name) is 'second_model_ids[]', this will be used as the id= part of the tag.
The second parameter (value) of the checkbox is the id of s (current instance of SecondModel).
The third parameter (checked) is:
first_model.second_models.include?(s)
You are right about the meaning, and you don't need an 'if'. The include?() returns a boolean (like most Ruby methods that end in a question mark). You can try this in irb or rails console:
[1,2,3].include?(2)
# => true
The final option:
:name => 'first_model[second_model_ids][]'
passes in a hash of options which will be used as html. In this case a single hash value with the key :name (not to be confused with the first parameter above, which was used as the id='...' in the html tag), this will be used directly in the tag as
name='first_model[second_model_ids][]'
You were right about the syntax here also. The brackets help Rails parse this into the correct nesting of the params hash with
first_model: {foo: 1, bar: 2, second_model: {some: stuff, other: stuff}}

Rails 3 best_in_place Edit Nested Model

Given
<% #incidents.each_with_index do |incident,i| %>
I can't figure out how to in place edit attributes on incident and parent associations such as incident.user or incident.contact
This works for example:
best_in_place incident, :notes, type: :input, nil: 'Add Note'
But I can't figure out how to do incident.customer to get a drop down of Customer.all (incident belongs_to :customer)
I get various errors each way I try it.
If I understand you correctly, in your controller's show action, or wherever's relevant:
#customer = Customer.all.map { |c| [c.id, c.customer_name] } # or whatever the customer name attribute is
In your view:
= best_in_place incident, :notes, :type => :select, :collection => #customer
This produces the [[a,b], [c,d]] format that the docs say is needed.
It would be less wordy with Customer.pluck(:id, :name) but that's only in Edge Rails at the time of writing (link to guides).

Active Admin - refresh second drop down based on first drop down, Ruby on Rails

I am using Active Admin Gem on Ruby on Rails. I have a form in which i have selected category and sub category and then accordingly i have to fill the data. So i created two tables in sqlite added in active admin resouce.
Every thing is working fine but the drop down of sub category is not getting filtered based on the category choosen.
I am new to Ruby and RoR too. I don't know how to refresh dropdown of the subcategory after selecting category.
I know i can do it from AJAX and javascript but i dont know where to code for that?
Also, is there any specific filter avaliable in Active Admin which will make it happen without ajax or javascript.
Any ideas or help will be highly appreciated.
i don't know if there is any specific filter avaliable in Active Admin, but i solved it in this 3-steps way (assuming category - is a house, subcategory - is a flat):
1-st step: define helper containing ajax request
(of course, you have to predefine path in routes.rb)
#application_helper.rb
def remote_request(type, path, params={}, target_tag_id)
"$.#{type}('#{path}',
{#{params.collect { |p| "#{p[0]}: #{p[1]}" }.join(", ")}},
function(data) {$('##{target_tag_id}').html(data);}
);"
end
2-nd step: add this method for :onchange action
#admin/inhabitants.rb (DSL with formtastic)
form do |f|
f.inputs do
#...
f.input :house, :input_html => {
:onchange => remote_request(:post, :change_flats, {:house_id=>"$('#house_id').val()"}, :flat_id)
}
f.input :flat
#...
end
end
3-rd step: render result of filtering
(you can render partial instead of :text, I decided leave it in one activeadmin resource file )
controller do
def change_flats
#flats = House.find_by_id(params[:house_id]).try(:flats)
render :text=>view_context.options_from_collection_for_select(#flats, :id, :flat_number)
end
end
I accomplished this as any non-rails developer working on a rails project would - quick and dirty. Here's how:
#...
f.input :user, :input_html => {
:onchange => "
var user = $(this).val();
$('#order_location_id').val(0).find('option').each(function(){
var $option = $(this),
isCorrectUser = ($option.attr('data-user') === user);
$option.prop('disabled',!isCorrectUser);
});
"
}
f.input :location, collection: Location.all.map{ |loc|
[loc.name,loc.id, {"data-user" => loc.user_id}]
}
#...
No AJAX required. Note that this does not remove the unwanted options, it just disables them (sufficient for my scenario). This could easily be made modular with a helper, but I really only needed the functionality once.
For anyone else wrestling with the same problem, look at this railscast
I faced the same problem here
here's how I implemented multiple dynamic select menus in activeadmin:
config/initializers/active_admin.rb
config.register_javascript 'exam_registrations.js.coffee'
app/admin/exam_registrations.rb
form do |f|
f.inputs "Exam Registration Details" do
f.input :user_id, :label => 'Teacher', :as => :select, :collection => User.where(:admin => 'false', :active => true).order(:name), :include_blank => true
f.input :student_id, :hint => 'Students grouped by teacher names', :as => :select, :collection => option_groups_from_collection_for_select(User.where(:admin => false, :active => true).order(:name), :students, :name, :id, :name)
f.input :lesson_id, :hint => 'Lessons grouped by student names', :as => :select, :collection => option_groups_from_collection_for_select(Student.where(:active => true).order(:name), :lessons, :name, :id, :name)
end
f.buttons
end
app/assets/javascripts/exam_registrations.js.coffee
#first menu
jQuery ->
$('#exam_registration_student_id').parent().hide()
students = $('#exam_registration_student_id').html()
$('#exam_registration_user_id').change ->
user = $('#exam_registration_user_id :selected').text()
escaped_user = user.replace(/([ #;&,.+*~\':"!^$[\]()=>|\/#])/g, '\\$1')
options = $(students).filter("optgroup[label='#{escaped_user}']").html()
if options
$('#exam_registration_student_id').html(options)
$('#exam_registration_student_id').parent().show()
else
$('#exam_registration_student_id').empty()
$('#exam_registration_lesson_id').empty()
# second menu
$('#exam_registration_lesson_id').parent().hide()
lessons = $('#exam_registration_lesson_id').html()
$('#exam_registration_student_id').click ->
student = $('#exam_registration_student_id :selected').text()
escaped_student = student.replace(/([ #;&,.+*~\':"!^$[\]()=>|\/#])/g, '\\$1')
options = $(lessons).filter("optgroup[label='#{escaped_student}']").html()
if options
$('#exam_registration_lesson_id').html(options)
$('#exam_registration_lesson_id').parent().show()
else
$('#exam_registration_lesson_id').empty()
restart the server and the menus work!
Now it's possible with this gem https://github.com/holyketzer/activeadmin-ajax_filter, use in you form code like this:
f.input :category_id, as: :select # ...
f.input :subcategory_id, as: :ajax_select, data: {
ajax_search_fields: [:category_id],
search_fields: [:subcategory_atrribute],
url: '/admin/subcategories/filter'
}
And in you subcategory resource page:
ActiveAdmin.register Subcategory do
include ActiveAdmin::AjaxFilter
# ...
end
don't forget to include assets
You can also use activeadmin_addons gem Nested Select
dependent-select could be also a good option

Rails - Get 3 ID's in a form

I have a group#view page, that is accessed by a Person. In this page, the Person can see the members of the group via methods I developed. The problem is that I need to create the model Honors using the Id from the group, the id from the person accessing the page, and the id from a member of this group.
In my Honors controller I have:
def create
#person = Person.find(current_person)
#honor = Honor.create(:group => Group.find(params[:group_id]),
:person => Person.find(current_person), :honored => Person.find(current_person))
if #honor.save
...
end
The problem is in this :honored => Person.find(current_person), that is not getting the right ID and I don`t know how to get it.
In my view:
<% #asked_groupmembership.each do |agm| %>
<% form_for(:honor, :url => honors_path(:group_id => #group.id, :person => current_person.id,:honor => agm.member.id)) do |f| %>
Any help?
Thanks.
If you need 3 components to properly create an honor record, you need to pass them from the form. You seem to be doing that part correctly.
:group_id => #group.id
:person => current_person.id
:honor => agm.member.id
To create the record, access the passed variables.
Honor.create(:group => Group.find(params[:group_id]),
:person => Person.find(params[:person]),
:honored => Person.find(params[:honor]))
Understanding the above isn't the most efficient, but used for demonstrative purposes. You'd likely want to avoid redundant database hits, e.g. :person => current_person rather than another query

multiparameter error with datetime_select

I have the following code in my form.
<%= f.datetime_select(:date_time, :prompt => {:day => 'Day', :month => 'Month', :year => 'Year'}, :start_year => Date.today.year, :end_year => Date.today.year + 2, :minute_step => 15, :include_blank => false) %> if either one is blank.
When one of the fields is left blank, I get:
1 error(s) on assignment of multiparameter attributes
The params that are being passed are:
{"utf8"=>"✓",
"authenticity_token"=>"kQpfsj5RxnDtxkvBdwPEFnX1fY6euKnMQeDRAkvJvIE=",
"event"=>{"description"=>"",
"venue"=>"",
"street"=>"",
"city"=>"",
"country_id"=>"",
"date_time(1i)"=>"",
"date_time(2i)"=>"",
"date_time(3i)"=>"",
"date_time(4i)"=>"00",
"date_time(5i)"=>"00",
"ticket_url"=>""},
"x"=>"94",
"y"=>"12"}
Anyone know why this is occurring?
There seems to be a "dirty" fix for this at this link, but perhaps there is a better solution in Rails 3?
Christian. This is a bug in Rails that checks the database to infer the type needed for the multiparameter attributes. My guess is that your "date_time" attribute is not associated with a time column in your database.
I recently tackled this problem where I wanted a non-database attribute to accepted multiparameter attributes, this was the best solution I could come up with:
I found myself wanting to set an attr_accessor to handle passing a date to my model in a form_for tag with the f.datetime_select helper. So this is what I had:
Model:
attr_accessor :my_time
View:
<%= f.datetime_select :my_time %>
Unfortunately when I submit my form I get this:
1 error(s) on assignment of multiparameter attributes
Well it turns out that this is actually a Rails bug a ticket for which has been submitted. In the meantime how do we make this work? The only solution I could find that was remotely attractive was to make use of composed_of as a replacement for attr_accessor. so...
Model:
composed_of :my_time,
:class_name => 'Time',
:mapping => %w(Time to_s),
:constructor => Proc.new{ |item| item },
:converter => Proc.new{ |item| item }
I know almost nothing about the composed_of method so you should probably do your own reading on it, but what I do know is that it creates both a reader and writer for the given instance variable, and more importantly, the setter accepts multiparameter attributes. How I chose the options:
class_name: the name of our expected class. In this case, Time
mapping: the first argument is the class and the second argument seems to work with any method that an instance of the class responds to. I chose to_s
constructor: Not really sure how this is supposed to work. Seems to be called when #my_time is nil.
converter: Not really sure how this is supposed to work. Seems to be called when from my_time=, but doesn't seem to be applied with mass assignment.
One problem I ran into with this solution was that times were getting set in UTC instead of the environment's time zone. So unfortunately we cannot use my_time directly, but instead need to convert it to the proper time zone:
Time.zone.parse(my_time.to_s(:number))
What Does ActiveRecord::MultiparameterAssignmentErrors Mean?
def initialize(attributes={})
date_hack(attributes, "deliver_date")
super(attributes)
end
def date_hack(attributes, property)
keys, values = [], []
attributes.each_key {|k| keys << k if k =~ /#{property}/ }.sort
keys.each { |k| values << attributes[k]; attributes.delete(k); }
attributes[property] = values.join("-")
end
I had the same problem using a date dropdown that wasn't backed by a database attribute. I wrote a little Rack middleware to cope with the problem:
class DateParamsParser
def initialize(app)
#app = app
end
def call(env)
if %w{POST PUT}.include? env['REQUEST_METHOD']
params = Rack::Utils.parse_query(env["rack.input"].read, "&")
# selects only relevant params like 'date1(1i)'
filtered_params = params.select{ |key, value| key =~ /\(\di\)/ }
# delete date params
filtered_params.each { |key, value| params.delete(key) }
# returns something like {'date1' => [2012, 5, 14], 'date2' => [2002, 3, 28]}
date_array_params = filtered_params.sort.reduce({}) do |array_params, keyvalue|
date_key = keyvalue.first.match(/(.+)\(/)[1] + ']'
array_params[date_key] ||= []
array_params[date_key] << keyvalue.last
array_params
end
# Creates params with date strings like {'date1' => '2012-5-14', 'date2' => '2002-3-28'}
date_params = Hash[date_array_params.map{ |key, date_array| [key, date_array.join('-')] }]
params.merge! date_params
env["rack.input"] = StringIO.new(Rack::Utils.build_query(params))
env["rack.input"].rewind
end
#app.call(env)
end
end
And in application.rb I put
config.middleware.insert_before ActionDispatch::ParamsParser, "DateParamsParser"
Note that I only build a date string here. So if you also require time you'll need to build the date_params differently.
I faced the same problem with the model below
class Reservation < ActiveRecord::Base
attr_accessor :sid, :check_in, :credit_card_number, :expiration_date
attr_accessible :expiration_date
end
The corresponding form with the field for the expiration date:
<div class="field">
<%= f.label :expiration_date %>
<%= f.date_select(:expiration_date, start_year: Time.now.year + 3, :end_year => Time.now.year - 3, discard_day: true) %>
</div>
as mentioned by #gabeodess the problem is checking the database to infer the type accordingly the solution I did for it was adding the following code to the model to put the type of the needed attribute in this case :expiration_date so the model is modified to be the following
class Reservation < ActiveRecord::Base
attr_accessor :sid, :check_in, :credit_card_number, :expiration_date
attr_accessible :expiration_date
columns_hash["expiration_date"] = ActiveRecord::ConnectionAdapters::Column.new("expiration_date", nil, "date")
end
Hope this is useful
Remove :include_blank => false from your code.
<%= f.datetime_select(:date_time, :prompt => {:day => 'Day', :month => 'Month', :year => 'Year'}, :start_year => Date.today.year, :end_year => Date.today.year + 2, :minute_step => 15 %>
Thanks....
I was facing the same problem.
I just added attr_accessible for that attribute and it works fine.
Hope it helps.

Resources