How to display error messages in a multi-model for

2020-05-04 10:52发布

问题:

Two models, Organization and User, have a 1:many relationship. I have a combined signup form where an organization plus a user for that organization get signed up.

The problem I'm experiencing is: When submitting invalid information for the user, it renders the form again, as it should, but the error messages (such as "username can't be blank") for the user are not displayed. The form does work when valid information is submitted and it does display error messages for organization, just not for user.

How should I adjust the code below so that also the error messages for user get displayed?

def new
  @organization = Organization.new
  @user = @organization.users.build
end

def create
  @organization = Organization.new(new_params.except(:users_attributes))    #Validations require the organization to be saved before user, since user requires an organization_id. That's why users_attributs are above excluded and why below it's managed in a transaction that rollbacks if either organization or user is invalid. This works as desired.

  @organization.transaction do
    if @organization.valid?
        @organization.save
        begin
          # I executed next line in debugger (with invalid user info), which correctly responds with: ActiveRecord::RecordInvalid Exception: Validation failed: Email can't be blank, Email is invalid, Username can't be blank, etc.
          @organization.users.create!(users_attributes)
        rescue
          # Should I perhaps add some line here that adds the users errors to the memory?
          raise ActiveRecord::Rollback
        end
     end
  end

  if @organization.persisted?
    flash[:success] = "Yeah!"
    redirect_to root_url
  else
    @user = @organization.users.build(users_attributes)  # Otherwise the filled in information for user is gone (fields for user are then empty)
    render :new
  end

end

The form view includes:

<%= form_for @organization, url: next_url do |f| %>
    <%= render partial: 'shared/error_messages', locals: { object: f.object, nested_models: f.object.users } %>
    <%= f.text_field :name %>
        # Other fields

    <%= f.fields_for :users do |p| %>
        <%= p.email_field :email %>
            # Other fields
    <% end %>

    <%= f.submit "Submit" %>
<% end %>

The error messages partial is as follows:

<% object.errors.full_messages.each do |msg| %>
  <li><%= msg.html_safe %></li>
<% end %>

Update: Following the steps from Rob's answer I arrived at the errors partial below. This still does not display error messages for User. I added debugger responses inside the code below and for some reason nested_model.errors.any? returns false, while the debugger inside the controller (see above) does return error messages for user.

<% if object.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains <%= pluralize(object.errors.count, "error") %>.
    </div>

    <ul>
      <% object.errors.full_messages.each do |msg| %>
        <li><%= msg.html_safe %></li>
      <% end %>
    </ul>

  </div>
<% end %>

<% if defined?(nested_models) && nested_models.any? %>
  # Debugger: responds with "local-variable" for "defined?(nested_models)" and for "nested_models.any?" returns true.
  <div id="error_explanation">
    <ul>
      <% nested_models.each do |nested_model| %>
      # Debugger: "nested_model" has the same values as "nested_models.any?", as you would expect. But for "nested_model.errors.any?" it returns false, which it shouldn't.
        <% if nested_model.errors.any? %>    #Initially had "unless nested_model.valid?" but then errors for User are immediately displayed on loading the form page (new method).
          <ul>
            <% nested_model.errors.full_messages.each do |msg| %>
              <li><%= msg.html_safe %></li>
            <% end %>
          </ul>
        <% end %>
      <% end %>
    </ul>
  </div>
<% end %>

回答1:

Try adding validates_associated :users under your has_many :users association in Organization.

http://apidock.com/rails/ActiveModel/Validations/ClassMethods/validates_associated



回答2:

Did you code successfully create a person during the rescue block?

  rescue ActiveRecord::RecordInvalid => exception
      # do something with exception here
      raise ActiveRecord::Rollback
      @organization.users.build if @organization.users.blank?
      render :new and return

This code looks like it will create a new empty User regardless of incorrect validations. And render new will simply return no errors because the user was successfully created, assuming Organization has no Users.

The control flow of this method has a few outcomes, definitely needs to be broken down some more. I would use byebug and walk through the block with an incorrect Organization, then incorrect name. Then an empty Organization with incorrect User attributes.



回答3:

organization has_many :users and user belongs_to :organization

organization.rb

accepts_nested_attributes_for :users

new.html.erb

<%= form_for @organization, url: next_url do |f| %>
 <%= render 'shared/error_messages', object: @organization %>
 <%= f.text_field :name %>
    # Other fields
 <%= f.fields_for(:users,@organization.users.build) do |p| %>
   <%= p.email_field :email %>
   # Other fields
 <% end %>
 <%= f.submit "Submit" %>
<% end %>

In controller

def create
  @organization = Organization.new(new_params)
  if @organization.save
    flash[:success] = "Yeah!"
    redirect_to root_url
  else
   render :new
  end
end


回答4:

This is very related to this question. The key is that <%= render 'shared/error_messages', object: f.object %> is, I assume, only calling the .errors method on the object it is passed (in this case, organization).

However, because the user errors reside with the user object, they won't be returned and therefore will not be displayed. This requires simply changing the view logic to also display the results of .errors on the various user models. How you want to do so is up to you. In the linked thread, the accepted answer had the error message display code inline instead of in a partial, so you could do it that way, but it would be somewhat redundant.

I would modify my shared/error_messages.html.erb file to check for another passed local called something like nested_models. Then it would use that to search the associated models and include the errors on that. We just would need to check whether it is defined first so that your other views that don't have a nested model won't cause it to raise an error.

shared/error_messages.html.erb

<% if object.errors.any? %>
  <div class="error-messages">
    Object Errors:
    <ul>
      <% object.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
    </ul>
    <% if defined?(nested_models) && nested_models.any? %>
      Nested Model(s) Errors:
      <ul>
        <% nested_models.each do |nested_model| %>
          <% unless nested_model.valid? %>
            <li>
              <ul>
                <% nested_model.errors.full_messages.each do |msg| %>
                  <li><%= msg %></li>
                <% end %>
              </ul>
            </li>
          <% end %>
        <% end %>
      </ul>
    <% end %>
  </div>
<% end %>

Then you would just need to change a single line in your view:

<%= render partial: 'shared/error_messages', locals: { object: @organization, nested_models: @organization.users } %>


回答5:

Looks like you have a lot of untestable logic in your controller. Looks like for you logic will be better to use simple FormObject pattern. https://robots.thoughtbot.com/activemodel-form-objects