Reputation: 2224
Organization
and User
have a many-to-many
relationship through Relationship
. There's a joined signup form. The sign up form works in that valid information is saved while if there's invalid information it rolls back everything.
The problem is that the form does not display the error messages for the nested User
object. Errors for Organization
are displayed, the form correctly re-renders if there are errors for User
, but the errors for User
are not displayed.
Why are the errors when submitting invalid information for users
not displayed? Any help is appreciated.
The signup form/view:
<%= form_for @organization, url: next_url do |f| %>
<%= render partial: 'shared/error_messages', locals: { object: f.object, nested_models: f.object.users } %>
... fields for organization...
<%= f.fields_for :users do |p| %>
...fields for users...
<% end %>
<%= f.submit "Register" %>
<% end %>
The shared error messages partial:
<% 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 %></li>
<% end %>
</ul>
</div>
<% end %>
<% if defined?(nested_models) && nested_models.any? %>
<div id="error_explanation">
<ul>
<% nested_models.each do |nested_model| %>
<% if nested_model.errors.any? %>
<ul>
<% nested_model.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
<% end %>
<% end %>
</ul>
</div>
<% end %>
The controller method:
def new
@organization = Organization.new
@user = @organization.users.build
end
def create
@organization = Organization.new(new_params.except(:users_attributes))
@organization.transaction do
if @organization.valid?
@organization.save
begin
@user = @organization.users.create!(users_attributes)
@relationship = @organization.relationships.where(user: @user).first
@relationship.update_attributes!(member: true, moderator: true)
rescue
raise ActiveRecord::Rollback
end
end
end
if @organization.persisted?
if @organization.relationships.where('member = ? ', true).any?
@organization.users.where('member = ? ', true).each do |single_user|
single_user.send_activation_email
end
end
flash[:success] = "A confirmation email is sent."
redirect_to root_url
else
@user = @organization.users.build(users_attributes) if @organization.users.blank?
render :new
end
end
The Organization model:
has_many :relationships, dependent: :destroy
has_many :users, through: :relationships, inverse_of: :organizations
accepts_nested_attributes_for :users, :reject_if => :all_blank, :allow_destroy => true
validates_associated :users
The Relationship model:
belongs_to :organization
belongs_to :user
The User model:
has_many :relationships, dependent: :destroy
has_many :organizations, through: :relationships, inverse_of: :users
Update: If I add an additional line to def create
as below, it seems to work, i.e., then it does display the error messages. However, then it for some reason doesn't save when valid information is submitted. Any ideas how to deal with that?
def create
@organization = Organization.new(new_params.except(:users_attributes))
@user = @organization.users.new(users_attributes)
@organization.transaction do
...
Upvotes: 3
Views: 1819
Reputation: 1527
In case someone might be looking for a solution to render form errors in a form, try:
f.object.errors["account.address"].present?`
The address is the nested attribute here.
Upvotes: 0
Reputation: 457
I had a similar problem. everything seemed to work fine, but I was not getting any errors The solution i found is to build the comment in article#show instead of the view:
@article = Article.find(params[:id])
@comment = @article.comments.build(params[:comment])
and in your articles#show don't use @article.comments.build
but @comment
:
<%= form_for([@article, @comment]) do |f| %>
<%= render 'shared/error_messages', :object => f.object %>
<p><%= f.submit %></p>
<% end %>
make sure you build the comment in your comment#create as well (you really have no choice though :P)
I think you need to pass f.object
instead of @comment
.
Upvotes: 0
Reputation: 1694
This is classic use case for form objects. It is convenient from many perpectives (testing, maintainance ...). For example:
class Forms::Registration
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
def persisted?
false
end
def initialize(attributes = {})
%w(name other_attributes).each do |attribute|
send("#{attribute}=", attributes[attribute])
end
end
validates :name, presence: true
validate do
[user, organization].each do |object|
unless object.valid?
object.errors.each do |key, values|
errors[key] = values
end
end
end
end
def user
@user ||= User.new
end
def organization
@organization ||= Organization.new
end
def save
return false unless valid?
if create_objects
# after saving business logic
else
false
end
end
private
def create_objects
ActiveRecord::Base.transaction do
user.save!
organization.save!
end
rescue
false
end
end
the controller:
class RegistrationsController < ApplicationController
def new
@registration = Forms::Registration.new
end
def create
@registration = Forms::Registration.new params[:registration]
if @registration.save
redirect_to root_path
else
render action: :new
end
end
end
and the view in HAML:
= form_for @registration, url: registrations_path, as: :registration do |f|
= f.error_messages
= f.label :name
= f.text_field :name
= f.submit
It is worth to read more about form objects.
Upvotes: 1
Reputation: 3437
Maybe try this:
<%= render partial: 'shared/error_messages',
locals: { object: f.object, nested_models: [ @user ] } %>
I guess the call to @organization.users.blank?
doesn't work in the way you expected it to do, as the user is not correctly created, because #create! threw an exeption. Rails probably does a check on the database, to see if there are any users now, and thinks there is still nothing in there. So your @organization.users.build(users_attributes)
gets called, but this doesn't trigger validation.
In general I would also recommend you the use of a form object (like in the other answer), when creating complex forms, as this clarifies things like that and makes the view more clean.
Upvotes: 2
Reputation: 7311
Nested attributes bit me SOOO hard every time I decided it's a good time to use them, and I see you know a bit of what I'm talking about.
Here's a suggestion of a different approach, use a form object instead of nested attributes: http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/ see under section 3. Extract Form Objects
You can extract your existing validations on the User
model into a module and import that, to expand on the solution from the blog:
https://gist.github.com/bbozo/50f8638787d6eb63aff4
With this approach you can make your controller code super-simple and make simple and fast unit tests of the not-so-simple logic that you implemented inside and save yourself from writing integration tests to test out all different possible scenarios.
Also, you might find out that a bunch of the validations in the user model are actually only within the concern of the signup form and that those validations will come and bite in later complex forms, especially if you're importing data from a legacy application where validations weren't so strict, or when you one day add additional validators and make half of your user records invalid for update.
Upvotes: 0