Chapter 5. Nested resources

This chapter covers

  • Building a nested resource
  • Declaring data associations between two database tables
  • Working with objects within an association

With the project resource CRUD done, the next step is to set up the ability to create tickets within the scope of a given project. The term for performing actions for objects within the scope of another object is nesting. This chapter explores how to set up nested routing for Ticket resources by creating a CRUD interface for them, scoped underneath the projects resource that you just created.

5.1. Creating tickets

To add the functionality to create tickets underneath the projects, you first develop the Cucumber features and then implement the code required to make them pass. Nesting one resource under another involves additional routing, working with associations in Active Record, and using more before_filters. Let’s get into this.

To create tickets for your application, you need an idea of what you’re going to implement. You want to create tickets only for particular projects, so you need a New Ticket link on a project’s show page. The link must lead to a form where a title and a description for your ticket can be entered, and the form needs a button that submits it to a create action in your controller. You also want to ensure the data is valid, just as you did with the Project model. Start by using the code from the following listing in a new file.

Listing 5.1. features/creating_tickets.feature
Feature: Creating Tickets
In order to create tickets for projects
As a user
I want to be able to select a project and do that

Background:
  Given there is a project called "Internet Explorer"
  And I am on the homepage
  When I follow "Internet Explorer"
  And I follow "New Ticket"

Scenario: Creating a ticket
  When I fill in "Title" with "Non-standards compliance"
  And I fill in "Description" with "My pages are ugly!"
  And I press "Create Ticket"
  Then I should see "Ticket has been created."

Scenario: Creating a ticket without valid attributes fails
  When I press "Create Ticket"
  Then I should see "Ticket has not been created."
  And I should see "Title can't be blank"
  And I should see "Description can't be blank"

When you run the code in listing 5.1 using the bin/cucumber features/creating_tickets.feature command, your background fails, as shown here:

And I follow "New Ticket"
no link with title, id or text 'New Ticket' found ...

You need to add this New Ticket link to the app/views/projects/show.html.erb template. Add it underneath the Delete Project link, as shown in the following listing.

Listing 5.2. app/views/projects/show.html.erb
<%= link_to "New Ticket", new_project_ticket_path(@project) %>

This helper is called a nested routing helper and is just like the standard routing helper. The similarities and differences between the two are explained in the next section.

5.1.1. Nested routing helpers

In listing 5.2, you used a nested routing helper—new_project_ticket_path—rather than a standard routing helper such as new_ticket_path because you want to create a new ticket for a given project. Both helpers work in a similar fashion, except the nested routing helper takes one argument always, the @project object for which you want to create a new ticket: the object that you’re nested inside. The route to any ticket URL is always scoped by /projects/:id in your application. This helper and its brethren are defined by changing this line in config/routes.rb

resources :projects

to the lines in the following listing.

Listing 5.3. config/routes.rb
resources :projects do
  resources :tickets
end

This code tells the routing for Rails that you have a tickets resource nested inside the projects resource. Effectively, any time you access a ticket resource, you access it within the scope of a project too. Just as the resources :projects method gave you helpers to use in controllers and views, this nested one gives you the helpers (where id represents the identifier of a resource) shown in table 5.1.

Table 5.1. Nested RESTful routing matchup

Route

Helper

/projects/:project_id/tickets project_tickets_path
/projects/:project_id/tickets/new new_project_ticket_path
/projects/:project_id/tickets/:id/edit edit_project_ticket_path
/projects/:project_id/tickets/:id project_ticket_path

As before, you can use the *_url alternatives to these helpers, such as project_tickets_url, to get the full URL if you so desire. The :project_id symbol here would normally be replaced by the project ID as well as the :id symbol, which would be replaced by a ticket’s ID.

In the left column are the routes that can be accessed, and in the right, the routing helper methods you can use to access them. Let’s make use of them by first creating your TicketsController.

5.1.2. Creating a tickets controller

Because you defined this route in your routes file, Capybara can now click the link in your feature and proceed before complaining about the missing TicketsController, spitting out an error followed by a stack trace:

And I follow "New Ticket"
      uninitialized constant TicketsController ...

Some guides may have you generate the model before you generate the controller, but the order in which you create them is not important. In Cucumber, you just follow the bouncing ball, and if Cucumber tells you it can’t find a controller, then you generate the controller it’s looking for next. Later, when you inevitably receive an error that it cannot find the Ticket model, as you did for the Project model, you generate that too.

To generate this controller and fix this uninitialized constant error, use this command:

rails g controller tickets

You may be able to pre-empt what’s going to happen next if you run Cucumber: it’ll complain of a missing new action that it’s trying to get to by clicking the New Ticket link. Open app/controllers/tickets_controller.rb and add the new action, shown in the following listing.

Listing 5.4. app/controllers/tickets_controller.rb
def new
  @ticket = @project.tickets.build
end

The build method simply instantiates a new record for the tickets association on the @project object, working in much the same way as the following code would:

Ticket.new(:project_id => @project.id)

Of course, you haven’t yet done anything to define the @project variable in TicketsController, so it would be nil. You must define the variable using a before_filter, just as you did in the ProjectsController. Put the following line just under the class definition in app/controllers/tickets_controller.rb:

before_filter :find_project

You don’t restrict the before_filter here: you want to have a @project to work with in all actions because the tickets resource is only accessible through a project. Underneath the new action, define the method that the before_filter uses:

private
  def find_project
    @project = Project.find(params[:project_id])
end

Where does params[:project_id] come from? It’s made available through the wonders of Rails’s routing, just as params[:id] was. It’s called project_id instead of id because you could (and later will) have a route that you want to pass through an ID for a ticket as well, and that would be params[:id]. Now how about that tickets method on your @project object? Let’s make sure it doesn’t already exist by running bin/cucumber features/creating_tickets.feature:

And I follow "New Ticket"
  undefined method 'tickets' for #<Project:0xb7461074> (NoMethodError)

No Rails magic here yet.

5.1.3. Defining a has_many association

The tickets method is defined by an association method in the Project class called has_many, which you can use as follows, putting it directly above the validation you put there earlier:

has_many :tickets

As mentioned before, this defines the tickets method you need but also gives you a whole slew of other useful methods, such as the build method, which you call on the association. The build method is equivalent to new for the Ticket class (which you create in a moment) but associates the new object instantly with the @project object by setting a foreign key called project_id automatically. Upon running the feature, you get this:

And I follow "New Ticket"
  uninitialized constant Project::Ticket (NameError)

You can determine from this output that the method is looking for the Ticket class, but why? The tickets method on Project objects is defined by the has_many call in the Project model. This method assumes that when you want to get the tickets, you actually want objects of the Ticket model. This model is currently missing; hence, the error. You can add this model now with the following command:

rails generate model ticket title:string description:text project:references

The project:references part defines an integer column for the tickets table called project_id in the migration. This column represents the project this ticket links to and is called a foreign key. You should now run the migration by using rake db:migrate and load the new schema into your test database by running rake db:test:prepare.

The rake db:migrate task runs the migrations and then dumps the structure of the database to a file called db/schema.rb. This structure allows you to restore your database using the rake db:schema:load task if you wish, which is better than running all the migrations on a large project again! The rake db:test:prepare task loads this schema into the test database, making the fields that were just made available on the development database by running the migration also now available on the test database.

Now when you run bin/cucumber features/creating_tickets.feature, you’re told the new template is missing:

And I follow "New Ticket"
Missing template tickets/new, application/new
  with {:handlers=>[:erb, :builder],
        :formats=>[:html],
        :locale=>[:en, :en]}.

  Searched in:
    * ".../ticketee/app/views"

A file seems to be missing! You must create this file in order to continue.

5.1.4. Creating tickets within a project

Create the file at app/views/tickets/new.html.erb, and put the following inside:

<h2>New Ticket</h2>
<%= render "form" %>

This template renders a form partial, which will be relative to the current folder and will be placed at app/views/tickets/_form.html.erb, using the code from the next listing.

Listing 5.5. app/views/tickets/_form.html.erb
<%= form_for [@project, @ticket] do |f| %>
  <%= f.error_messages %>
  <p>
    <%= f.label :title %><br>
    <%= f.text_field :title %>
  </p>
  <p>
    <%= f.label :description %><br>
    <%= f.text_area :description %>
  </p>
  <%= f.submit %>
<% end %>

Note that form_for is passed an array of objects rather than simply

<%= form_for @ticket do |f| %>

This code indicates to form_for that you want the form to post to the nested route you’re using. For the new action, this generate a route like /projects/1/tickets, and for the edit action, it generates a route like /projects/1/tickets/2. When you run bin/cucumber features/creating_tickets.feature again, you’re told the create action is missing:

And I press "Create Ticket"
  The action 'create' could not be found for TicketsController

To define this action, put it directly underneath the new action but before the private method:

Inside this action, you use redirect_to and specify an Array —the same array you used in the form_for earlier—containing a Project object and a Ticket object. Rails inspects any array passed to helpers, such as redirect_to and link_to, and determines what you mean from the values. For this particular case, Rails determine that you want this helper:

project_ticket_path(@project, @ticket)

Rails determines this helper because, at this stage, @project and @ticket are both objects that exist in the database, and you can therefore route to them. The route generated would be /projects/1/tickets/2 or something similar. Back in the form_for, @ticket was new, so the route happened to be /projects/1/tickets.

You could have been explicit and specifically used project_ticket_path in the action, but using an array is DRYer.

When you run bin/cucumber features/creating_tickets.feature, both scenarios report the same error:

And I press "Create Ticket"
  The action 'show' could not be found TicketsController

Therefore, you must create a show action for the TicketsController, but when you do so, you’ll need to find tickets only for the given project.

5.1.5. Finding tickets scoped by project

Currently, the first scenario is correct, but the second one is not.

Of course, now you must define the show action for your controller, but you can anticipate that you’ll need to find a ticket for the edit, update, and destroy actions too and pre-empt those errors. You can also make this a before_filter, just as you did in the ProjectsController with the find_project method. You define this finder underneath the find_project method in the TicketsController:

def find_ticket
  @ticket = @project.tickets.find(params[:id])
end

find is yet another association method provided by Rails when you declared that your Project model has_many :tickets. This code attempts to find tickets only within the scope of the project. Put the before_filter at the top of your class, just underneath the one to find the project:

before_filter :find_project
before_filter :find_ticket, :only => [:show,
                                      :edit,
                                      :update,
                                      :destroy]

The sequence here is important because you want to find the @project before you go looking for tickets for it. With this before_filter in place, create an empty show action in your controller to show that it responds to this action:

def show
end

Then create the view for this action at app/views/tickets/show.html.erb using this code:

<div id='ticket'>
  <h2><%= @ticket.title %></h2>
  <%= simple_format(@ticket.description) %>
</div>

The new method, simple_format, converts the line breaks[1] entered into the description field into HTML break tags (<br>) so that the description renders exactly how the user intends it to.

1 Line breaks are represented as and in strings in Ruby rather than as visible line breaks.

Based solely on the changes that you’ve made so far, your first scenario should be passing. Let’s see with a quick run of bin/cucumber features/creating_tickets.feature:

Then I should see "Ticket has been created."
...
2 scenarios (1 failed, 1 passed)
16 steps (1 failed, 2 skipped, 13 passed)

This means that you’ve got the first scenario under control and that users of your application can create tickets within a project. Next, you need to add validations to the Ticket model to get the second scenario to pass.

5.1.6. Ticket validations

The second scenario fails because the @ticket that it saves is valid, at least according to your application in its current state:

expected there to be content "Ticket has not been created" in "[text]"

You need to ensure that when somebody enters a ticket into the application, the title and description attributes are filled in. To do this, define the following validations inside the Ticket model.

Listing 5.6. app/models/ticket.rb
validates :title, :presence => true
validates :description, :presence => true

Now when you run bin/cucumber features/creating_tickets.feature, the entire feature passes:

2 scenarios (2 passed)
16 steps (16 passed)

Before we wrap up here, let’s add one more scenario to ensure that what is entered into the description field is longer than 10 characters. You want the descriptions to be useful! Let’s add this scenario to the features/creating_tickets.feature file:

Scenario: Description must be longer than 10 characters
  When I fill in "Title" with "Non-standards compliance"
  And I fill in "Description" with "it sucks"

 

Alternative validation syntax

You can use a slightly differently named method call to accomplish the same thing here:

validates_presence_of :title
validates_presence_of :description

Some people prefer this syntax because it’s been around for a couple of years; others prefer the newer style. It’s up to you which to choose. A number of other validates_* methods are available.

 

And I press "Create Ticket"
Then I should see "Ticket has not been created."
And I should see "Description is too short"

The final line here is written this way because you do not know what the validation message is, but you’ll find out later. To implement this scenario, add another option to the end of the validation for the description in your Ticket model, as shown in the following listing.

Listing 5.7. app/models/ticket.rb
validates :description, :presence => true,
                        :length => { :minimum => 10 }

If you go into rails console and try to create a new Ticket object by using create!, you can get the full text for your error:

irb(main):001:0> Ticket.create!
ActiveRecord::RecordInvalid: ... Description is too short
(minimum is 10 characters)

That is the precise error message you are looking for in your feature. When you run bin/cucumber features/creating_tickets.feature again, you see that all three scenarios are now passing:

3 scenarios (3 passed)
25 steps (25 passed)

You should ensure that the rest of the project still works. Because you have both features and specs, you should run the following command to check everything:

rake cucumber:ok spec

The summary for these two tasks together is[2]

2 The summary is altered to cut down on line noise with only the important parts shown.

9 scenarios (9 passed)
62 steps (62 passed)
# and
5 examples, 0 failures, 4 pending

Great! Everything’s still working. Push the changes!

git add .
git commit -m "Implemented creating tickets for a project"
git push

This section covered how to create tickets and link them to a specific project through the foreign key called project_id on records in the tickets table.

The next section shows how easily you can list tickets for individual projects.

5.2. Viewing tickets

Now that you have the ability to create tickets, you use the show action to create the functionality to view them individually.

When displaying a list of projects, you use the index action of the ProjectsController. For tickets, however, you use the show action because this page is currently not being used for anything else in particular. To test it, put a new feature at features/viewing_tickets.feature using the code from the following listing.

Listing 5.8. features/viewing_tickets.feature

Quite the long feature! We’ll go through it piece by piece in just a moment. First, let’s examine the within usage in your scenario. Rather than checking the entire page for content, this step checks the specific element using Cascading Style Sheets (CSS) selectors. The #ticket prefix finds all elements with an ID of ticket that contain an h2 element with the content you specified. This content should appear inside the specified tag only when you’re on the ticket page, so this is a great way to make sure that you’re on the right page and that the page is displaying relevant information.

The first step passes because you defined it previously; the next one is undefined. Let’s see this by running bin/cucumber features/viewing_tickets.feature:

Undefined step: "that project has a ticket:" (Cucumber::Undefined)

The bottom of the output tells you how to define the step:

Given /^that project has a ticket:$/ do |table|
# table is a Cucumber::Ast::Table
pending # express the regexp above with the code you wish you had
end

This step in the scenario is defined using the following syntax:

| title          | description                   |
| Make it shiny! | Gradients! Starbursts! Oh my! |

For Cucumber, this syntax represents a table, which is what the step definition hints at. Using the code shown in the following listing, define this step inside a new file at features/step_definitions/ticket_steps.rb.

Listing 5.9. features/step_definitions/ticket_steps.rb
Given /^that project has a ticket:$/ do |table|
  table.hashes.each do |attributes|
    @project.tickets.create!(attributes)
  end
end

Because you used a table here, Cucumber provides a hashes method for the table object, which uses the first row in the table as keys and the rest of the rows (as many as you need) for the values of hashes stored in an array. In this step, you iterate through this array, and each hash represents the attributes for the tickets you want to create.

One thing that you haven’t done yet is define the @project variable used inside this iterator. To do that, open features/step_definitions/project_steps.rb and change this line

Factory(:project, :name => name)

to the following:

@project = Factory(:project, :name => name)

Instance variables are available throughout the scenario in Cucumber, so if you define one in one step, you may use it in the following steps. If you run the feature again, you see that it can’t find the text for the first ticket because you’re not displaying any tickets on the show template yet:

expected there to be content "Make it shiny!" in "[text]"

5.2.1. Listing tickets

To display a ticket on the show template, you can iterate through the project’s tickets by using the tickets method, made available by the has_many :tickets call in your model. Put this code at the bottom of app/views/projects/show.html.erb, as shown in the next listing.

Listing 5.10. app/views/projects/show.html.erb
<ul id='tickets'>
  <% @project.tickets.each do |ticket| %>
    <li>
      #<%= ticket.id %> - <%= link_to ticket.title, [@project, ticket] %>
    </li>
  <% end %>
</ul>

 

Tip

If you use a @ticket variable in place of the ticket variable in the link_to’s second argument, it will be nil. You haven’t initialized the @ticket variable at this point, and uninitialized instance variables are nil by default. If @ticket rather than the correct ticket is passed in here, the URL generated will be a projects URL, such as /projects/1, rather than the correct /projects/1/tickets/2.

 

Here you iterate over the items in @project.tickets using the each method, which does the iterating for you, assigning each item to a ticket variable inside the block. The code inside this block runs for every single ticket. When you run bin/cucumber features/viewing_tickets.feature, you get this error:

When I follow "Ticketee"
    no link with title, id or text 'Ticketee' found

The reasoning behind wanting this not-yet-existing link is that when users click it, it takes them back to the homepage, which is where you want to go in your feature to get back to the projects listing. To add this link, put it into app/views/layouts/application.html.erb (as shown in the following listing) so that it’s available on every page, just above the <%= yield %>.

Listing 5.11. app/views/layouts/application.html.erb
<h1><%= link_to "Ticketee", root_path %></h1>
<%= yield %>

The call to yield should be used only once. If you put <%= yield %> then the content of the page would be rendered twice.

The root_path method is made available by the call to the root method in config/routes.rb. This simply outputs / when it’s called, providing a path to the root of your application.

Running bin/cucumber features/viewing_tickets.feature again, you can see this is all working:

1 scenario (1 passed)
18 steps (18 passed)

Your code expressly states that inside the TextMate 2 project, you should see only the "Make it shiny!" ticket, and inside the Internet Explorer project, you should see only the "Standards compliance" ticket. Both statements worked.

Time to make sure everything else is still working by running rake cucumber:ok spec. You should see that everything is green:

10 scenarios (10 passed)
80 steps (80 passed)
# and
5 examples, 0 failures, 4 pending

Fantastic! Push!

git add .
git commit -m "Implemented features for displaying a list of relevant
               tickets for projects and viewing particular tickets"
git push

Now you can see tickets just for a particular project, but what happens when a project is deleted? The tickets for that project would not be deleted automatically. To implement this behavior, you can pass some options to the has_many association, which will delete the tickets when a project is deleted.

5.2.2. Culling tickets

When a project is deleted, its tickets become useless: they’re inaccessible because of how you defined their routes. Therefore, when you delete a project, you should also delete the tickets for that project, and you can do that by using the :dependent option on the has_many association defined in your Project model.

This option has three choices that all act slightly differently from each other. The first one is the :destroy value:

has_many :tickets, :dependent => :destroy

If you put this in your Project model, any time you call destroy on a Project object, Rails iterates through each ticket for this project and calls destroy on them, then calls any destroy callbacks (such as any has_manys in the Ticket model, which also have the dependent option)[3] the ticket objects have on them, any destroy callbacks for those objects, and so on. The problem is that if you have a large number of tickets, destroy is called on each one, which will be slow.

3 Or any callback defined with after_destroy or before_destroy.

The solution is the second value for this option:

has_many :tickets, :dependent => :delete_all

This simply deletes all the tickets using a SQL delete, like this:

DELETE FROM tickets WHERE project_id = :project_id

This operation is quick and is exceptionally useful if you have a large number of tickets that don’t have callbacks. If you do have callbacks on Ticket for a destroy operation, then you should use the first option, :dependent => :destroy.

Finally, if you just want to disassociate tickets from a project and unset the project_id field, you can use this option:

has_many :tickets, :dependent => :nullify

When a project is deleted with this type of :dependent option defined, it will execute an SQL query such as this:

UPDATE tickets SET project_id = NULL WHERE project_id = :project_id

Rather than deleting the tickets, this option keeps them around, but their project_id fields are unset.

Using this option would be helpful, for example, if you were building a task-tracking application and instead of projects and tickets you had users and tasks. If you delete a user, you may want to reassign rather than delete the tasks associated with that user, in which case you’d use the :dependent => :nullify option instead.

In your projects and tickets scenario, though, you use :dependent => :destroy if you have callbacks to run on tickets when they’re destroyed or :dependent => :delete_all if you have no callbacks on tickets.

This was a little bit of a detour for the work you’re doing now, but it’s a nice thing to know if you ever need to delete an associated object when the original object is deleted.

Let’s look at how to edit the tickets in your application.

5.3. Editing tickets

You want users to be able to edit tickets, the updating part of this CRUD interface. This section covers creating the edit and update actions for the TicketsController.

The next feature you’re going to implement is the ability to edit tickets. This functionality follows a thread similar to the projects edit feature where you follow an Edit link in the show template. With that in mind, you can write this feature using the code in the following listing and put it in a file at features/editing_tickets.feature.

Listing 5.12. features/editing_tickets.feature
Feature: Editing tickets
  In order to alter ticket information
  As a user
  I want a form to edit the tickets
Background:
  Given there is a project called "TextMate 2"
  And that project has a ticket:
    | title           | description                   |
    |  Make it shiny! | Gradients! Starbursts! Oh my! |
  Given I am on the homepage
  When I follow "TextMate 2"
  And I follow "Make it shiny!"
  When I follow "Edit Ticket"

Scenario: Updating a ticket
  When I fill in "Title" with "Make it really shiny!"
  And I press "Update Ticket"
  Then I should see "Ticket has been updated."
  And I should see "Make it really shiny!" within "#ticket h2"
  But I should not see "Make it shiny!"

Scenario: Updating a ticket with invalid information
  When I fill in "Title" with ""
  And I press "Update Ticket"
  Then I should see "Ticket has not been updated."

When you run this feature using bin/cucumber features/editing_tickets.feature, the first two steps pass, but the third fails:

When I follow "Edit"
no link with title, id or text 'Edit Ticket' found

To fix this, add the Edit Ticket link to the TicketsController’s show template, because that’s where you’ve navigated to in your feature. Put it on the line underneath the <h2> tag in app/views/tickets/show.html.erb:

<%= link_to "Edit Ticket", [:edit, @project, @ticket] %>

Here is yet another use of the Array argument passed to the link_to method, but rather than passing all Active Record objects, you pass a Symbol first. Rails, yet again, works out from this Array what route you wish to follow. Rails interprets this array to mean the edit_project_ticket_path method, which is called like this:

edit_project_ticket_path(@project, @ticket)

Now that you have an Edit Project link, you need to add the edit action to the TicketsController.

5.3.1. Adding the edit action

The next logical step is to define the edit action in your TicketsController, which you can leave empty because the find_ticket before filter does all the hard lifting for you (shown in the following listing).

Listing 5.13. app/controllers/tickets_controller.rb
def edit

end

Again, you’re defining the action here so that anybody coming through and reading your TicketsController class knows that this controller responds to this action. It’s the first place people will go to determine what the controller does, because it is the controller.

The next logical step is to create the view for this action. Put it at app/views/tickets/edit.html.erb and fill it with this content:

<h2>Editing a ticket in <%= @project.name %></h2>
<%= render "form" %>

Here you re-use the form partial you created for the new action, which is handy. The form_for knows which action to go to. If you run the feature command here, you’re told the update action is missing:

And I press "Update"
  The action 'update' could not be found TicketsController

5.3.2. Adding the update action

You should now define the update action in your TicketsController, as shown in the following listing.

Listing 5.14. app/controllers/tickets_controller.rb
def update
  if @ticket.update_attributes(params[:ticket])
    flash[:notice] = "Ticket has been updated."
    redirect_to [@project, @ticket]
  else
    flash[:alert] = "Ticket has not been updated."
    render :action => "edit"
  end
end

Remember that in this action you don’t have to find the @ticket or @project objects because a before_filter does it for the show, edit, update, and destroy actions. With this single action implemented, both scenarios in your ticket-editing feature pass:

2 scenarios (2 passed)
20 steps (20 passed)

Now check to see if everything works:

12 scenarios (12 passed)
100 steps (100 passed)
# and
5 examples, 0 failures, 4 pending

Great! Let’s commit and push that:

git add .
git commit -m "Implemented edit action for the tickets controller"
git push

In this section, you implemented edit and update for the TicketsController by using the scoped finders and some familiar methods, such as update_attributes. You’ve got one more part to go: deletion.

5.4. Deleting tickets

We now reach the final story for this nested resource, the deletion of tickets. As with some of the other actions in this chapter, this story doesn’t differ from what you used in the ProjectsController, except you’ll change the name project to ticket for your variables and flash[:notice]. It’s good to have the reinforcement of the techniques previously used: practice makes perfect.

Let’s use the code from the following listing to write a new feature in features/deleting_tickets.feature.

Listing 5.15. features/deleting_tickets.feature
Feature: Deleting tickets
  In order to remove tickets
  As a user
  I want to press a button and make them disappear

  Background:
    Given there is a project called "TextMate 2"
    And that project has a ticket:
      | title           | description                   |
      |  Make it shiny! | Gradients! Starbursts! Oh my! |
    Given I am on the homepage
    When I follow "TextMate 2"
    And I follow "Make it shiny!"

  Scenario: Deleting a ticket
    When I follow "Delete Ticket"
    Then I should see "Ticket has been deleted."
    And I should be on the project page for "TextMate 2"

When you run this using bin/cucumber features/deleting_tickets.feature, the first step fails because you don’t yet have a Delete Ticket link on the show template for tickets:

When I follow "Delete Ticket"
no link with title, id or text 'Delete Ticket' found (Capybara::ElementNotFound)

You can add the Delete Ticket link to the app/views/tickets/show.html.erb file just under the Edit link (shown in the next listing), exactly as you did with projects.

Listing 5.16. app/views/tickets/show.html.erb
<%= link_to "Delete Ticket", [@project, @ticket], :method => :delete,
             :confirm => "Are you sure you want to delete this ticket?" %>

The :method => :delete is specified again, turning the request into one headed for the destroy action in the controller. Without this :method option, you’d be off to the show action because the link defaults to the GET method. Upon running bin/cucumber features/deleting_tickets.feature, you’re told a destroy action is missing:

When I follow "Delete Ticket"
The action 'destroy' could not be found in TicketsController

The next step must be to define this action, right? Open app/controllers/tickets_controller.rb, and define it directly under the update action:

def destroy
  @ticket.destroy
  flash[:notice] = "Ticket has been deleted."
  redirect_to @project
end

With that done, your feature should now pass:

1 scenario (1 passed)
8 steps (8 passed)

Yet again, check to see that everything is still going as well as it should by using rake cucumber:ok spec. If it is, you should see output similar to this:

13 scenarios (13 passed)
108 steps (108 passed)
and
5 examples, 0 failures, 4 pending

Commit and push!

git add .
git commit -m "Implemented deleting tickets feature"
git push

You’ve now completely created another CRUD interface, this time for the tickets resource. This resource is accessible only within the scope of a project, so you must request it using a URL such as /projects/1/tickets/2 rather than /tickets/2.

5.5. Summary

In this chapter, you generated another controller, the TicketsController, which allows you to create records for your Ticket model that will end up in your tickets table. The difference between this controller and the ProjectsController is that the TicketsController is accessible only within the scope of an existing project because you used nested routing.

In this controller, you scoped the finds for the Ticket model by using the tickets association method provided by the association helper method has_many call in your Project model. has_many also provides the build method, which you used to begin to create new Ticket records that are scoped to a project.

In the next chapter, you learn how to let users sign up and sign in to your application using a gem called devise. You also implement a basic authorization for actions such as creating a project.

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

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