This article is based on Rails 3 in Action, to be published Fall 2011. It is being reproduced here by permission from Manning Publications. Manning publishes MEAP (Manning Early Access Program,) eBooks and pBooks. MEAPs are sold exclusively through Manning.com. All pBook purchases include free PDF, mobi and epub. When mobile formats become available all customers will be contacted and upgraded. Visit Manning.com for more information. [ Use promotional code ‘java40beat’ and get 40% discount on eBooks and pBooks ]
The Comments Controller
Introduction
In a ticket-tracking application, tickets aren’t just there to provide information of a particular problem or suggestion; rather, they’re there to provide the workflow for it. The general workflow of a ticket is that a user will file it and it’ll be classified as a “new” ticket. When the developers of the project look at this ticket and decide to work on it, they’ll switch the state on the ticket to “open” and once they’re done mark it as “resolved”. If a ticket needs more information on it then another state such as “needs more info”. A ticket could also be a duplicate of another ticket or it could be something that the developers determine isn’t worthwhile putting in. In cases such as this the ticket may be marked as “duplicate” or “invalid,” respectively.
Explanation
The point here is: tickets have a workflow, and that workflow revolves around state changes. We’ll allow the admin users of this application to add states, but not to delete them. The reason for this is if an admin were to delete a state that was used then we’d have no record of that state ever existing. It’s best if once states are created and used on a ticket that they can’t be deleted.
To track the states, we’d let users leave a comment. With a comment, users will be able to leave a text message about the ticket and may also elect to change the state of the ticket to something else by selecting it from a drop down box. However, not all users will be able to leave a comment and change the state. We will protect both creating a comment and changing the state. By the time we’re done with all of this, the users of our application will have the ability to add comments to our tickets.
In order for a comment form to have somewhere to post we need to generate the CommentsController. We can do this by running this command:
rails g controller comments
A create action in this controller will provide the receiving end for the comment form, so we should add this now. We’ll need to define two before_filters in this controller. The first is to ensure the user is signed in because we don’t want anonymous users creating comments for and another to find the Ticket object. This entire controller is shown in listing 1.
Listing 1 app/controllers/comments_controller.rb
class CommentsController < ApplicationController before_filter :authenticate_user! before_filter :find_ticket def create @comment = @ticket.comments.build(params[:comment].merge(:user => current_user)) if @comment.save flash[:notice] = "Comment has been created." redirect_to [@ticket.project, @ticket] else flash[:alert] = "Comment has not been created." render :template => "tickets/show" end end private def find_ticket @ticket = Ticket.find(params[:ticket_id]) end end #A redirect_to with array #B render :template
In this action we use the template option of render when our @comment.save returns false to render a template of another controller.
You would use the action option to render templates for the current controller. By doing this, the @ticket and @comment objects become available when the app/views/tickets/show.html.erb template is rendered.
If the object saves successfully, we redirect back to the ticket’s page by passing an Array argument to redirect_to, which redirects to a nested route similar to /projects/1/tickets/2.
By creating the controller, we’ve now got all the important parts needed to create comments. Let’s check this feature by running bundle exec cucumber features/creating_comments.feature. We’ll see that it’s able to create the comment but it’s unable to find the text within the #comments element on the page.
Then I should see “Added a comment!” within “#comments” scope ‘//*[@id = ‘comments’]’ not found on page (Capybara::ElementNotFound)
This is because we haven’t added the comments listing to the show template yet. Let’s do this by adding the code from listing 2 above the comment form.
Listing 2 app/views/tickets/show.html.erb
<h3>Comments</h3> <div id=‘comments’> <% if @ticket.comments.exists? %> <co id=‘ch09_191_1’ /> <%= render @ticket.comments.select(&:persisted?) %> <% else %> There are no comments for this ticket. <% end %> </div>
Here, we create the element our scenario requires: one with an id attribute of comments. In this we check if there are no comments by using the exists?. This will do a very light query similar to this to check if there are any comments:
SELECT “comments”.”id” FROM “comments” WHERE (“comments”.ticket_id = 1) LIMIT 1
It only selects the “id” column from the comments table and limits the result set to 1. We could use empty? here instead, but that would load the comments association in its entirety and then check to see if the array is empty. By using exists?, we stop this potential performance issue from cropping up.
Inside this div, if there are comments, we call render and pass it the argument of @ticket.comments and on the end of that call select on it.
We use select here because we don’t want to render the comment object we’re building for the form at the bottom of the page. If we left off the select, @ticket.comments would include this new object and render a blank comment box. When we call select on an array, we can pass it a block which it will evaluate on all objects inside that array and return any element which makes the block evaluate to anything that is not nil or false.
The argument we pass to select is called a Symbol-to-Proc and is a shorter way of writing this:
{ |x| x.persisted? }
This is a new syntax versions of Ruby = 1.8.7 and used to be in Active Support in Rails 2. It’s a handy way of writing a short block.
The persisted? method checks if an object is persisted in the database by checking if it has its id attribute set and will return true if that’s the case and false if not.
By using render in this form, Rails will render a partial for every single element in this collection and will try to locate the partial using the first object’s class name. Objects in this particular collection are of the Comment, so the partial Rails will try to find will be at app/views/comments/_comment.html.erb, but we don’t have this file right now. Let’s create it and fill it with the content from listing 3.
Listing 3 app/views/comments/_comment.html.erb
<%= div_for(comment) do %> <h4><%= comment.user %></h4> <%= simple_format(comment.text) %> <% end %>
Here we’ve used a new method, div_for. This method generates a div tag around the content in the block and also sets a class and id attribute based on the object passed in. In this instance, the div tag would be this:
<div id="comment_1" class="comment">
The class method from this tag is used to style our comments so that they will look like figure 1 when the styles from the stylesheet are applied.
Figure 1 A commentWith this partial now complete, when we run our feature again by running bundle exec cucumber features/creating_comments.feature, it will all be passing!
2 scenario (2 passed) 23 steps (23 passed)
Good to see. We have now got the base for users to be able to change the state of a ticket. Before we proceed further, we should make sure that everything is working as it should by running rake cucumber:ok spec and we should also commit our changes. When we run the tests we’ll see this output:
47 scenarios (47 passed) 534 steps (534 passed) # and 23 examples, 0 failures, 7 pending
Good stuff! Let’s only commit this now.
git add . git commit -m "Users can now leave comments on tickets"
Hey, we’ve got a new file here that begins with capybara in our commit output! This file is the file that was generated by Capybara to show us the page when we used the “Then show me the page” step. We should open our .gitignore file and now ignore this file by putting this line in the file:
capybara*
This will ignore all capybara files. Let’s remove all the capybara files now and amend this commit and push our changes now.
git rm capybara* git commit --amend -m "Users can now leave comments on tickets" git push
This newly amended commit will override our previous commit, and the push puts our code on GitHub, where it’s safe. With this push, we’ve completed the first half of our state select feature. We now have added the ability for users of our application to add comments.
Summary
We’ve enabled users to leave comments on tickets. This feature is useful because it provides a way for users of a project to have a discussion about a ticket and keep track of it.