Nested form using accepts_nested_attributes_for with pre-population from another table - ruby-on-rails

I'm using Rails 2.3.5 and have a nested structure as follows:
Lists has_many Items
Items_Features has_many Features
Items_Features has_many Items
Items_Features has a text field to hold the value of the feature
Then I have a nested form with partials to update and display this so that it updates Lists, Items and Items_Features
What I want to do is generate input fields for each of the rows in features so that the user can fill in a value and it gets inserted/updated in items_features. I also want a label next to the box to display the feature name.
It might look like this:
List name: Cheeses
Item1 name: Edam
Feature, hardness: - fill in - <= this list of features from feature table
Feature, smell: - fill in -
How can I interrupt the nice and easy accepts_nested_attributes_for system to display this as I want?
EDIT:
Here's the code for the Item class now i've got some sql in there:
class Item < ActiveRecord::Base
belongs_to :list
has_many :users
has_many :votes
has_many :items_features
validates_presence_of :name, :message => "can't be blank"
accepts_nested_attributes_for :items_features, :reject_if => lambda { |a| a.values.all?(&:blank?) }, :allow_destroy => true
def self.get_two_random(list_id)
Item.find_by_list_id(list_id, :limit => 2, :order => 'rand()')
end
def self.get_item_features(item_id)
sql = "select items_features.*, features.id as new_feature_id, features.name as feature_name "
sql += "from items_features "
sql += " right outer join features "
sql += " on features.id = items_features.feature_id "
sql += " left outer join items "
sql += " on items.id = items_features.item_id "
sql += "where items_features.item_id = ? or items_features.item_id is null"
find_by_sql([sql, item_id])
end
end
Here's my display code - I need to save the feature_id into new records somehow:
<% form_tag item_path, :method => :put do %>
<% for items_feature in #item_features %>
<% fields_for "items_features[]", items_feature do |f| %>
<%=h items_feature.feature_name %> <%= f.text_field :featurevalue %><br><Br>
<% end %>
<% end %>
<p><%= submit_tag "Submit"%></p>
<% end %>
Hmm, that isn't working - it prints out fine but then the server gives me:
/!\ FAILSAFE /!\ Thu Apr 15 16:44:59 +0100 2010
Status: 500 Internal Server Error
expected Array (got Hash) for param `items_features'
When I post it back

Try the examples here
http://github.com/ryanb/complex-form-examples

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 form with validation always failing

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.

Rails 4- Associations -Tutorial Movielist

Hi I am trying to use Rails on Rest 2 - movielist tutorial with rails 4 and making adjustments as I go for new rails. I am stuck on associations of Movies Roles. I have added to Movie.rb
class Movie < ActiveRecord::Base
has_many :roles, :dependent => :destroy
has_many :people, :through => :roles
validates_presence_of :title
def new_role=(values)
values.each do |i, hash|
unless hash[:name].blank?
roles.create(:person_id => hash[:person_id], :name => hash[:name])
roles.save
end
end
end
def deleted_roles=(values)
values.each do |role_id|
roles.find(role_id).destroy
end
end
end
and also to show _Form.html.rb that I render (excert below)
<b>Add New People</b><br />
<% (1..3).each do |i| %>
<%= select_tag 'movie_new_role_person_id', options_for_select(#people), {
:name => "movie[new_role][#{i}][person_id]"
} %>
<%= text_field_tag 'movie_new_role_name', '', {
:name => "movie[new_role][#{i}][name]"
} %><br />
<% end %>
</p>
<p>
<%= f.submit "Update" %>
</p>
It renders the list to choose from but when i submit - nothing is written to database table.
If I manually enter data in database then it displays on movielist page ie: "Stephen Spielberg - Director" etc...
Any help appreciated- Driving me nuts at this stage
I thought it might be params driven restriction but I do not have a good example of associations style params filter
Thanks
Alan
After debugging including Chicagogrrl's !flag I investigated the params.permits in the movies_Controller again and figured I would have to add the method types to the list of excepted. I could not find detailed info on syntax for this but trial and error paid off.
app/controllers/movies_controllers.rb (excerpt)
......
# Never trust parameters from the scary internet, only allow the white list through.
def movie_params
params.require(:movie).permit(:title,:description, :rating, :role, :deleted_roles=>[],
:new_role=> ['person_id', 'name'])
end
......
The delete_roles=>[] takes the array params and process to delete_roles method in movies.rb
the new_role=>['person_id', 'name'] takes the new_role individual params.
I Hope this saves somebody else some time andIf anybody needs anymore info just ask thanks again Alan

RecordNotFound with accepts_nested_attributes_for and belongs_to

I get
ActiveRecord::RecordNotFound: Couldn't find Client with ID=3 for Order with ID=
when trying to submit an Order form for an existing client. This happens through the form or the console by typing:
Order.new(:client_attributes => { :id => 3 })
payment_form.html.erb:
<%= semantic_form_for #order, :url => checkout_purchase_url(:secure => true) do |f| %>
<%= f.inputs "Personal Information" do %>
<%= f.semantic_fields_for :client do |ff| %>
<%= ff.input :first_name %>
<%= ff.input :last_name %>
<!-- looks like semantic_fields_for auto-inserts a hidden field for client ID -->
<% end %>
<% end %>
<% end %>
Order.rb:
class Order < ActiveRecord::Base
belongs_to :client
accepts_nested_attributes_for :client, :reject_if => :check_client
def check_client(client_attr)
if _client = Client.find(client_attr['id'])
self.client = _client
return true
else
return false
end
end
end
The reject_if idea came from here but I logged the method and it's not even being called! It doesn't matter what its name is!
Note: Feb 2020
Since I'm starting to get downvotes on this 8 years later, adding this note. While this was the original solution I went with 8 years ago, a better one has been proposed by MatayoshiMariano (5 years after my OP).
My Original Fix
Fixed the issue by overloading the client_attributes= method, as described here:
def client_attributes=(client_attrs)
self.client = Client.find_or_initialize_by_id(client_attrs.delete(:id))
self.client.attributes = client_attrs
end
If you only want a new Order with an existing client, without modifying the client, you need to assign the id.
Order.new(client_id: 3)
This is another way to do this without overloading the client_attributes= method and cleanest
The new Order now has the client with ID 3
If you also want to update ant client's attributes you must add the client_attributes, for example:
Order.new(client_id: 3, client_attributes: { id: 3, last_order_at: Time.current })
See https://github.com/rails/rails/issues/7256 from 2012.
If you have has_many relationship, this will work. Tested on Rails 6.0.2
def clients_attributes =(attributes)
# Get IDs for any clients that already exist.
client_ids = attributes.values.map { |a| a[:id] }.compact
# Now find them all and move them to this section.
clients << Client.find(client_ids)
# Update them with standard `accepts_nested_attributes_for` behaviour.
super attributes
end
Had the same error creating a new Thing for existing model with has_many and belongs_to relations.
Fixed it by adding a hidden field for the id of the existing model, for instance User, to the form.
= form.input :user_id, as: :hidden
Then new Thing was created without the error.

Modifying attributes on the join model with accepts_nested_attributes_for

Simply, a Contact can have various associated Time Windows, which may or may not be Active as a Schedule. To wit:
Models
class Contact < ActiveRecord::Base
has_many :schedules
has_many :time_windows, :through => :schedules
accepts_nested_attributes_for :schedules, :allow_destroy => true
end
class TimeWindow < ActiveRecord::Base
has_many :schedules
has_many :contacts, :through => :schedules
end
class Schedule < ActiveRecord::Base
belongs_to :contact
belongs_to :time_window
end
View
<% TimeWindow.all.each do |tw| %>
<% schedule = Schedule.find_by_contact_id_and_time_window_id(#contact.id, tw.id)
schedule ||= Schedule.new %>
<p>
<%= f.label tw.description %>
<%= hidden_field_tag "contact[schedules_attributes][][id]", schedule.id %>
<%= check_box_tag "contact[schedules_attributes][][time_window_id]",
tw.id, #contact.time_windows.include?(tw) %>
<%= check_box_tag "contact[schedules_attributes][][active]", nil,
schedule.active %>
</p>
<% end %>
This submits something like this:
Parameters: { "commit" => "Update", "contact" => {
"group_ids" => ["2"], "enabled" => "1",
"schedules_attributes" => [ { "time_window_id"=>"1", "id"=>"46"},
{ "time_window_id" => "2", "id" => "42", "active" => "on" },
{ "time_window_id" => "3", "id" => "43"},
{ "time_window_id" => "4", "id" => "44", "active" => "on"}],
"last_name" => ...
The update action in the controller is basically stock, except to handle another instance of another related model which I coded using the "Handling Multiple Models" example from the Advanced Rails Recipes book.
According to this API doc, I think the above ought to work. However, nothing about the Schedules is getting updated. This shows up in the server log:
[4;35;1mSchedule Update (0.2ms)[0m [0mUPDATE `schedules` SET `updated_at` = '2010-09-30 20:39:49', `active` = 0 WHERE `id` = 42[0m
[4;36;1mSchedule Update (0.1ms)[0m [0;1mUPDATE `schedules` SET `updated_at` = '2010-09-30 20:39:49', `active` = 0 WHERE `id` = 44[0m
(NetBeans is giving me those stupid "[0m"'s in the output. I don't know what's wrong there.)
The SQL shows that the "active" boolean field is getting set to 0 where checked. How do I get this to correctly set the active bit?
As a followup, how would I organize this to get rid of the Schedule "connection" at all? I'm thinking I need to submit a :_delete with the Schedule from the form, but how would I do that conditionally when a checkbox is involved?
Thanks for any help you can provide. Rails is turning out to be a vast subject for me, and I want to do it "right." I'm really close here, but there's got to be a way to make this -- not just correct -- but elegant. The view code just feels way too cumbersome to be proper Rails. ;-)
I've kept trying different approaches to this problem, and I've come up with this, which works. Mostly. The only problem is that it doesn't handle NOT having a "Schedule" for each "Time Window". The form will render, and I'll get a disabled check_box (to prevent me from trying to delete something that isn't there), but I don't have a way to add it back, and submitting without it throws off the params hash (and causes Rails to give me an "Expected Hash (got Array)" error)
<% TimeWindow.all.each do |tw| %>
<% schedule = Schedule.find_by_contact_id_and_time_window_id(#contact.id, tw.id)
schedule ||= Schedule.new %>
<% f.fields_for "schedules_attributes[]", schedule do |sf| %>
<p>
<%= sf.label tw.description %>
<%= sf.hidden_field :id %>
<%= sf.check_box :_destroy, :disabled => schedule.new_record? %>
<%= sf.check_box :active %>
</p>
<% end %>
<% end %>
Note that the "schedules_attributes[]" array will automatically give you an existing ID within the braces in your HTML (which is nice), but the _attributes hash is expecting an "id" alongside the other attributes in order to make sense of the sub-hashes.
One of the big lessons I've learned here is that the "check_box_tag" method doesn't (seem to) give me a paired-up hidden field for Rails to parse in the unchecked case. I would have expected this. Adding one in by hand made a mess, which led me to finally giving into the "fields_for" method, and trying many incarnations before finding the appropriate syntax to get what I wanted out of it.
I've realized that my model isn't quite appropriate in this setup, so I'm going to change it, but I was so close to this answer, I wanted to at least get to the point of being able to see the end before I moved on.

Resources