How to use error_messages_for in a multi-model form?
If you want to separate error messages for parent and child object it can be a little complicated. Since when you save parent object it also validates child objects and it contains errors for children. So you can do something like this:
<% form_for @album do |f| %>
<%= custom_error_messages_helper(@album) %>
<%= f.label :title %><br />
<%= f.text_field :title %>
<% f.fields_for :tracks do |t| %>
<%= t.error_messages message => nil, :header_message => nil %>
<%= render :partial => 'tracks/fields', :locals => {:f => t} %>
<% end %>
<%= f.submit "Submit" %>
<% end %>
Or you can put this line with t.error_messages
in 'tracks/fields'
partial (I renamed form builder object form f
to t
because it was confusing). It should display (at least it works for me) only errors for specific child object (so you can see what title error is for which object). Also keep in mind, that Rails will automaticaly add css class fieldWithErrors
to fields that contains errors, on example add to css:
.fieldWithErrors {
border: 1px solid red;
}
With errors for parent object it is more complicated since @album.errors
contains also errors for child objects. I didn't find any nice and simple way to remove some errors or to display only errors that are associatted with parent object, so my idea was to write custom helper to handle it:
def custom_error_messages_helper(album)
html = ""
html << '<div class="errors">'
album.errors.each_error do |attr, error|
if !(attr =~ /\./)
html << '<div class="error">'
html << error.full_message
html << '</div>'
end
end
html << '</div>'
end
It will skip all errors that are for attribute which name conatins '.' - so it should print all errors that are associated with the parent object. The only problem is with errors that are added to base - since they attr
value is base
and I'm not sure how are errors added to base to child object added to errors in parent object. Probably they attr
value is also base
so they will be printed in this helper. But it won't be problem if you don't use add_to_base
.
Multi model saving, how to wrap in transaction and report errors
Let's start from the beginning.
We want our registration form object to have the same API as any other ActiveRecord model:
// view.html
<%= form_for(@book) do |f| %>
<% end %>
# controller.rb
def create
@book = Book.new(book_params)
if @book.save
redirect_to @book, notice: 'Book was successfully created.'
else
render :new
end
end
To do that, we create the following object:
class RegForm
include ActiveModel::Model
attr_accessor :company_name, :email, :password
def save
# Save Location and Account here
end
end
Now, by including ActiveModel::Model
, our RegForm
gains a ton of functionality, including showing errors and validating attributes (yes, it's unnecessary to include ActiveModel::Validations
). In this next step we add some validations:
validates :email, :password, presence: true
And we change save
so that it runs those validations:
def save
validate
# Save Location and Account here
end
validate
returns true
if all validations pass and false
otherwise.
validate
also adds errors to the @reg_form
(All ActiveRecord models have an errors
hash which is populated when a validation fails). This means that in the view we can do any of these:
@reg_form.errors.messages
#=> { email: ["can't be blank"], password: ["can't be blank"] }
@reg_form.errors.full_messages
#=> ["Email can't be blank", "Password can't be blank"]
@reg_form.errors[:email]
#=> ["can't be blank"]
@reg_form.errors.full_messages_for(:email)
#=> ["Email can't be blank"]
Meanwhile, our RegistrationsController
should look something like this:
def create
@reg_form = RegForm.new(reg_params)
if @reg_form.save
redirect_to @reg_form, notice: 'Registration was successful'
else
render :new
end
end
We can clearly see that when @reg_form.save
returns false
, the new
view is re-rendered.
Finally, we change save
so that our models save
calls are wrapped inside a transaction:
def save
if valid?
ActiveRecord::Base.transaction do
location = Location.create!(location_params)
account = location.create_account!(account_params)
end
true
end
rescue ActiveRecord::StatementInvalid => e
# Handle database exceptions not covered by validations.
# e.message and e.cause.message can help you figure out what happened
end
Points worthy of note:
create!
is used instead ofcreate
. The transaction is only rolled back if an exception is raised (which methods with a bang usually do).validate
is just an alias forvalid?
. To avoid all that indentation we could instead use a guard at the top of thesave
method:return if invalid?
We can turn a database exception (like an email uniqueness constraint) into an error by doing something like:
rescue ActiveRecord::RecordNotUnique
errors.add(:email, :taken)
endWe can add an error not directly associated with an attribute by using the symbol
:base
:errors.add(:base, 'Company and Email do not match')
Ruby on Rails - How to delegate error messages from nested model
Rails has the validates_associated
helper (also available in Mongoid) which will call valid?
upon each one of the associated objects.
The default error message for validates_associated is "is invalid".
Note that each associated object will contain its own errors
collection; errors do not bubble up to the calling model.Rails Guides: Active Record Validations
class User
include Mongoid::Document
has_many :images
accepts_nested_attributes_for :image
validates_associated :images
end
Note that you should not add validates_associated :user
to Image
since it would cause an infinite loop.
You can access the errors for the nested images like so:
<% if @user.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2>
<ul>
<% @user.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
<% if @user.images.any? %>
<ul>
<% @user.images.each do |image| %>
<% if image.errors.any? %>
<li>
<ul>
<% image.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</li>
<% end %>
<% end %>
</ul>
<% end %>
</div>
<% end %>
Associated models and a nested form with validation not working
Your validation does not work due to a Catch-22
To apply for this job, you would have to be insane; but if you are
insane, you are unacceptable.
ActiveRecord models get their ID from the database when they are saved.
But the validation on the nested user runs before the organization is inserted into the the database.
You would guess that just checking validates_presence_of
instead would pass:
validates_presence_of :organization, unless: -> { usertype == 1 }
Unfortunatly not. In order for validates_presence_of :organization
to pass the organization must be persisted to the database. Catch-22 again.
In order for the validation to pass we would need to split creating the organization and user into two steps:
org = Organization.create(name: 'M & M Enterprises')
user = org.users.build(username: 'milo_minderbinder', ...)
user.valid?
Unfortunatly the means that you cannot use accepts_nested_attributes_for :users
- well at least not straight off the bat.
By using a transaction we can insert the organization into the the database and and roll back if the user is not valid.
def create
@organization = Organization.new(new_params.except(:users_attributes))
@organization.transaction do
@organization.save!
if new_params[:users_attributes].any?
@organization.users.create!(new_params[:users_attributes])
end
end
if @organization.persisted?
# ...
if @organization.users.any?
# send emails ...
end
else
@organization.users.build if @organization.users.blank?
render :new
end
end
Followup questions
We use @organization.persisted?
since we presumably want to redirect to the newly created organisation no matter if the there is a User record created.
because the emails are sent to users? It shouldn't matter since organization is rolled back if no user is created.
The transaction is not rolled back if there is no user created. Only if the user(s) fails to save due to invalid parameters. This is based on your requirement:
But an organization can also (temporarily) have no users.
If you need the @organisation to be invalid without users you could do:
@organisation.errors.add(:users, 'No users provided') unless new_params[:users_attributes].any?
@organization.transaction do
@organization.save!
if new_params[:users_attributes].any?
@organization.users.create!(new_params[:users_attributes])
end
end
You would use @organization.users.any?
to check if there are any users. @organization.users.persisted?
will not work since .persisted?
is a method on model instances - not collections.
On a different note, I assume it's not possible to overwrite/update an existing organization/user with this method (which shouldn't be) instead of always creating a new record?
Right, since this will always issue two SQL insert statements it will not alter existing records.
It is up to you however to create validations that guarantee the uniqueness of the database columns (IE you don't want several records with the same user.email or organiation.name).
On the plus side is that none of these caveats apply when updating an existing organization:
def update
@organisation.update(... params for org and and users ...)
end
Since you don't get the whole chicken or egg dilemma when validating the users.
Capture all errors in form and ensure atomicity -rails
You have to catch the exception outside the transaction.
The transactions are rollbacked when an exception goes out the transaction block
begin
ActiveRecord::Base.transaction do
@var_types.each_with_index do |var,index|
var.save!
puts "saving"
end
end
rescue
puts "rescued"
end
UPDATE after reading your comment:
ActiveRecord::Base.transaction do
raise ActiveRecord::Rollback unless @var_types.map(&:save).all? #passing here a block like { |res| res == true } is redundant.
redirect_to some_index_path, notice: "Everything saved"
end
render action: 'edit' # something didn't pass the validation, re-render the view
Things to note here:
If you raise ActiveRecord::Rollback
inside a transaction block, you don't need a rescue for it (read the docs at http://api.rubyonrails.org/classes/ActiveRecord/Rollback.html)
Someone might say that you should not drive the flow based in exception-throwing, if you don't feel comfortable with it, you can do something like this:
all_saved = false # need to define this var outside, or it will a block-level variable, visible only in the block
ActiveRecord::Base.transaction do
all_saved = @var_types.map(&:save).all?
raise ActiveRecord::Rollback unless all_saved
end
if all_saved
redirect_to some_index_path, notice: "Everything saved"
else
render action: 'edit' # something didn't pass the validation, re-render the view
end
Rails render flash error message from child model
You can use the transaction: https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
This allows you to handle exceptions from both create!
and save!
and prevent @registrant
from being saved.
I consider your code should looks something like
def create
@registrant = Registrant.new(registrant_params)
@patient = Registrant.find(session[:patient_id_to_add_caregiver]) if session[:patient_id_to_add_caregiver]
begin
ActiveRecord::Base.transaction do
@registrant.save!
#other objects creation
CaregiverPatient.create!(patient: @patient, caregiver: @registrant, linked_by: current_login.user.id, link_description: 0) if @patient.present?
redirect_to registrant_path(@registrant), notice: 'Registrant Added'
end
rescue ActiveRecord::RecordInvalid => exception
build_registrant_associations
render :new, { errors: exception.message }
end
end
Related Topics
Rails 3 + Daemons Gem: Exception When Querying Model
Error Occurs When Trying to Install Homebrew on a MAC for Ruby on Rails
Define a Class Method in a Module
Need Help on Join Table, Limiting Results to Only the Resource Id
Ruby on Rails View Rendering Db Info on Page
API Post with Array Using Http Gem (Or Restclient)
Module and Class with the Same Name in Rails Project
Variables in Ruby Method Names
Disabling Flash Message Without Disabling Cache on Click on Back Button in Rails
No Such File to Load -- Soap4R -- Why
Why Does the Script Affect Everything on My Rails 3 App Even When Cased in This Code
How to Convert a Formatted String into Plain Text
Mongomapper Association Skips Duplicates
Regex to Remove the Webpage Part of a Url in Ruby