Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nested model validation - errors don't show

There have been many questions about this, but none of them seem to help. And yes, I have watched this rails cast.

I have an Author who has many Books, like so:

Author:

class Author < ActiveRecord::Base
  attr_accessible :name
  has_many :books, dependent: :destroy

  accepts_nested_attributes_for :books, allow_destroy: true

  validates :name, presence: true
  validates :name, length: { minimum: 3 }
end

Book:

class Book < ActiveRecord::Base
  attr_accessible :name, :year
  belongs_to :author

  validates :name, :year, presence: true
  validates :year, numericality: { only_integer: true, less_than_or_equal_to: Time.now.year }
end

I created the following form to add a book to an author in authors#show:

<%= form_for([@author, @book], html: { class: "well" }) do |f| %>
<% if @book.errors.any? %>
    <div class="alert alert-block">
        <ul>
            <% @author.errors.full_messages.each do |msg| %>
                <li><%= msg %></li>
            <% end %>
        </ul>
    </div>
<% end %>
#labels and buttons...
<% end %>

...with the following authors_controller method:

def show
    @author = Author.find(params[:id])
    @book = @author.books.build
end

...and the following books_controller method:

def create
    @author = Author.find(params[:author_id])
    if @author.books.create(params[:book])
      redirect_to author_path(@author)
    else
      render action: :show
    end
  end

I cannot seem to figure out why the form does not display any error messages. I followed the example from railscasts where they say there should be an instance variable of books in the form instead of @author.books.build, so I put the latter in the controller and @book in the form - still to no avail.

Thanks for any help!

like image 717
weltschmerz Avatar asked Oct 19 '12 10:10

weltschmerz


2 Answers

Let's step through it.

You submit the create, and that enters your create action

def create
  @author = Author.find(params[:author_id])
  if @author.books.create(params[:book])
    redirect_to author_path(@author)
  else
    render action: :show
  end
end

(Side note, what if @author is not found. You are not handling that case.)

Now, the Author is found, but @author.books.create fails (returns false), so you render the show action.

This uses the show template, but does not call the show action code. (Side note, maybe the new page would be a better choice, so the user can try to create again.)

At this point @author is instantiated with the Author you found, but not @book. So @book, if called will be nil.

Your show template does

if @book.errors.any?

which will not be true, so the rest of the template inside the if will be skipped. That's why there are no errors.

You don't need a form_for to display error messages. If you switch to using the new template, then there will be a form to try again.

So let's switch to rendering new.

Class BooksController < ApplicationController
  def new
    @author = Author.find(params[:author_id])
    @book = @author.books.build
  end

  def create
    @author = Author.find(params[:author_id])
    @book = @author.books.build(params[:book])
    if @author.save
      redirect_to author_path(@author)
    else
      render action: :new
    end
  end

Your new template will be

<% if @author.errors.any? %>
    <div class="alert alert-block">
        <ul>
            <% @author.errors.full_messages.each do |msg| %>
                <li><%= msg %></li>
            <% end %>
        </ul>
    </div>
<% end %>
<% if @book.errors.any? %>
    <div class="alert alert-block">
        <ul>
            <% @book.errors.full_messages.each do |msg| %>
                <li><%= msg %></li>
            <% end %>
        </ul>
    </div>
<% end %>

<%= form_for([@author, @book], html: { class: "well" }) do |f| %>
#labels and buttons...
<% end %>
like image 140
Marlin Pierce Avatar answered Oct 04 '22 01:10

Marlin Pierce


In Books controller /books_controller.rb

def new
   @author = Author.find_by_id(params[:author_id])
   @book = @author.books.build
end

def create 
   @author = Author.find_by_id(params[:author_id])
   if @author
     @book = @author.books.build(params[:book])
     if @book.save
       flash[:notice] = "Book saved successfully"
       redirect_to author_path(@author)
     else
       render :new
     end
  else
   flash[:notice] = "Sorry no author found"
   redirect_to author_path
  end 
end

If author is not present redirect to authors index page with error message dont render the new form as you'll not be able to build the books form as author is nil.

And in your books new form you can have the error listed for books

/books/new.html.erb

  <% if @book.errors.any? %>
    <div class="alert alert-block">
      <ul>
         <% @books.errors.full_messages.each do |msg| %>
             <li><%= msg %></li>
         <% end %>
     </ul>
   </div>
 <% end %>
like image 24
Icicle Avatar answered Oct 04 '22 01:10

Icicle