6.4. Nested Resources

What you'll need to address all these points is the ability to nest the two resources. Edit your config outes.rb file to replace the following two lines (non-adjacent in your routes file):

map.resources :comments
map.resources :articles, :collection => { :unpublished => :get }

with:

map.resources :articles, :has_many => :comments, :collection => { :unpublished =>
:get }

Alternatively, when you need more control, you can use this equivalent block-based syntax:

map.resources :articles, :collection => {:unpublished => :get} do |article|
  article.resources :comments
end

Once you've added the highlighted line to the routes file, run rake routes again and you'll see that all the /comments are gone, replaced by /articles/:article_id/comments. Awesome!

On top of that, you'll have _path and _url helpers, as you're accustomed to, for comments as well. Given that the routes are nested, the stem for these methods will be different, as shown in the output of the routes task you just ran. For example, article_comments_url will accept an article as an argument and return the URL to access a list of all its comments. If you hadn't nested the resources, that helper would have been comments_url, and it wouldn't have accepted any arguments.

You changed the routes file, so go ahead and restart Mongrel. The bad news is that if you try to load http://localhost:3000/articles/1/comments/new in your browser, you will get a nasty error like this one:

undefined method 'comments_path' for #<ActionView::Base:0x59bfcf8>

The error is pretty obvious if you think about it. The view template generated by scaffold in the file appviewscomments ew.html.erb uses the default helper methods, such as comments_path. Because you changed the routes file to define nested routes, you now need to ensure that any helpers in the controller and the view layers are changed to reflect this new arrangement.

Let's do that right away.

6.4.1. Adapting the Controller

The scaffold generator created a standard controller for the comment resource. However, you changed the routes file so that comments are logically tied to an existing article. For this reason you'll need to adapt the Comments controller.

Let's start by instructing the Comments controller to use the same layout you used for the articles. You do this by invoking the layout method in its definition:

layout "articles"

This indicates to the Comments controller that it should render the articles.html.erb layout for all the actions it defines. So go ahead and delete the comments.html.erb layout generated by scaffold.

The layout method also accepts the :except and :only conditions whenever you need to define a specific layout only for certain actions. Specifying layout nil is one way to indicate that no layouts should be rendered.

In the index action you have:

@comments = Comment.find(:all)

This index action will need to list all the comments for a certain article. Assuming that this article is stored in @article, you can access all of its comments with @article.comments. This returns an array of Comment objects. If you need to refine the comment search with some condition, you can do so by chaining a finder method to @article.comments just like you would with a named scope or a regular ActiveRecord::Base object (for example, @article.comments is equivalent to @article.comments.find(:all)).

Replace the preceding line in the index action with the following:

@comments = @article.comments

Now, change the show, edit, update, and destroy actions. Replace the following assignment in all of them:

@comment = Comment.find(params[:id])

with:

@comment = @article.comments.find(params[:id])

Replace this line in the new action:

@comment = Comment.new

with this:

@comment = @article.comments.new

This will create a new, unsaved Comment object whose article_id already references the id in @article.

Finally, in the create action replace:

@comment = Comment.new(params[:comment])

with:

@comment = @article.comments.build(params[:comment])

You are almost done with the controller. You just have to replace the redirects so that they'll work with the nested resources. After creating a comment, you want to redirect back to the article that lists all the comments as well (for HTML requests). To do so, change the existing format.html { redirect_to(@comment) } in the create action with:

format.html { redirect_to(@article) }

On the other hand, when you update a comment, an operation that the blog's owner might do, you are okay with redirecting back to the show action for the comment in question, thus verifying that the comment looks good. To do so you can't simply use redirect_to(@comment) because comments are now a resource that is nested into articles. Therefore you'll need to use redirect_to([@article, @comment]). This is a succinct notation to express that you want to redirect to the show action for the object @comment, which "belongs to" the @article object. It's equivalent to using redirect_to(article_comment_url(@article, @comment)).

Go ahead and replace the following line from the update action:

format.html { redirect_to(@comment) }

with:

format.html { redirect_to([@article, @comment]) }

Note also that the destroy action performs a redirect with redirect_to(comments_url) in an attempt to redirect to the list of comments after deleting the comment (well, at least for HTML requests). With your new routes file in place, you'll have to modify this and replace it with redirect_to(article_comments_url(@article)).

When you are confused as to what stem of the helper to use, always remember to use rake routes. After a while you'll become accustomed to them and will no longer need to look them up.

The last thing left to do is to ensure that before each action the @article variable is set to the article indicated by the article_id parameter in the request. You do this in two steps. You first define a private method called get_article in the Comments controller:

private

def get_article
  @article = Article.find(params[:article_id])
end

This is private because it's just an auxiliary method in the controller, and it doesn't need to be exposed to the end user as an action. The next step is instructing the controller to invoke this method before any action is executed. You do this by using the before_filter method:

before_filter :get_article

In this way when, for example, in the index action you have @comments = @article.comments, the variable @article will exist, and already be set to the object whose id is the article_id parameter in the request. For instance, when you send a GET request for http://localhost:3000/articles/3/comments this invokes the get_article method, finds and assigns the record with id 3 to the @article variable, and then invokes the index action, which retrieves all the comments for that record with @comments = @article.comments.

The resulting code of the controller is shown in Listing 6-1.

In production mode, a 404 status code is returned when a record cannot be found.

Example 6.1. The comments_controller.rb File Adjusted for Nested Resources
class CommentsController < ApplicationController
  layout "articles"

  before_filter :get_article

  # GET /comments
  # GET /comments.xml
  def index
    @comments = @article.comments

    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @comments }
    end
  end

  # GET /comments/1
  # GET /comments/1.xml
  def show
    @comment = @article.comments.find(params[:id])

    respond_to do |format|
      format.html # show.html.erb
      format.xml  { render :xml => @comment }
    end
  end

  # GET /comments/new
  # GET /comments/new.xml
  def new
    @comment = @article.comments.new

    respond_to do |format|

format.html # new.html.erb
      format.xml  { render :xml => @comment }
    end
  end

  # GET /comments/1/edit
  def edit
    @comment = @article.comments.find(params[:id])
  end

  # POST /comments
  # POST /comments.xml
  def create
    @comment = @article.comments.build(params[:comment])

    respond_to do |format|
      if @comment.save
        flash[:notice] = 'Comment was successfully created.'
        format.html { redirect_to(@article) }
        format.xml  { render :xml => @comment, :status => :created, :location => @
comment }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @comment.errors, :status => :unprocessable_
entity }
      end
    end
  end

  # PUT /comments/1
  # PUT /comments/1.xml
  def update
    @comment = @article.comments.find(params[:id])

    respond_to do |format|
      if @comment.update_attributes(params[:comment])
        flash[:notice] = 'Comment was successfully updated.'
        format.html { redirect_to([@article, @comment]) }
        format.xml  { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml  { render :xml => @comment.errors, :status => :unprocessable_
entity }
      end
    end
  end

  # DELETE /comments/1
  # DELETE /comments/1.xml
  def destroy
    @comment = @article.comments.find(params[:id])
    @comment.destroy

respond_to do |format|
      format.html { redirect_to(comments_url) }
      format.xml  { head :ok }
    end
  end

  private

  def get_article
    @article = Article.find(params[:article_id])
  end
end

6.4.2. Adapting the View Layer

The following is the automatically generated code for appviewscomments ew.html.erb:

<h1>New comment</h1>

<% form_for(@comment) do |f| %>
  <%= f.error_messages %>

  <p>
    <%= f.label :article %><br />
    <%= f.text_field :article %>
  </p>
  <p>
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </p>
  <p>
    <%= f.label :email %><br />
    <%= f.text_field :email %>
  </p>
  <p>
    <%= f.label :body %><br />
    <%= f.text_area :body %>
  </p>
  <p>
    <%= f.submit "Create" %>
  </p>
<% end %>

<%= link_to 'Back', comments_path %>

The highlighted lines are problematic. The first highlighted one doesn't work because you are generating a RESTful form for @comment, but comments are now nested into articles, so the form will need to be aware of the article for which you are generating the form as well. Then you have a text field, and its label, for the article object. These two lines will get scrapped, because you don't want to see an input box prompting you to insert the article_id manually. Finally, due to the nested routes you defined, comments_path is no longer an existing helper, so you'll also need to change this.

You need to modify this code and while you are at it, you should place it into a partial template (as you did for the articles in the previous chapter) given that both the new and edit actions of the Comments controller share almost identical code. Create a _form.html.erb partial and place it in appviewscomments.

Now add the code shown in Listing 6-2 (downloadable as listing0602.html.erb).

Example 6.2. The appviewscomments\_form.html.erb Partial Template
<% form_for [article, comment] do |f| %>
  <%= f.error_messages %>

  <% field_set_tag do %>

    <div class="field">
      <%= f.label :name %>
      <%= f.text_field :name %>
    </div>

    <div class="field">
      <%= f.label :email %>
      <%= f.text_field :email %>
    </div>

    <div class="field">
      <%= f.label :body %>
      <%= f.text_area :body %>
    </div>

  <% end %>

  <% field_set_tag do %>
    <%= f.submit button_value, :class => "button" %>
  <% end %>
<% end %>

The first highlighted line uses the array [article, comment]. The form_for method is smart enough to figure out that it should create a comment form for the article specified in article. As usual, if comment is a new unsaved object, the form will be empty (new action) or pre-filled with the existing values if not (edit action).

The second highlighted line is just the usual trick of defining a local button_value variable so that the partial can be rendered from both new.html.erb and edit.html.erb with the button having the names "Create Comment" and "Save Changes," respectively.

Another difference between the scaffold code and the partial is that we added the fieldset tags (through the field_set_tag helper) and the CSS classes to customize the appearance.

6.4.2.1. Fixing new.html.erb and edit.html.erb

Now you need to modify appviewscomments ew.html.erb and appviewscommentsedit.html.erb to render the partial and fix the links by using helpers for the nested routes.

Replace all the code within appviewscomments ew.html.erb with the following:

<h1>New comment</h1>

<%= render :partial => "form", :locals => {:article => @article,
                                           :comment => @comment,
                                           :button_value => "Create Comment"} %>

<%= link_to 'Back', article_comments_path(@article) %>

The highlighted line will assign the @article and @comment instance variables (defined in the Comments controller) to the local variables article and comment in the partial _form.html.erb. It will also assign the value "Create Comment" to the button_value local variable.

You then change the last line to use the helper article_comments_path(@article) to obtain a link that sends a GET request for /articles/:article_id/comments (where :article_id is the actual value of the id of the Article object assigned to @article). This will allow you to return to the list of comments for the article.

Now replace the content of appviewscommentsedit.html.erb with the following:

<h1>Editing comment</h1>

<%= render :partial => "form", :locals => {:article => @article,
                                           :comment => @comment,
                                           :button_value => "Save Changes"} %>

<%= link_to 'Show', [@article, @comment] %> |
<%= link_to 'Back', article_comments_path(@article) %>

The code for rendering the partial is identical, except for the button_value variable being set to "Save Changes" instead of "Create Comment." At the bottom of the template there is also an extra link to show the comment. This uses [@article, @comment] to generate a link that sends a GET request to the server, for the path /articles/:article_id/comments/:id, where :article_id and :id are the actual values of the id attribute of the model objects @article and @comment, respectively. For example, /articles/1/comments/4 shows the comment with id4 that was made for the article with id 1.

6.4.2.2. Fixing index.html.erb and show.html.erb

You now need to modify the other two templates: appviewscommentsindex.html.erb and appviewscommentsshow.html.erb. You are not really too concerned about their appearance, because you'll embed comments into the article template so these templates will only be seen by the blog author.

In other words, they act as your quick admin interface for performing CRUD operations on the comments.

Simply replace the helpers placed in there by scaffold with their equivalent nested route helper.

In appviewscommentsindex.html.erb replace:

<td><%= link_to 'Show', comment %></td>
    <td><%= link_to 'Edit', edit_comment_path(comment) %></td>

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

with:

<td >
      <%= link_to 'Show', [@article, comment] %> ]]>·
      <%= link_to 'Edit', [:edit, @article, comment] %> ]]>·
      <%= link_to 'Destroy', [@article, comment], :confirm => 'Are you sure?', :method =>
:delete %>
    </td>

The &middot; is there just to visualize the link in a more compact manner.

[:edit, @article, comment] is a convenient notation that is equivalent to using edit_article_comment_path(@article, comment).

Also replace the following:

<%= link_to 'New comment', new_comment_path %>

with:

<%= link_to 'New comment', new_article_comment_path(@article) %> |
<%= link_to 'Back to the article', @article %>

Now, in appviewscommentsshow.html.erb replace the following couple of lines:

<%= link_to 'Edit', edit_comment_path(@comment) %> |
<%= link_to 'Back', comments_path %>

with:

<%= link_to 'Edit', [:edit, @article, @comment] %> |
<%= link_to 'Back', article_comments_path(@article) %>

All four templates will now be compatible with the nested resources you defined. Try to visit http://localhost:3000/articles/4/comments and http://localhost:3000/articles/4/comments/new. Assuming that you have an article with id 4, you should now see a (empty) list of comments and the form for creating a new comment, respectively. If you do, all the changes were applied correctly.

We made a lot of chances to the initial scaffolding code. If you see errors instead of nice forms, you can go through the steps presented here again, in order to spot any differences between this text and your code.

The remaining problem is that if you visit http://localhost:3000 or http://localhost:3000/articles/4, nowhere will you see references to comments or a way to create new ones. This is because you haven't touched the article templates yet. Let's do that.

6.4.3. Embedding Comments in Articles

Before you begin modifying the templates, think for a moment about how you want to embed comments. It is safe to assume that the user is accustomed to the following two conventions:

  1. On the front page there should be a link to the comments for each article. In your case there is only one article on the front page, but the code should still work if you change your pagination policy.

  2. When showing an article, all the comments should be listed below the main text and a form for adding a new one should be present as well.

The first is just a link that needs to be added in the "About this article" section. So go ahead and add the following highlighted line to the appviewsarticles\_article.html.erb partial:

<h3>About this article</h3>
<%= publication_status(article) %><br />
<%= link_to pluralize(article.comments.size, 'comment'), article %>

The link uses the pluralize helper provided by ActiveSupport, to obtain "1 comment" when there is only one comment or the pluralized version when there are more comments (for example, "23 comments"). The front page with this new link is shown in Figure 6-5.. This link leads to the show action of the Articles controller, which will display the article and (at the bottom) all the existing comments.

Alternatively you could link directly to the comments section through an HTML anchor.

Figure 6.5. Figure 6-5

The future tense in the previous sentence is necessary because the show action of the Articles controller does not know anything about comments yet. You'll need to perform a few changes in order to be able to display all the comments and a new form at the bottom.

6.4.3.1. Listing the Comments and the New Form

Currently, the show action of the Articles controller takes care of displaying one article. You'd like to be able to display a list of associated comments and a form to create a new one as well though. To do this, add a @comments and a @comment variable to the show action. In appcontrollersarticles_controller.rb add the highlighted lines:

@article = Article.find(params[:id])
@comments = @article.comments
@comment = @article.comments.new

Now the action will retrieve the article first, and then all of its comments, assigning them to @comments. It will also prepare a new Comment object and assign it to @comment. In the view, let's modify appviewsarticlesshow.html.erb to take advantage of this. Listing 6-3 shows the updated code.

Example 6.3. The appviewsarticlesshow.html.erb Template with Embedded Comments
<%= render :partial => @article %>

<div class="clear">
  <div class="column span-6">
    <% unless @comments.empty? %>
        <div id="comments">
          <h2 id="comments_count"><%= pluralize(@comments.size, 'Comment') %></h2>
          <ul>
            <%= render :partial => "comment", :collection => @comments %>
          </ul>
        <!-- /comments -->
        </div>
    <% end %>

    <div id="add-comment">
      <h2>Add a comment</h2>
      <%= render :partial => "comments/form",
                 :locals => {:article => @article,
                             :comment => @comment,
                             :button_value => "Create Comment"} %>
    <!-- /add-comment -->
    </div>
  <!-- /column -->
  </div>
<!-- /clear -->
</div>

Aside from a few tags and CSS ids and classes to give it a proper look and feel, the juicy parts that you are going to analyze in detail are highlighted.

You first want to display all the comments. Initially you verify if there are any comments so far with unless @comments.empty?. Notice that you can't simply say unless @comments, because [] is still considered as true in Ruby.

Then you add an h2 tag that displays the number of existing comments. Again, pluralize is used to ensure that you don't get "1 comments" in order not to irritate the most obsessive-compulsive readers.

You then proceed to render a partial:

<%= render :partial => "comment", :collection >>= @comments %>

The partial will contain the code for displaying a comment. The :collection option is used so that the partial is rendered for each comment in @comments. The name of the partial that's indicated is comment and you are going to create it in the articles folder in a moment.

Note that you can't use render :partial => @comments, because otherwise ActionView would attempt to render a nonexisting comments/_comment.html.erb partial. Using :partial =>"comment" indicates that you want to render the _comment.html.erb partial defined within the same folder (that is, articles) as the template invoking render (that is, appviewsarticlesshow.html.erb).

Go ahead and create appviewsarticles\_comment.html.erb and add the following code to it:

<li>
    <emphasis role="strong"><%=h comment.name %></emphasis><![CDATA[ wrote:
    <div class="entry">
        <%= sanitize comment.body, :tags => %w{strong b em i a p br} %>
    </div>
</li>

For each comment, you display its author name (escaped for security reasons) and its body. You don't want angry readers, so their email address is not displayed. Notice that you sanitize the comment by allowing only the strong, b, em, i, a, p, and br tags. All other tags will be stripped from their attributes and HTML encoded.

The :attributes option exists as well to specify allowed attributes. If the sanitizing rules are consistent across the application, you can define them in the configuration of the application. Consult the documentation for sanitize for examples of this.

Back in Listing 6-3, the last highlighted bit is:

<%= render :partial => "comments/form",
           :locals => {:article => @article,
                       :comment => @comment,
                       :button_value => "Create Comment"} %>

With this you tell Rails to render the _form.html.erb partial defined in the comments folder and pass to it @article, @comment, as well as the label for the button. You are essentially using the partial that you defined before, in the same way as you rendered it in appviewscomments ew.html.erb. The only difference is that before you could simply say :partial => "form" because the partial was located in the same folder.

If you direct your browser to http://localhost:3000/articles/4 (or use an id for an article that you actually have), you will see a form at the bottom for adding new comments, as shown in Figure 6-6..

Figure 6.6. Figure 6-6

This form may be excessively large for comments. Let's make it smaller by modifying the existing form in appviewscomments\_form.html.erb to include a :rows option for text_area:

<%= f.text_area :body, :rows => 10 %>

:column, :size, and :disabled are also supported options. As usual, check the online documentation for examples.

Save, reload the page, and you should see a smaller box. Now go ahead and create one or two comments. In doing so, you'll see that these are displayed as well, as shown in Figure 6-7.

Figure 6.7. Figure 6-7

Let's also add a link to the "scaffold admin" at the bottom, as shown in Figure 6-8. This way the blog owner will be able to edit and destroy comments. Edit appviewsarticlesshow.html.erb and add the highlighted line:

<!-- /add-comment -->
    </div>
<%= link_to "Edit comments", article_comments_path(@article) %>
<!-- /column -->

NOTE

Notice that for sake of simplicity, we kept the scaffold interface as it is. In the real world, you'd probably want to create a more elaborate small admin interface or ditch the scaffold UI entirely, and change the code to present an Edit and Delete link next to a comment, when the blog author is logged in.

Figure 6.8. Figure 6-8

When you click that "Edit comments" link, you are redirected to http://localhost:3000/articles/4/comments where you will see the scaffold interface for the comments, as shown in Figure 6-9.

Figure 6.9. Figure 6-9

Feel free to click "Back to the article" and add further comments in the page that shows the article, to see how these are displayed between the article and the new comment form.

Having arrived at this point, you can safely take a break from the sample blog application to have a closer look at a few other aspects of the Rails framework.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.247.11