Validation on formtastic nested fields - ruby-on-rails

I have a one-to-many association in my code, like this:
class Second < ActiveRecord::Base
has_many :firsts
end
class First < ActiveRecord::Base
belongs_to :second
accepts_nested_attributes_for :second
end
In my erb for First, I have:
<%= f.input :one_field, :label => false %>
<%= f.semantic_fields_for :second do |cp_f| %>
<%= cp_f.input :another_field, :as => :string, :label => "another field" %>
<%= end %>
The form correctly populates the data in the nested table.
I need to put some validation in the controller, and I'd like to point the user to the field where the error occurred. If I write an error like this:
errors.add :one_field, "This is wrong"
This works no problem and puts the error on the page right by the field. But I'd like to do the same thing for the nested field, like maybe:
errors.add :second.another_field, "Another wrong one"
But I get an error:
undefined method `another_field' for :second:Symbol
Is there a way to put an error on the nested field?

The problem was accessing the errors member of First. What I needed was:
second.errors.add :another_field, "Another wrong one"

Related

Rails5, nested form, undefined param

Huston, we have a problem:
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
Form:
<%= form_for(first_model) do |f| %>
<%= f.fields_for :merged_model do |ff| %>
<%= ff.label :date %>
<%= ff.date_select :start_date %>
Problem:
Processing by FirstModelsController#create as HTML Parameters:
{"utf8"=>"✓",
"authenticity_token"=>"f+D8AaVzM6ahrUyo/nwxISFEleVrXGxo8m30sIiLIe7gvG8J9KfONjuT09j6z3M4Rvw+n3Hm6PMddOtfbgjt5g==",
"first_model"=>{"first_name"=>"yyyy", "last_name"=>"yyy",
"merged_model"=>{"start_date(1i)"=>"2017", "start_date(2i)"=>"2",
"start_date(3i)"=>"28", "second_model_id"=>"1"}}, "commit"=>"Create"}
Unpermitted parameter: merged_model Unpermitted parameter:
merged_model
First model's controller's strong params:
params.require(:first_model).permit(:first_name, :last_name, merged_models_attributes: [:id, :start_date])
First model acccepts nested attributes of merged model:
accepts_nested_attributes_for :merged_models
However, after creating a FirstModel, MergedModel does not get created. Tried to create it in form:
<%= f.fields_for :merged_model [first_model.merged_models.build] do |ff| %>
But got:
no implicit conversion of MergedModel into Integer
Not completely understand what that means..
Also tried creating a new MergedModel from a FirstModel's create action, with a bang:
#merge_model = MergedModel.create!
And got the same error - no implicit conversion...
Could anyone explain more about this? I feel its about passing an Array of my MergedModel's params into MergedModel's params...? I am totally lost here...
Your form should have fields_for :merged_models instead of just merged_model.
On fresh installs of Rails5 applications, belongs_to implies optional: false by default (previously called required: true).
You need to create a MergedModel both with FirstModel AND SecondModel associated...
It looks like you are trying create a MergedModel only with a FirstModel associated, if SecondModel is optional, you need to say that for belongs_to with...
belongs_to :second_model, optional: true

Double has_many attributes

I'm beginner in rails and having trouble finding a proper way out with my problem.
I have three models : Conversation, participant, messages which have the following attributes :
Conversation :
module Messenger
class Conversation <ActiveRecord::Base
has_many :participants, :class_name => 'Messenger::Participant'
def messages
self.participants.messages.order(:created_at)
end
end
end
Participant :
module Messenger
class Participant <ActiveRecord::Base
has_many :messages, :class_name => 'Messenger::Message'
belongs_to :conversation, :class_name => 'Messenger::Conversation'
end
end
Message :
module Messenger
class Message <ActiveRecord::Base
default_scope {order(:created_at)}
default_scope {where(deleted: false)}
belongs_to :participant, :class_name => 'Messenger::Participant'
end
end
My trouble is that I'm trying to make a single form to create a conversation with a first message in it. The form looks like this :
= form_for #conversation, url: messenger.conversations_create_path do |f|
.row
.col-md-12.no-padding
.whitebg.padding15
.form-group.user-info-block.required
= f.label :title, t('trad'), class: 'control-label'
= f.text_field :title, class: 'form-control'
.form-group.user-info-block.required
= f.label :model, t('trad'), class: 'control-label'
= f.text_field :model, class: 'form-control'
.form-group.user-info-block.required
= f.label :model_id, t('trad'), class: 'control-label'
= f.text_field :model_id, class: 'form-control'
= fields_for #message, #conversation.participants.message do |m|
= m.label :content, t('trad'), class: 'control-label'
= m.text_area :content, class:'form-control'
.user-info-block.action-buttons
= f.submit t('trad'), :class => 'btn btn-primary pull-right'
I've tried many ways to make this form simple but I've encountered some problems which I don't know how to fix using rails properly.
I've tried using Field_forto include a message in my conversation form, but since I have nothing saved in my database yet it seems I can't link a message to an unexisting participant.
So basically I want my first form, once validated, to create a conversation, link the current user to that conversation and link the message to that first user, but I assume there are ways to do it with the framework and I would not like to do it manually.
What is the proper way to follow to achieve that? Am I even on the good track or shoould I change something or add something?
Edit : to make it more understandable, a participant got a user_id and a conversation_id, which means this is a relation table. I can't adapt the attributes of my models to make it easier since I must keep it in that way for security reasons.
Message needs to belong_to Conversation directly, since you need to disambiguate when participants have more than one conversation.
So having done that, you can build the conversation's default message in the controller using
#conversation.messages.build(participant: #conversation.participants.first)
That's pretty wordy, so you can add a couple of model methods to reduce the controller call to
#conversation.build_default_message
In this case, you want to create a Conversation, but it needs to create a Message with user input as well. So Conversation needs to accept attributes on behalf of Message. You can do that using accepts_nested_attributes_for
class Conversation
accepts_nested_attributes_for :messages
end
This would allow you to create a Conversation with 1 or more associated Messages using
Conversation.create(
...,
messages_attributes: [
{ participant_id: 1, content: 'question' }
]
)
First, in order for your form to accept nested attributes using the fields_for form helper, you need to specify accepts_nested_attributes_for on your Conversation model:
module Messenger
class Conversation <ActiveRecord::Base
has_many :participants, :class_name => 'Messenger::Participant'
# Required for form helper
accepts_nested_attributes_for :participants
[...]
Since you want to save a both Participant as well as its Message from the same form, you need to add a second accepts_nested_attributes_for on your Participant model:
module Messenger
class Participant <ActiveRecord::Base
has_many :messages, :class_name => 'Messenger::Message'
# Required for form helper
accepts_nested_attributes_for :messages
belongs_to :conversation, :class_name => 'Messenger::Conversation'
end
end
Next, in your controller, since this is a new Conversation that doesn't have any Participants at first, you need to build an associated Participant (presumably based on the current_user), and also an associated Message for this new Participant:
def new
#conversation.participants.build(user: current_user).messages.build
end
Finally in your view, specify the attribute fields in three nested blocks, form_for #conversation do |f|, f.fields_for :participants do |p|, and p.fields_for :messages do |m|:
= form_for #conversation, url: messenger.conversations_create_path do |f|
[...]
= f.fields_for :participants do |p|
= p.fields_for :messages do |m|
= m.label :content, t('trad'), class: 'control-label'
= m.text_area :content, class:'form-control'
.user-info-block.action-buttons
= f.submit t('trad'), :class => 'btn btn-primary pull-right'
Side note: the (incorrectly implemented) messages method in Conversation should be replaced by a simple has_many :through relation:
has_many :messages, through: :participants
First of all, I think, you have mistakes in this method:
def messages
self.participants.messages.order(:created_at)
end
since Conversation has_many :participants:
self.participants will return an array, not a single Participant active record object. So you can't directly call messages on an array. You need to iterate that array and call messages on each object.
Use nested form and fields_for method and accepts_nested_attributes_for (you can find how to write these from SO or documentation) and post the code code and error what you got. Then someone might be able to help you.
And to use fields_for:
Since you can't link a message directly to a Conversation and you need a message field, you need to link a #message to any participant. you can build a Participant from current_user or first user i.e User.first for the #conversation and then build a #message for this Participant.

F.select input not saving value on edit

I have an f.select input on my Rails app that comes from this helper method.
def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method)
collection.map do |group|
option_tags = options_from_collection_for_select(
group.send(group_method), option_key_method, option_value_method)
content_tag(:optgroup, option_tags, :label => group.send(group_label_method))
end.join.html_safe
end
The select in the view is shown below.
<%= f.select(:type_id, option_groups_from_collection_for_select(#categories, :types, :category, :id, :name)) %>
When saving the Post, the correct type_id is getting saved, but when I go and edit the post, the select doesn't show the currently selected item like it's supposed to. I'm assuming something is wrong in my code.
Here is my category model
has_many :posts
has_many :types, :order => "name"
and here is my type model
belongs_to :category
You have to provide a 5th argument which is the selected key. Try the code below:
<%= f.select(:type_id, option_groups_from_collection_for_select(#categories, :types, :category, :id, :name, f.object.type_id)) %>
f.object.type_id returns the type_id attribute of the object passed in the form if it has one. Otherwise, it would be nil and will not have anything selected.
I had the same problem and I found that I had simply misspelled the object's name in my controller file. It did not save because the matching object could not be found.

Rails 3, many-to-many form using accepts_nested_attributes_for, how do I set up correctly?

I have a many-to-many relationship between Recipes and Ingredients. I am trying to build a form that allows me to add an ingredient to a recipe.
(Variants of this question have been asked repeatedly, I have spent hours on this, but am fundamentally confused by what accepts_nested_attributes_for does.)
Before you get scared by all the code below I hope you'll see it's really a basic question. Here are the non-scary details...
Errors
When I display a form to create a recipe, I am getting the error "uninitialized constant Recipe::IngredientsRecipe", pointing to a line in my form partial
18: <%= f.fields_for :ingredients do |i| %>
If I change this line to make "ingredients" singular
<%= f.fields_for :ingredient do |i| %>
then the form displays, but when I save I get a mass assignment error Can't mass-assign protected attributes: ingredient.
Models (in 3 files, named accordingly)
class Recipe < ActiveRecord::Base
attr_accessible :name, :ingredient_id
has_many :ingredients, :through => :ingredients_recipes
has_many :ingredients_recipes
accepts_nested_attributes_for :ingredients
accepts_nested_attributes_for :ingredients_recipes
end
class Ingredient < ActiveRecord::Base
attr_accessible :name, :recipe_id
has_many :ingredients_recipes
has_many :recipes, :through => :ingredients_recipes
accepts_nested_attributes_for :recipes
accepts_nested_attributes_for :ingredients_recipes
end
class IngredientsRecipes < ActiveRecord::Base
belongs_to :ingredient
belongs_to :recipe
attr_accessible :ingredient_id, :recipe_id
accepts_nested_attributes_for :recipes
accepts_nested_attributes_for :ingredients
end
Controllers
As RESTful resources generated by rails generate scaffold
And, because the plural of "recipe" is irregular, inflections.rb
ActiveSupport::Inflector.inflections do |inflect|
inflect.irregular 'recipe', 'recipes'
end
View (recipes/_form.html.erb)
<%= form_for(#recipe) do |f| %>
<div class="field">
<%= f.label :name, "Recipe" %><br />
<%= f.text_field :name %>
</div>
<%= f.fields_for :ingredients do |i| %>
<div class="field">
<%= i.label :name, "Ingredient" %><br />
<%= i.collection_select :ingredient_id, Ingredient.all, :id, :name %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
Environment
Rails 3.2.9
ruby 1.9.3
Some things tried
If I change the view f.fields_for :ingredient then the form loads (it finds Recipe::IngredientRecipe correctly, but then when I save, I get a mass-assignment error as noted above. Here's the log
Started POST "/recipes" for 127.0.0.1 at 2012-11-20 16:50:37 -0500
Processing by RecipesController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"/fMS6ua0atk7qcXwGy7NHQtuOnJqDzoW5P3uN9oHWT4=", "recipe"=>{"name"=>"Stewed Tomatoes", "ingredient"=>{"ingredient_id"=>"1"}}, "commit"=>"Create Recipe"}
Completed 500 Internal Server Error in 2ms
ActiveModel::MassAssignmentSecurity::Error (Can't mass-assign protected attributes: ingredient):
app/controllers/recipes_controller.rb:43:in `new'
app/controllers/recipes_controller.rb:43:in `create'
and the failing lines in the controller is simply
#recipe = Recipe.new(params[:recipe])
So the params being passed, including the nested attributes, are incorrect in some way. But I have tried lots of variants that fix-one-break-another. What am I failing to understand?
Thanks to clues from all, I have found what was wrong with my approach. Here's how I solved it.
I had originally tried with a simple HABTM many-to-many relationship, where the join table was named following standard Rails convention: ingredients_recipes. Then I realized that in a way, accepts_nested_attributes_for is designed for a 1-to-many relationship. So I converted to using has_many_through, creating a model IngredientsRecipes.
That name was the core problem, because Rails needs to be able to convert from plural to singular when using build to create form elements. This caused it to look for the non-existant class Recipe::IngredientsRecipe. When I changed my form so it used fields_for :ingredient the form displayed, but still failed to save with a mass assignment error. It even failed when I added :ingredients_attributes to attr_accessible. It still failed when I added #recipe.ingredients.build to RecipesController#new.
Changing the model to a singular form was the final key to resolve the problem. IngredientsRecipe would have worked, but I chose RecipeIngredients, as it makes more sense.
So to summarize:
can't use accepts_nested_attributes_for with has_and_belongs_to_many; need has_many with through option. (Thanks #kien_thanh)
adding accepts_nested_attributes_for creates a accessor that must be added to attr_accessible in the form <plural-foreign-model>_attributes, e.g. in Recipe I added attr_accessible :name, :ingredients_attributes (Thanks #beerlington)
before displaying the form in the new method of the controller, must call build on the foreign model after creating a new instance, as in 3.times { #recipe.ingredients.build }. This results in HTML having names like recipe[ingredients_attributes][0][name] (Thanks #bravenewweb)
join model must be singular, as with all models. (All me :-).
If you inspect the form that is generated, you'll notice that the nested fields have a name like "ingredients_attributes". The reason you're getting the mass-assignment error is because you need to add these fields to the attr_accessible declaration.
Something like this should fix it (you'll need to doublecheck the field names):
class Recipe < ActiveRecord::Base
attr_accessible :name, :ingredients_attributes
#...
end
Update: There's a similar answer here
Leave the call as
<%= f.fields_for :ingredients do |i| %>
But before that do
<% #recipe.ingredients.build %>
Im guessing that will allow your form to be created the right way, but there are likely other errors with your models, I can look # it more in detail when I have more time if its still not working, but:
As far as what accepts_nested_attributes_for does, when you pass in a correctly formatted params hash to the Model.new or Model.create or Model.update, it allows those attributes on the related model to be saved if they are in the params hash. In addition though, you do need to make the attributes accessible if they are unaccessible in the parent model as stated by beerlington.
I think you just need set up a one-to-many association, one recipe has many ingredients and one ingredient belongs to one recipe, so your model look like:
class Recipe < ActiveRecord::Base
attr_accessible :name, :ingredients_attributes
has_many :ingredients
accepts_nested_attributes_for :ingredients
end
class Ingredient < ActiveRecord::Base
attr_accessible :name, :recipe_id
belongs_to :recipe
end
You are built right form, so I don't write it again here. Now in your new and create controller will be like this:
def new
#recipe = Recipe.new
# This is create just one select field on form
#recipe.ingredients.build
# Create two select field on form
2.times { #recipe.ingredients.build }
# If you keep code above for new method, now you create 3 select field
end
def create
#recipe = Recipe.new(params[:recipe])
if #recipe.save
...
else
...
end
end
How does params[:recipe] look like? If you just have one select field, maybe like this:
params = { recipe: { name: "Stewed Tomatoes", ingredients_attributes: [ { id: 1 } ] } }
If you have 2 ingredient select field:
params = { recipe: { name: "Stewed Tomatoes", ingredients_attributes: [ { id: 1 }, { id: 2 } ] } }

Rails 3 form error: "undefined method `quoted_table_name'"

I have a Rails 3 form (simple_form, really) that has a set of nested attributes:
<%= simple_form_for(#user, :url => the_path(#user)) do |f| %>
...
<%= f.simple_fields_for :credit_card do |c| %>
<%= c.input :number, :label => 'Credit card number' %>
...
<% end %>
...
<% end %>
The problem is that the :credit_card attributes belong to the class CreditCard, which is not a model since I'm not storing any of the credit card data in the database. I have that Class defined in /app/models/credit_card.rb like this (per this RailsCast):
class CreditCard
include ActiveModel::Validations
include ActiveModel::Conversion
include ActiveModel::Naming
attr_accessor :number, :expiration_month, :expiration_year, :cvv
validates_presence_of :number, :expiration_month, :expiration_year, :cvv
def initialize(attributes = {})
attributes.each do |name, value|
send("#{name}=", value)
end
end
def persisted?
false
end
end
In user.rb, I have this:
has_one :credit_card
accepts_nested_attributes_for :credit_card
When I access the page, I get this error:
undefined method `quoted_table_name' for CreditCard:Class
Googling that error didn't yield any suggestions. I'm able to create CreditCard objects from Rails Console, but for some reason the Rails form generator isn't seeing the class.
I already tried swapping out simple_form_for with form_for (and the related changes), so I don't think it's a simple_form problem.
This seems error in the association with user
This seems to be a problem with having a model without an associated table in the database. I am having the same situation where I don't want to store a nested model in application database table but rather fetch and save information to and from an external service.
class Order < ActiveRecord::Base
attr_accessor :identifier, :amount
has_one :credit_card_transaction
end
class CreditCardTransaction
attr_accessor :number, :expiration_date
belongs_to :order
end
I don't want to save credit card transaction object in local database. And getting the same problem as you are. Please keep posted for any updates.
It may be a case of the presence of another class that has the same name "CreditCard" which in this case the class is not an ActiveRecord and thus does not have quoted_table_name method.

Resources