5.13. Adding a Custom REST Action

At this point in time, any scheduled article or draft will be excluded from the homepage. You could edit them from the console or access them by placing their id in the URL, but this is far from convenient. What you can do is create a custom action called unpublished. Add the following action to your ArticlesController:

# GET /articles/unpublished
# GET /articles/unpublished.xml
def unpublished
  @articles = Article.unpublished.find(:all, :order => "published DESC, published_at DESC")

  respond_to do |format|
    format.html { render :action => "index" }
    format.xml  { render :xml => @articles }
  end
end

To retrieve the list of unpublished articles, you can chain the unpublished named scope with a finder that sorts them to display scheduled articles first and drafts second. Furthermore, you keep the inverse chronological order for each of the two "groups."

The template for the unpublished action would be identical to the one of the index action because they both display a bunch of articles, no matter what these are.

Truth be told, the destroy action redirects every HTML request to the index action. This means that if you delete an article from the "unpublished page" you'll be redirected back to the index action, not to unpublished. It's a minor nuisance that can be fixed by improving the redirecting logic inside the destroy action, to make it aware of the previous action (for example, index or unpublished). To do so, replace redirect_to(articles_url) in destroy with redirect_to(:back).

Rather than fostering repetition by copying the index.html.erb template into a unpublished.html.erb file, it's far nicer to simply render the template of the index action via render :action => :index.

NOTE

Unlike redirect_to, using render implies that only the index.html.erb template will be rendered and there is no risk that the index action will be executed, thereby assigning the wrong array of articles to the @articles variable.

If you try to access http://localhost:3000/articles/unpublished you'll get an ugly error message:

Couldn't find Article with ID=unpublished

The reason for this is that you are using a custom REST action that is not automatically mapped by map.resources :articles in config outes.rb. Therefore routing assumes that unpublished is the id of the record that you are looking for, not the name of an action.

You'll just have to plug the new action into the collection of REST actions within the routes file. Change it as follows:

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

Restart Mongrel, because you've modified the routes file, and visit http://localhost:3000/articles/unpublished again. This time you should see a list of scheduled and drafted articles. Likewise, visiting http://localhost:3000/articles/unpublished.xml will return the same articles (encoded in XML format) to you.

Beautiful! Now that it works, you can add a link (the highlighted line) in the articles.html.erb layout:

<li><%= link_to 'Home', root_path %></li>
      <li><%= link_to 'Unpublished Articles', unpublished_articles_path %></
li><li><%= link_to 'New Article', new_article_path %></li>

Notice that when you added your custom REST action to the collection in the routes file, you essentially created a named route for it, so you can also use the handy "url" and "path" helper methods (for example, unpublished_articles_path). If you reload the homepage, you should now see that the new menu bar includes a working link to the unpublished articles.

When adding some form of authentication to the blog, it will be necessary to hide this link from the general public.

A final touch would be the ability to distinguish drafts from scheduled posts. You could, for example, provide this bit of information in the "About this article" section on the right side of the article's body. Instead of saying "Published on" all the time, you could distinguish and use "Scheduled for" and "This is still a draft." when appropriate.

5.13.1. Defining a Helper Method

One way to do this is by defining a status instance method in the Article model that returns a string indicating the status of the article object. You would then need to use this value to create a full string that includes the proper "for" or "on" preposition, plus the publication date and time in the view layer. If you take this approach, you could even think of formulating the whole string, as it needs to appear to the user, directly in the status method from within the model. This latter idea is a very bad one! You shouldn't include presentation logic in the model, because a distinct separation of concerns is king when it comes to Rails development.

A much more straightforward approach consists of simply defining a custom helper method that receives the article object as an argument and returns the desired string. You can define helper methods in the apphelpers directory of your project. If you take a look in that folder, you'll find two files, application_helpers.rb and articles_helpers.rb. The naming convention has the same logic as that of controllers and layouts. Methods defined in application_helpers.rb will be available site-wide, whereas those defined in articles_helpers.rb will be accessible only from the article templates.

In this case the helper method that you need is highly specific to articles, so go ahead and change your apphelpersarticles_helpers.rb file to look like this:

module ArticlesHelper
  def publication_status(article)
    publication_time = article.published_at.to_s(:long_ordinal)

    if article.published
      if article.published_at <= Time.now
        "Published on #{publication_time}"
      else
        "Scheduled for #{publication_time}"
      end
    else
      "This article is still a draft."
    end
  end
end

Notice how this uses the attributes of the article object passed as an argument to determine its status and in turn decides which string to return. In the case of a draft, it doesn't make much sense to publish the publication date, so the string literal "This article is still a draft." is returned.

Now you need to use the helper publication_status in both the index.html.erb and show.html.erb templates. Before doing that though, you need to make an important consideration. This isn't the first time that you had to change the code in both templates. When this happens, it's a surefire giveaway that the code could adhere more to the DRY principle by employing a partial template. Effectively, if you take a look at the code of index.html.erb and show.html.erb you'll notice that they are virtually the same, except that the template associated with the index action loops through the list of articles.

5.13.2. More about Partials

To solve this, create a partial in the appviewsarticles directory and call it _article.html.erb.

Place the following code, cut and pasted from the index.html.erb template, inside the partial you just created:

<div class="article clear">
    <h2><%= link_to h(article.title), article %></h2>
    <div  class="column span-6">
      <div class="entry">
        <%= textilize(article.body) %>
      </div>
    <!-- /column -->
    </div>

<div  class="column span-4 last">
    <div class="meta">
      <h3>About this article</h3>
      Published on <%= article.published_at.to_s(:long_ordinal) %>
    </div>

    <div class="tools">
      <h3>Tools</h3>
      <%= link_to 'Edit', edit_article_path(article) %>·
      <%= link_to 'Destroy', article, :confirm => 'Are you sure?', :method =>
:delete %>
    </div>
  <!-- /column -->
  </div>
<!-- /article -->
</div>

You can immediately modify it to use the publication_status helper you defined before. Replace the following line in the _article.html.erb partial:

Published on <%= article.published_at.to_s(:long_ordinal) %>

with:

<%= publication_status(article) %>

Now that you have created this partial, go ahead and remove the entire content of show.html.erb, replacing it with the following line:

<%= render :partial => @article %>

The render method is smart enough to figure out the partial name, inferring it from the name of the class of the object, @article. In our case, it will render _article.html.erb. That method also makes the object assigned to the @article instance variable available through the article local variable in the partial.

Some people prefer to avoid all this magic and opt instead to specify the name of the partial and the local variables explicitly. Another common option is :object, which passes the value assigned to it down to the local variable that matches the name of the partial. For example: render :partial =>"article", :object => @article will accomplish the same results as render :partial => @article, but in a less concise way. Be careful though, because render :partial =>"my_partial", :object => @article would assign the value of @article to the local variable my_partial, not article.

You made the show.html.erb template as DRY as possible, however you still need to address the index.html.erb one. The peculiarity of this template is that it loops through a collection of articles, so you'd have to render the partial you saw before (within a loop).

Thankfully, the render method is flexible and smart enough to infer from an @articles collection of Article objects assigned to :partial, that the partial _article.html.erb should be rendered once for each object in the array. Every time the partial is rendered, the current object in the array will be assigned to the local variable article in the partial. This syntax is much more concise. What's more, the local variable article_counter will be set to store the current position in the list of records (starting from 1, not 0).

Replace the entire contents of the index.html.erb file with the following:

<%= render :partial => @articles %>

Earlier you saw that :partial => @article could be rewritten using the :object option if you wanted to. Similarly, :partial => @articles could be rewritten using the :collection hash. For example, render :partial => "article", :collection => @articles is equivalent to render :partial => @articles, which you used in index.html.erb. Just like :object, :collection defines the local variable based on the name of the partial. In the case of collections, this can be overwritten with the option :as introduced in Rails 2.2.

Assuming you followed each step carefully, everything should be working now, just as planned, and the code that you've got in your hands here definitely applies the Don't Repeat Yourself principle. Figure 5-19 shows you the "Unpublished Articles" page for the articles I created on my machine (I highlighted the rendered output of the publication_status helper with a rectangle).

Figure 5.19. Figure 5-19

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

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