Think I've solved this in the process of writing it. I'll post my solution below.
tl;dr Rails fields_for not generating the expected params.
I have a User class, with the following code:
class User < ActiveRecord::Base
has_one :woojit, dependent: :destroy, validate: true
accepts_nested_attributes_for :woojit
# ... more
end
and a Woojit class
class Woojit < ActiveRecord::Base
belongs_to :user
end
Our UsersController includes this method (we're using the gem Administrate, but it's a subclass of Rails' ApplicationController):
class WoojitsController < Admin::ApplicationController
def new
# ... other stuff
#woojit = #user.build_woojit
end
end
And our user _form.html.erb partial looks like this:
<%= form_for([namespace, page.resource], html: { class: "form" }) do |f| %>
<!-- User fields -->
<fieldset>
<legend>Woojit fields</legend>
<%= f.fields_for #woojit do |ff| %>
<%= render 'woojit_fields', f: ff %>
<% end %>
</fieldset>
<% end %>
So when I submit the form, what I'm hoping to see is a params hash with this subset:
{
"user" => {
"woojit_attributes" => {
"attr_1"=>"Foo", # etc
}
}
}
But what I'm actually getting in the create action is a params hash with this subset:
{
"user" => {
"woojit" => {
"attr_1"=>"Foo", # etc
}
}
}
Ie 'woojit' instead of 'woojit_attributes'. I could hack this in the create action, but that seems like a horrible way of resolving the problem. I want to know why the key is getting mis-named to begin with.
I originally tried the alternative of using the line <%= f.fields_for :woojit do |ff| %> (ie symbol instead of instance var), and not building a Woojit object in the #new action.
The #fields_for guide suggests in the One-To-One section that this should work, but the code in the block was never executed, so the line returned nil - and the woojit params never made it to the create action in any form.
So the problem was apparently that #fields_for behaves differently when given an instance of a model (#woojit), rather than a symbol (:woojit).
In the former instance it generates fields that reference (model name), eg:
<input type="text" name="user[woojit][woojit_attr]" id="user_pledge_pledgor_name" />
In the latter instance it generates fields fields that reference the model attributes, eg:
<input type="text" name="user[woojit_attributes][woojit_attr]" id="user_pledge_attributes_pledgor_name" />
Hope that saves someone some of the frustration I've just been through!
Related
I have this code which I don't really understand:
app\controllers\look_controller.rb
class LookController < ApplicationController
def at
#data_hash = params[:cruncher]
#cruncher = Cruncher.new(#data_hash[:crunch])
#data = #cruncher.crunch
end
def input
end
end
app\models\cruncher.rb
class Cruncher
attr_reader :crunch
attr_writer :crunch
def initialize(data)
#crunch = data
end
end
app\views\look\input.rhtml:
<html>
<head>
<title>Using Text Fields</title>
</head>
<body>
<h1>Working With Text Fields</h1>
This Ruby on Rails application lets you read data from text fields.
<br>
<%= start_form_tag ({:action => “at”}, {:method => “post”}) %>
Please enter your name.
<br>
<%= text_field (“cruncher”, “crunch”, {“size” => 30}) %>
<br>
<br>
<input type=”submit”/>
<%= end_form_tag %>
</body>
</html>
I do not understand what is the relationship between <%= text_field (“cruncher”, “crunch”, {“size” => 30}) %> and the model. What do text_fields attributes cruncher and crunch have to do with the model?
As I understand the params is a special hash that stores the data from the user, and by using #data_hash = params[:hash] inside the controller we store that data.
But what about this #cruncher = Cruncher.new(#data_hash[:crunch]), why do we now use #data_hash[:crunch]?
Why not just #data_hash?
Thanks.
if you look at he html produced by the input view, you'll see something like this for the text field:
<input type="text" name="cruncher_crunch" value="cruncher[crunch]" size="30" />
this means that the params hash, created when the form is submitted, and sent to the LookController#at method will be formatted like this:
{cruncher: {crunch: 'somevalue'}}
which is exactly the format that the Cruncher.new(#data_hash[:cruncher]) expects.
Its not that strange that you don't understand it.
This code is probably ludicrously old (.rhtml and start_form_tag put it at Rails 1 or 2) and really bad, it does not even run as there are two syntax errors as well as the quotes that look like an artifact from pasting the code into MS Word
# don't put a space before parens when calling methods in Ruby!
text_field (“cruncher”, “crunch”, {“size” => 30})
It would also give NoMethodError on #data = #cruncher.crunch.
In Rails 5 the same example can be written as:
class Cruncher
include ActiveModel::Model
attr_accessor :crunch
def crunch
# have no idea what this was supposed to do
end
end
class LookController < ApplicationController
def at
#cruncher = Cruncher.new(cruncher_params)
#data = #cruncher.crunch
end
private
def cruncher_params
params.fetch(:cruncher).permit(:crunch)
end
end
# I really have no idea what the actual routes are supposed to be
<%= form_for(#cruncher, url: '/look/at') do %>
<%= f.text_field(:crunch size: 30) %>
<% end %>
Its still just a strange and non RESTful example though. Sometimes garbage code is best left buried.
I do not understand what is the relationship between <%= text_field
(“cruncher”, “crunch”, {“size” => 30}) %> and the model. What do
text_fields attributes cruncher and crunch have to do with the model?
Nothing. There is no data binding. Its just a plain-jane text input.
But what about this #cruncher = Cruncher.new(#data_hash[:crunch]), why
do we now use #data_hash[:crunch]?
Because the author didn't know what they where doing. And probably had not figured out that you can pass hashes to your methods.
ANSWER: The model calls were redundant. There should only be one and the last one in this example was the winner. I was misusing the Form Object DSL. :/
I've got a Reform Form object in a Rails 4.1 form that is structured like...
Form Object
class MyForm < Reform::Form
include Composition
model :user
model :user_group
property :name, on: :user_group
property :email, on: :user
end
Controller
# ...
#form = MyForm.new(user: User.new, user_group: UserGroup.new)
# ...
View
<%= form_for(#form) do |f| %>
<%= f.text_field(:name) %>
<%= f.email_field(:email) %>
<% end %>
Rendered HTML
<input type="text" name="user_group[name]" id="user_group_name">
<input type="email" name="user_group[email]" id="user_group_email">
My question is why are the fields seeming to ignore the model mappings and rendering them to the wrong model name? What am I doing incorrectly here?
You can call ::model only once! Once you call it with the correct model (or any name), the rendering will name the fields to whatever ::model you specify. The point about Reform is to hide internals about your model names!
I've googling and trying everything I could think of for the past couple of days to solve a relatively simple (I presume) issue with has_and_belongs_to_many relation.
I managed to successful use the HABTM relation to submit a single relation value. Here's the sample code:
Model:
class Livre < ActiveRecord::Base
has_and_belongs_to_many : auteurs
end
class Auteur < ActiveRecord::Base
has_and_belongs_to_many :livres
end
Controller:
def new
#livre = Livre.new
#auteurs = Auteur.all
end
def create
#livre = Livre.new(livre_params)
if #livre.save
redirect_to [:admin, #livre]
else
render 'new'
end
end
private
def livre_params
params.require(:livre).permit(:name, :auteur_ids)
end
View:
<% f.label :auteur %><br>
<% f.collection_select(:auteur_ids, #auteurs, :id, :name) %>
Posted Params:
{"utf8"=>"✓",
"authenticity_token"=>"mAXUm7MRDgJgCH00VPb9bpgC+y/iOfxBjXSazcthWYs=",
"livre"=>{"name"=>"sdfsdfd",
"auteur_ids"=>"3"},
"commit"=>"Create Livre"}
But when I try to add "multiple true" to the view's collection_select helper, the (now multiple) relation doesn't get saved anymore. Sample code:
(both Model and Controller unchanged)
View:
<% f.label :auteur %><br>
<% f.collection_select(:auteur_ids, #auteurs, :id, :name, {}, {:multiple => true}) %>
Posted Params:
{"utf8"=>"✓",
"authenticity_token"=>"mAXUm7MRDgJgCH00VPb9bpgC+y/iOfxBjXSazcthWYs=",
"livre"=>{"name"=>"sdfsdf",
"auteur_ids"=>["1",
"5",
"3"]},
"commit"=>"Create Livre"}
As you can see, the params for "auteur_ids" is now an array. That's the only difference.
What am I doing wrong?
Just to clarify: both piece of code are able to add a new record to the livres db table, but only the 1st code is able to add the appropriate record to the auteurs_livres db table. The second one simply does not insert anything into auteurs_livres.
(I run on ruby 1.9.3p194 and rails 4.0.1)
Thanks!
Answer
For the fine folks stuck with the same problem, here's the answer:
Edit your controller and change the permitted parameter from :auteur_ids to {:auteur_ids => []}
params.require(:livre).permit(:name, {:auteur_ids => []})
And it now works :)
For the fine folks stuck with the same problem, here's the answer:
Edit your controller and change the permitted parameter from :auteur_ids to {:auteur_ids => []}
params.require(:livre).permit(:name, {:auteur_ids => []})
And it now works :)
You worked out the solution because Rails now expects auteur_ids to be an array, rather than a single item. This means that instead of just passing a single entity to the model, it will package the params as [0][1][2] etc, which is how you can submit your HABTM records now
There is a more Rails way to do this, which is to use accepts_nested_attributes_for. This is going to seem like a lot more work, but it will dry up your system, and also ensure convention over configuration:
Model
class Livre < ActiveRecord::Base
has_and_belongs_to_many : auteurs
accepts_nested_attributes_for :auteurs
end
class Auteur < ActiveRecord::Base
has_and_belongs_to_many :livres
end
Controller
def new
#livre = Livre.new
#livre.auteurs.build
#auteurs = Auteur.all
end
def create
#livre = Livre.new(livre_params)
if #livre.save
redirect_to [:admin, #livre]
else
render 'new'
end
end
private
def livre_params
params.require(:livre).permit(:name, auteur_attributes: [:auteur_id])
end
Form
<%= form_for #livre do |f| %>
<%= f.text_field :your_current_vars %>
<%= f.fields_for :auteurs do |a| %>
<%= a.collection_select(:auteur_id, #auteurs, :id, :name, {}) %>
<% end %>
<% end %>
This will submit the auteur_id for you (and automatically associate the livre_id foreign key in the HABTM model. Currently, this will only submit the number of objects which have been built in the new action -- so in order to add more items, you'll have to build more
On my payments page there are certain variables such as card_number that I want to pass from the View to the Model but I do not want to store them in the db. I can usually easily achieve this by simply using attr_accessor but in this case the model is being passed in params through accepts_nested_attributes_for and for some reason the params are not being passed through:
in User.rb i have
has_many :credit_cards
accepts_nested_attributes_for :credit_cards
in the view file i have a nested form field, something like:
blah blah
<h2>Credit card</h2>
<%= f.fields_for :credit_cards do |builder| %>
<%= render "credit_card_fields", :f => builder %>
<% end %>
inside that
<p>
<%= f.label :test %><br />
<%= f.text_field :test %>
</p>
now back in credit_card.rb i have:
attr_accessor :test
before_create :show_me_test_param
private
def show_me_test_param
raise "#{test}"
end
Now the strange thing is that when I try to save a record, it simply returns an empty exception. The param does not seem to have been passed through from User to CreditCard through accepts_nested_attributes_for?
The param being passed in is:
{"email"=>"name#example.com", "password"=>"pass123", "password_confirmation"=>"pass123", "credit_cards_attributes"=>{"0"=>{"test"=>"helllo this is the second attempt", "name_on_card"=>"first lastname", "card_number"=>"987498742897", "card_verification_value"=>"232", "expiry_date"=>"2141"}}}
Does anyone know whats going on? Does accepts_nested_attributes_for work with attr_accessor?
This has messed me up several times in the past! Params for nested objects come to the controller with the key model_name_attributes which gets passed to the new or update_attributes method of the model in the controller.
So you'll need to add :credit_card_attributes to your attr_accessor to allow that key to be passed in.
I'm serializing a hash that is stored in a settings field in a table, and would like to be able to edit that hash in a form field.
class Template < ActiveRecord::Base
serialize :settings
end
But I just do <%= f.text_area :settings %> then the text area just shows the serialized data instead of the hash.
How can I get the hash to show in the text area?
Maybe setting up another accessor for your model would work.
class Template < ActiveRecord::Base
serialize :settings
attr_accessor :settings_edit
before_save :handle_settings_edit, :if => lambda {|template| template.settings_edit.present? }
def settings_edit
read_attribute(:settings).inspect # should display your hash like you want
end
protected
def handle_settings_edit
# You may want to perform eval in your validations instead of in a
# before_save callback, so that you can show errors on your form.
begin
self.settings = eval(settings_edit)
rescue SyntaxError => e
self.settings = settings_edit
end
end
end
Then in your form use <%= f.text_area :settings_edit %>.
I have not tested any of this code, but in theory it should work. Good luck!
WARNING: Using eval like this is very dangerous, in this example a user could delete the entire Template table with one line in the edit box Template.destroy_all. Use a different method to convert the string to a hash if user input is involved.
... or you could use something like this (without any logic in model):
<% #template.settings.each do |name, value| %>
<div>
<%= label_tag name %>
<%= text_field_tag "template[settings][#{name}]", value %>
</div>
<% end %>
you should use something like
class Template < ActiveRecord::Base
serialize :settings, Hash
end