Rails 3 Baby Steps – Part 5

Go here if you want to see an index for the entire series.

In the last entry of my Rails 3 series, we started looking at how you would implement a typical CRUD controller in Rails, but we really only explored how you would display the data from the controller. We also took a look at how you would route requests to that controller, and also how you might do some simple testing with RSpec. In this part of my Rails 3 series, we are going to take a look at the "UD" part of the CRUD controller; the update and delete actions.


The Destroy Action

The destroy action is pretty straightforward and if we remember from our last post, the action itself just looked like this:

def destroy
  @baby = Baby.find(params[:id])
  @baby.destroy
  redirect_to(babies_url)
end

All we are doing is grabbing the baby specified by the url, then calling the "destroy" method on it. While it is also possible to delete a model by calling the class method "delete" and passing an id, unfortunately this just sends a delete command to the database and doesn’t call any events or enforce any constraints that you might have put on your model. Calling destroy on an instance causes all callbacks, events, and any other code you might be running on cleanup to execute. So while calling "delete" is more efficient, it is almost always not what you want to do.

So now that we understand this action, how do we use it? You may have noticed when we previously looked at the output of "rake routes" that there were two routes that looked similar to the normal "baby" route, but had different HTTP verbs:

PUT  /babies/:id  {:action=>"update", :controller=>"babies"}

DELETE /babies/:id {:action=>"destroy", :controller=>"babies"}

You can see here that one route uses "PUT" and the other route uses "DELETE". You can probably guess which route we need for the destroy action! If we want to link to the destroy action, all we have to do is create a link that look like this:

<%= link_to 'Destroy', baby, 
  :confirm => 'Are you sure?', :method => :delete %>

Here you can see that it is just a normal call to "link_to" but with an added option called "method" to which we are passing "delete". This in turn renders a link into the page that looks like this:

<a href="/babies/1" data-confirm="Are you sure?" 
  data-method="delete" rel="nofollow">Destroy</a>

If you take a look you’ll see two of the html5 "data-" attributes on this tag. Rails 3 uses these to wire up JavaScript events to send the delete to the server. (If you hear people using the term "unobtrusive JavaScript", this is what they are referring to) It uses "data-confirm" in order to pop a confirmation dialog asking you if you want to delete, and then it uses the "data-method" attribute in order to know how to excute.

If you were to snoop the actual request going across the wire you might be upset when you realize that a HTTP DELETE is not actually being sent across the wire, but instead a POST is being sent. This is because Rails wanted to go for the highest level of compatibility possible, and instead decided to pass a "_method" field in the post which specifies the http verb to execute. On the server side, Rails picks this up and routes to the proper action.

The Update Action

Next we will take a look at the update action:

def update
  @baby = Baby.find(params[:id])
  if @baby.update_attributes(params[:baby])
    redirect_to(@baby, :notice => 'Baby was successfully updated.')
  else
    render :action => "edit"
  end
end

In this action we are first retrieving the proper baby from our data store. Then we are calling the "update_attributes" method on the baby. This method allows us to pass a hash of values from the form and update the attributes on the baby using them. If that is successful then we will redirect to the "show" action for that baby, otherwise we re-render the edit form (which will show any validation errors).

Let’s go ahead and take a glance at the edit form to see what it is doing:

<%= form_for(@baby) do |f| %>
  <% if @baby.errors.any? %>
    <div id="error_explanation">
      <h2>
        <%= pluralize(@baby.errors.count, "error") %> 
          prohibited this baby from being saved:
      </h2>

      <ul>
      <% @baby.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </div>
  <div class="field">
    <%= f.label :age %><br />
    <%= f.text_field :age %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

First we see the "form_for" call where we are passing in an instance of our baby that we are editing. This creates our form tag and puts all of the proper attributes on it:

<form accept-charset="UTF-8" action="/babies/1" 
  class="edit_baby" id="edit_baby_1" method="post">

As you can see, we still have a method of "post" here on this form. This is for the same reason that Rails used a post for the delete link above. Instead you’ll notice a "_method" field added to the post data which contains "put".

The next block of code just checks for any errors that may be on the model and displays those on the form. These are validation errors that have occurred when the form was edited previously.

Next you see the two fields that we are rendering in order to enter the name and age of our baby. We can see that the "f" variable being passed in from the block on the form is being used to render the fields. Both of these fields are rendered using "f.text_field" and then they are passed the name of the attribute that the field is rendered for. If we look at the html for these fields you’ll see this input tag:

<input id="baby_name" name="baby[name]" size="30" 
  type="text" value="Test Baby">

Notice the "baby[name]" value in the name attribute. This is how on the server side when we do this:

@baby.update_attributes(params[:baby])

Rails knows which values to grab from the post values because at this point the params variable is a hash, but then the value of the key "baby" is a hash containing all of the values we posted from our form. This makes it possible to post nested models or mix in custom fields, but still be able to use this built in functionality.

Testing

We skipped over the testing of this functionality in this post because it was very similar to the testing that we did in previous episodes when we were testing controller actions. The generated specs for the delete action look like this:

describe "DELETE destroy" do
  it "destroys the requested baby" do
    Baby.stub(:find).with("37") { mock_baby }
    mock_baby.should_receive(:destroy)
    delete :destroy, :id => "37"
  end

  it "redirects to the babies list" do
    Baby.stub(:find) { mock_baby }
    delete :destroy, :id => "1"
    response.should redirect_to(babies_url)
  end
end

In the first test we are stubbing out the find method on our Baby model and setting it up to return the mock_baby. Then we just set the expectation that the mock_baby will receive a call to destroy, an then invoke the method on the controller. In the second test we do something very similar, only after we invoke our method, we test to make sure that a redirect occurred back to the "babies_url", since after a delete we redirect to the list of all babies.

When it comes to the update action, there are a couple more generated specs, but we will just look at two of them. The first:

it "updates the requested baby" do
  Baby.stub(:find).with("37") { mock_baby }
  mock_baby.should_receive(:update_attributes).with({'these' => 'params'})
  put :update, :id => "37", :baby => {'these' => 'params'}
end

Here we are merely testing to see if the baby receives the posted parameters. Since the baby class is a mock, we can pass any attributes that we want, we are only testing that the baby actually receives the posted information. And the second:

it "re-renders the 'edit' template" do
  Baby.stub(:find) { mock_baby(:update_attributes => false) }
  put :update, :id => "1"
  response.should render_template("edit")
end

Here we are setting up the mock so that when "update_attributes" is called, it returns false. This way it looks like the update failed, and so we can check to see if the "edit" form was re-rendered. We could also setup tests to make sure that the model instance variable is getting assigned.

Like I’ve said before, you may find some of these tests more or less useful depending on your situation. Although I would say with absolute certainty that they are not a suitable replacement for integration tests that actually go through the entire request from top to bottom. These tests play an important role in making sure that all of the pieces of your application are functioning properly together.

Summary

I hope you enjoyed this fifth part of my Rails 3 series, at this point you should be well on your way to writing your first Rails app, and with this entry you are now able to do more than just display and link to models, you can now update and delete them! I hope that you’ll come back for part 6 where I am going to take a look at integration testing, and how we can create some integration tests for this controller.

Be Sociable, Share!

Leave a Reply

Your email address will not be published. Required fields are marked *