How to Display Error Messages in a Multi-Model Form with Transaction

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:

  1. create! is used instead of create. The transaction is only rolled back if an exception is raised (which methods with a bang usually do).

  2. validate is just an alias for valid?. To avoid all that indentation we could instead use a guard at the top of the save method:

    return if invalid?
  3. 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)
    end
  4. We 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



Leave a reply



Submit