In my last article, I showed you how to get started with your Rails application. The result of the simple commands,
rails blogdemo; cd blogdemo; ruby script/generate scaffold article title:string author:string content:text; rake db:migrate; ruby script/server was that you had your own simple blog up and running. The blog support articles each of which have a title, an author, and text content. This initial model was generated for us with no editing on our part.
In this second article, I will expand the blog to allow comments to our articles. This will require us to get our hands a bit more dirty with the Rails code, but it won’t be too bad. And even better: If you complete the instructions in this article, you can call yourself a Rails programmer!
Are you ready to dive in head first?
The initial structure
- Like in the example with the article model class, we create a comment model.
ruby script/generate scaffold comment article_id:integer title:string author:string body:text. Notice the article_id column, which will be our foreign key to the article.
- rake db:migrate db:fixtures:load test
- You can now see the comments at http://localhost:3000/comments/
Connecting the comments to articles
To get the articles and comments connected, we need to do four things. I will take you thought the process in detail.
- We have to update the routes for URLs to let Rails know how to connect articles to comments
- We have to update the model classes for the two classes, to let them know about the relationship
- We have to update the CommentsController to refer to the correct article when it finds, creates or modifies a comment.
- We have to include the information about the comments in the article views.
Here we go:
- Lets start with routing. The file
config/routes.rbtells us how our application URLs are structured. Remove
map.resource :comments, and add
commentsas a “nested route” instead:
map.resources :articles do |article| article.resources :comments, :name_prefix => nil end
This change means that you will find the comments like this: http://localhost:3000/articles/article-id/comments/
app/view/articles/show.html.erbso we can get the comments related to an article. Add the following code:
<%= link_to "Comments", comments_path(@article) %>
- http://localhost:3000/comments/ no longer works. Instead: Select an article, and click the new Comments link. This gives an error, because the routes we generated don’t take the relationship between articles and comments into consideration.
- The best way to fix this is to change the methods we use to find comments. We can do this by updating the view helper in
def comment_path(comment) url_for :controller => "comments", :action => "show", :article_id => comment.article, :id => comment end def edit_comment_path(comment) url_for :controller => "comments", :action => "edit", :article_id => comment.article, :id => comment end
- You now need to inform the model classes about each other via belongs_to and has_many associations. Update
class Comment < ActiveRecord::Base belongs_to :article end class Article < ActiveRecord::Base has_many :comments end
Updating the controller
If you click on the Comments link, you will now be rewarded with a seemingly meaningful set of comments for the current article. But appearances can be deceiving. We have still not made the controllers aware of the fact that only the comments for the current article should be displayed. If you click around in the application, it will soon become apparent that all comments are displayed under all articles! We need to update the controller to restrict to only comments for the current article.
app/controllers/comments_controller.rb, and make sure we always have an article available. This is usually done through a "before_filter". Add the following line to the beginning of the file:
before_filter :find_article, right after the
- Implement the filter. Add the following to the very end of the class file, just before the final "end":
protected def find_article @article = Article.find(params[:article_id]) end
- You can now replace every
newon the Comment class with similar code in the article model. Replace every place where the code says
@article.comments.find(...), and every place the code says
- We also need to let the controller know how to redirect to a created or updated view. Add the following right after the end of the find_article code above.
def comment_url(comment) url_for :controller => "comments", :action => "show", :article_id => comment.article, :id => comment end
- Finally, we should never let the user give
article_idin the input forms. To avoid this, you can add the line
At this stage, creating, viewing, updating and deleting comments should all work as expected. However, before we turn to the view, you might have noticed a strange thing with our test data. The comments generated by running
rake db:fixtures:load are not connected to the correct articles. The place to fix this is in
test/fixtures/comments.yml, and the way to fix it is a new feature with Rails 2.0:
test/fixtures/comments.yml, change the lines that read
article_id: 1 into
article: one. Regenerate the test data by running
rake db:fixtures:load. Both articles should now be attached to your first article.
Integrating the views
Finally, we want to improve the way all this looks. Polishing the view will be the topic of my next article, so for now, let's just mash the information together.
- We want to display all the comments in the details view for an article. Replace the link we created to the comments in
app/view/articles/show.html.erbwith the full contents of
- Still in
<% for comment in @comments %>with
<% for comment in @article.comments %<and
<%= link_to 'New comment', new_comment_path %>with
<%= link_to "New comment", new_comment_path(@article) %>. Your articles should now display the corresponding comments.
- We also want our users to be able to add new comments when they view the article. Still in
app/view/articles/show.html.erb, replace the "New comment" link with the full contents of
<% form_for(@comment) do |f| %>with
<% form_for :comment, :url => comments_path(@article) do |f| %>and remove the link
<%= link_to 'Back', comments_path %>
- If you look at the page for an article now, you will be rewarded with a functional, but ugly page that despite its appearance displays the article, all comments for the article, and allows the users to add new comments.
Fixing the tests
Before we leave, lets make sure that our tests still pass. Run
rake test. You tests should fail at this point. This is because the tests don't supply the article_id attribute CommentsController now requires.
- All post and get requests in
test/functional/comments_controller_test.rbneed have an argument
:article_id => articles(:one)added to them. For example:
get :index, :article_id => articles(:one). Do this for all the get and post requests in the test.
- One test still doesn't run. In
assert_redirected_to comment_path(assigns(:comment))should be changed to
assert_redirected_to comment_path(:id => assigns(:comment), :article_id => assigns(:article))
- The tests should now pass.
This more lengthy post takes you through all the steps that are needed to create a web application that displays a one-to-many relationship, namely that of one article having many comments. We had to update both the URL routing to make comments a nested resource, the model class to associate the comments and articles, the controller to only find comments on the current articles and the article views to include the comment data, but every change is fairly simple.
Until this point, I have assumed that you as a reader don't know much about Rails. After completing this article, you have touched quite a few of the core concepts of Rails already. If you found the ride too confusing because I didn't provide a lot of details about the way a Rails application works, I would very much like to know it.
The next article in this series will clean up the view and add AJAX support.
Stay tuned, we are just getting started!