Model Validations

How to Write a Model Validation

At the moment, we simply save records into the database without checking if the data is valid or not. However, our application is built with the assumption that a post should always have a title, a body and an author. If we allow posts without a title, body or an author to be saved into the database, repercussions will surface in many places and cause bugs in our code. This would occur because of the mismatch of the integrity of data and the assumption of the application code.

As a general guideline, whenever an application allows user's input, we want to validate the data as early as possible. Let's look at how we can do that with our Post model before the records are saved into the database.

### app/models/post.rb ###

class Post

  # ...

  def valid?
    title.present? && body.present? && author.present?
  end

  # ...

end

We are adopting the validation rule that a post is only valid when title, body and author fields are all present.

And notice when we check for the presence of each of the attributes, we're using the present? method, which is a method that Rails provides:

> ''.present?
=> false

> nil.present?
=> false

> 'something'.present?
=> true

As you can see above, this present? method returns false if it's called on nil or an empty string. Just what we're looking for to test if a title, body, or author is present.

And now that we have a way to check if a post is valid or not, let's make sure we do that before saving a post, and react appropriately:

### app/models/post.rb ###

class Post

  # ...

  def save
    return false unless valid?

    if new_record?
      insert
    else
      update
    end

    true
  end

  # ...

end

Here we use what's called a guard clause:

return false unless valid?

We use this guard clause here to "guard" against execution further into Post#save unless the post is valid. This prevents invalid posts from being saved to the database.

So now when we call Post#save, we'll return true if validation passes and the post is saved to the DB. Otherwise, if the validation fails and the post is not saved, we'll return false.

We'll see how to integrate this model validation with the form submission workflow next.

Integrate Validations With Forms

Let's now look at how we can integrate the validation logic to the form submission process.

From a user experience point of view, when the user provides invalid inputs, we want to:

  1. instead of taking the user to the listing posts page, we would "bounce back" to the page with the input form
  2. preserve the user's input so they don't have to fill the form from scratch
  3. show the user what input field(s) are not valid

Let's see how to do this with the "create a post" workflow:

### app/controllers/application_controller.rb ###

class ApplicationController < ActionController::Base

  # ...


  def create_post
    post = Post.new('title' => params['title'], 'body' => params['body'],
      'author' => params['author'])
    if post.save
      redirect_to '/list_posts'
    else
      render 'application/new_post', locals: { post: post }
    end
  end

  # ...

end

From this code you can see if save returns false, in the case of validation failure, we will render the new_post template again with the post object that contains all the user's inputs.

We render the new_post view here because we need to provide the user with a form to keep editing the post.

We also have to change the new_post view so that it would render with a post object passed into it.

app/views/application/new_post.html.erb

<html>
  <body>
    <form method="post" action="/create_post">

      <label for="title"> Title</label>
      <input id="title" name="title" type="text" value="<%= post.title %>"/>
      <br /> <br />

      <label for="body"> Body</label>
      <textarea id="body" name="body"><%= post.body %></textarea>
      <br /> <br />

      <label for="author"> Author</label>
      <input id="author" name="author" type="text" value="<%= post.author %>"/>
      <br /> <br />

      <input type="submit" value="Create Post" />

    </form>

    <br />
    <a href="/list_posts">Back to Posts</a>

  </body>
</html>

With our view ready, we'll just need to adjust the new controller action to pass in an instantiated "empty" post object.

### app/controllers/application_controller.rb ###

class ApplicationController < ActionController::Base

  # ...

  def new_post
    post = Post.new

    render 'application/new_post', locals: { post: post }
  end

  # ...

end

While we're at it, let's also alter our code for updating a post, as we'll want to include validations for that workflow as well.

class ApplicationController < ActionController::Base

  # ...

  def update_post
    post = Post.find(params['id'])
    post.set_attributes('title' => params['title'], 'body' => params['body'],
      'author' => params['author'])
    if post.save
      redirect_to '/list_posts'
    else
      render 'application/edit_post', locals: { post: post }
    end
  end

  # ...
end
<!-- app/views/application/edit_post.html.erb -->

<html>
  <body>
    <form method="post" action="/update_post/<%= post.id %>">

      <label for="title"> Title</label>
      <input id="title" name="title" type="text" value="<%= post.title %>"/>
      <br /> <br />

      <label for="body"> Body</label>
      <textarea id="body" name="body"><%= post.body %></textarea>
      <br /> <br />

      <label for="author"> Author</label>
      <input id="author" name="author" type="text" value="<%= post.author %>"/>
      <br /> <br />

      <input type="submit" value="Update Post" />

    </form>

    <br />
    <a href="/list_posts">Back to Posts</a>

  </body>
</html>

Now we have implemented 1) and 2) from the three requirements we listed before. We are going to tackle the displaying errors requirement in the next lesson.

NOTE: When you reload a page with a browser, it asks the browser to replay the last HTTP request. In the case of a form submission with errors, the last request will be a POST request because the behavior on the server side is to render the form and not redirect to another path.

Display Validations

The last piece we need to do to ensure a good user experience, is to show the validation errors so the user knows what went wrong with their inputs.

Those messages should be created while we validate the user inputs, which currently happens in the Post model. We could just store an array of error messages in the model. Since the model will be passed into the view template when it's rendered, the view template can then pull the error messages out of the model to display on the screen.

Here's what we'll do:

### app/models/post.rb ###

class Post
  attr_reader :id, :title, :body, :author, :created_at, :errors
  # ...

  def initialize(attributes={})
    # ...

    @errors = {}
  end


  def valid?
    @errors['title']  = "can't be blank" if title.blank?
    @errors['body']   = "can't be blank" if body.blank?
    @errors['author'] = "can't be blank" if author.blank?
    @errors.empty?
  end

  # ...

end

The blank? method is defined by Rails to work on nil and strings with the following behavior:

> ''.blank?
=> true

> nil.blank?
=> true

> 'something'.blank?
=> false

As you probably have guessed, blank? is the opposite of present?

Now that we know an invalid post created from user input would always have errors set in the object, we can adjust our view to show the error messages:

app/views/application/new_post.html.erb

<html>
  <body>

    <div class="errors">
      <% post.errors.each do |attribute, error| %>
        <p class="error" style="color: orange">
          <%= attribute %>: <%= error %>
        </p>
      <% end%>
    </div>

    <form method="post" action="/create_post">

      <label for="title"> Title</label>
      <input id="title" name="title" type="text" value="<%= post.title %>"/>
      <br /> <br />

      <label for="body"> Body</label>
      <textarea id="body" name="body"><%= post.body %></textarea>
      <br /> <br />

      <label for="author"> Author</label>
      <input id="author" name="author" type="text" value="<%= post.author %>"/>
      <br /> <br />

      <input type="submit" value="Create Post" />

    </form>

    <br />
    <a href="/list_posts">Back to Posts</a>

  </body>
</html>

Make sure to include the changes we've made above in your edit_post.html.erb file as well. Now is a good time to run through our app and test the code that we have just added in. Try making a post with invalid inputs; the new post view should render and the application should display a list of errors.