Nesting Awards in Students

The connections between students and awards are workable, but the way that the two models are handled by the web application doesn’t reflect their actual roles in the data models. Depending on your application and your preferences, this may be perfectly acceptable. There is, however, a better way to represent the awards model that more clearly reflects its relationship to students, implemented in ch09/students002.

The models will stay the same, and the views will stay almost the same. The main things that will change are the routing and the controller logic. Chapter 13 will explain routing in much greater depth, but for now it’s worth exploring the ways that routing can reflect the relationships of your data models.

Note

If the work involved in creating a nested resource seems overwhelming, don’t worry. It’s not mandatory Rails practice, though it is certainly a best practice. Unfortunately, it’s just complicated enough that it’s hard to automate—but maybe someday this will all disappear into a friendlier script/generate command.

Changing the Routing

Near the top of the config/routes.rb file are the lines:

map.resources :awards

map.resources :students

Delete them, and replace them with:

map.resources :students, :has_many => [ :awards ]

It’s another has_many relationship, which is this time expressed as a parameter to map.resources. You don’t need to specify the belongs_to relationship. Yes, this is kind of a violation of “Don’t Repeat Yourself,” but at the same time it expresses a resource relationship, not simply a data model relationship.

You’ll still be able to visit http://localhost:3000/students/, but http://localhost:3000/awards/ will return an error. The routing support that the link_to methods expected when the original scaffolding was built has been demolished. The views in the app/views/awards directory are now visible only by going through students, and this change of position requires some changes to the views.

Instead of the old URLs, which looked like:

http://localhost:3000/awards/2

the URLs to awards now follow a more complicated route:

http://localhost:3000/students/3/awards/2

That added students/3 reflects that the award with the id of 2 belongs to the student with the id of 3.

Changing the Controller

While changing the routing is a one-line exercise, the impact on the controller is much more complicated. Most of it reflects the need to limit the awards to the specified student. Example 9-1 shows the new controller, with all changes bolded and commented. Most of the changes simply add the student object as context.

Example 9-1. Updating a controller to represent a nested resource

class AwardsController < ApplicationController

  before_filter :get_student
  # :get_student is defined at the bottom of the file,
  # and takes the student_id given by the routing and
  # converts it to an @student object.

  def index
    @awards = @student.awards
    # was @awards = Award.find(:all)

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

  def show
    @award = @student.awards.find(params[:id])
    # was Award.find(params[:id])

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

  def new
    @award = @student.awards.build
    # was @award = Award.new

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

  # GET /awards/1/edit
  def edit
    @award = @student.awards.find(params[:id])
    # was @award = Award.find(params[:id])
  end

  # POST /awards
  # POST /awards.xml
  def create
    @award = @student.awards.build(params[:award])
    # was @award = Award.new(params[:award])

    respond_to do |format|
      if @award.save
        flash[:notice] = 'Award was successfully created.'
        format.html { redirect_to([@student, @award]) }
                      # was redirect_to(@award)
        format.xml  { render :xml => @award, :status => :created, :location =>
@award }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @award.errors, :status =>
:unprocessable_entity }
      end
    end
  end

  def update
    @award = @student.awards.find(params[:id])
    # was @award = Award.find(params[:id])

    respond_to do |format|
      if @award.update_attributes(params[:award])
        flash[:notice] = 'Award was successfully updated.'
        format.html { redirect_to([@student, @award]) }
                      # was redirect_to(@award)
        format.xml  { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml  { render :xml => @award.errors, :status =>
:unprocessable_entity }
      end
    end
  end

  def destroy
    @award = @student.awards.find(params[:id])
    # was @award = Award.find(params[:id])
    @award.destroy

    respond_to do |format|
      format.html { redirect_to(student_awards_path(@student)) }
                    # was redirect_to(awards_url)
      format.xml  { head :ok }
    end
  end

  private
  # get_student converts the student_id given by the routing
  # into  an @student object, for use here and in the view.
  def get_student
    @student = Student.find(params[:student_id])
  end
end

Most of these changes, in some form or another, convert a reference to awards generally to a reference to an award that applies to a particular student. You’ll see some naming inconsistencies as that context forces different syntax: find(:all) simply disappears, new becomes build, and awards_url becomes student_awards_path. These new, different methods are created automatically by Rails thanks to the routing changes made earlier. Eventually these shifts will feel normal to you.

The new AwardsController uses one new technique. It starts with a before_filter, a call to code that will get executed before everything else does. In this case, the before_filter calls the get_student method, which helps reduce the amount of repetition in the controller. The controller will receive the student_id value from routing, taking from the URL. Practically all of the time, though, it makes more sense to work with the corresponding Student object. The get_student method takes the student_id and uses it to retrieve the matching object and place it in the @student variable. That simplifies the methods in the controller and will also be used in the views.

Note

It’s not hard to imagine a circumstance in which users want a complete list of awards and students. You can still provide one—it’s just an extra step beyond the nested resource, requiring its own routing, controller method, and view.

Changing the Award Views

If users visit the new URLs at this point, they’ll get some strange results. Rails routing originally defined one set of methods to support the old approach, and not only the results but also the method names and parameters need to change.

In the old version of app/views/awards/index.html.erb, the Show/Edit/Destroy links looked like Example 9-2, while the updated version looks like Example 9-3. Updates are marked in bold.

Example 9-2. Code for displaying awards before nesting by student

<h1>Listing awards</h1>

<table>
  <tr>
    <th>Name</th>
    <th>Year</th>
    <th>Student</th>
  </tr>

<% for award in @awards %>
  <tr>
    <td><%=h award.name %></td>
    <td><%=h award.year %></td>
    <td><%=h award.student.name %></td>
    <td><%= link_to 'Show', award %></td>
    <td><%= link_to 'Edit', edit_award_path(award) %></td>
    <td><%= link_to 'Destroy', award, :confirm => 'Are you sure?', :method =>
:delete %></td>
  </tr>
<% end %>
</table>

<br />

<%= link_to 'New award', new_award_path %>

Example 9-3. Displaying the awards on a student-by-student basis

<h1>Awards for <%=h @student.name %></h1>

<% if @student.awards.count > 0 %>
  <table>
    <tr>
      <th>Name</th>
      <th>Year</th>
    </tr>

  <% for award in @awards %>
    <tr>
      <td><%=h award.name %></td>
      <td><%=h award.year %></td>
      <td><%= link_to 'Show', [@student, award] %></td>
      <td><%= link_to 'Edit', edit_student_award_path(@student, award) %></td>
      <td><%= link_to 'Destroy', [@student, award], :confirm => 'Are you sure?',
:method => :delete %></td>
    </tr>
  <% end %>
  </table>

  <br />
<% else %>
  <p><%=h @student.given_name %> hasn't won any awards yet.</p>
<% end %>

<p>
  <%= link_to 'New award', new_student_award_path(@student) %> |
  <%= link_to 'Back', @student %>
</p>

In the new version, Example 9-3, the additional information about the student informs nearly every interaction. The headline (h1) has acquired the name of a specific student, rather than just being “Awards” generally. There’s extra logic—the if and else statements—to make sure that awards are only displayed for students who have awards, presenting a polite message for students without award.

The largest changes, however, are in the logic that creates links. The Show and Destroy links change arguments, from just award to [@student, award], reflecting the additional information link_to will need to create a proper link. The links for Edit and New Award call a different method, new_student_award_path, which will work through the nested resource routing to generate a link pointing to the right place. Given an argument for both a student and an award, it will generate a link to edit that award; given just a student argument, it will generate a link to create a new award for that student.

There’s also a new Back link that goes back to the student’s page. That’s completely new navigation, necessary because of the extra context this page now has. Figure 9-8 shows what all of this looks like for Jules Miller, with his two awards, while Figure 9-9 shows the result for Milletta Stim, who hasn’t won any yet.

The awards list, scoped to a particular student

Figure 9-8. The awards list, scoped to a particular student

The awards list, when the student doesn’t have any awards yet

Figure 9-9. The awards list, when the student doesn’t have any awards yet

The changes to show.html.erb are smaller, turning the links from:

<%= link_to 'Edit', edit_award_path(@award) %> |
<%= link_to 'Back', awards_path %>

to:

<%= link_to 'Edit', edit_student_award_path(@student, @award) %> |
<%= link_to 'Back', student_awards_path(@student) %>

The information displayed is the same, and context has little effect except on the links. Everything still looks like Example 9-3, except that the URL is different and you’d see a different link in the status bar if you rolled over Edit or Back.

There are also some minor changes to new.html.erb and edit.html.erb. Both of them get new headlines:

<h1>New award for <%=h @student.name %></h1>

and:

<h1>Editing award for <%=h @student.name %></h1>

Both of them change their form_for call from:

<% form_for(@award) do |f| %>

to:

<% form_for([@student, @award]) do |f| %>

Given an array of arguments instead of a single argument, form_for can automatically adjust to get the routing right for its data. The rest of the form fields look the same, except that the select call to create the picklist for students disappears completely, as that information comes from context.

And yet again, the links at the bottom change (though only the second line applies to new.html.erb):

<%= link_to 'Show', [@student, @award] %> |
<%= link_to 'Back', student_awards_path(@student) %>

Figure 9-10 shows the form for entering a new award in use, and Figure 9-11 shows the form for editing an existing award.

Entering a new award for a particular student

Figure 9-10. Entering a new award for a particular student

Editing an award—note the disappearance of the select box

Figure 9-11. Editing an award—note the disappearance of the select box

Connecting the Student Views

There’s one last set of things to address: adding links from the student views to the awards views. Awards used to have their own independent interface, but now they’re deeply dependent on students. There are only two places where adding links makes clear sense, though: in the index listing and in the view that shows each student.

In show.html.erb, add a link to the awards for the student between Edit and Back with:

<%= link_to 'Awards', student_awards_path(@student) %> |

As shown in Figure 9-12, that’ll give you a path to the awards for a student. (You might drop the existing list of awards there, too.)

Adding a link from a student to a student’s awards

Figure 9-12. Adding a link from a student to a student’s awards

That may actually be all the interface you want, but sometimes it’s easier to look at a list of students and click on an Awards button for them. To add that, you need to add a column to the table displayed in index.html.erb. Between the links for Edit and Destroy, add:

<td><%= link_to 'Awards', student_awards_path(student) %></td>

The result will look like Figure 9-13. If users click on the Awards links, that will bring them to pages like Figures 9-8 and 9-9.

Students listing with connection to awards for each

Figure 9-13. Students listing with connection to awards for each

Is Nesting Worth It?

Shifting awards from having their own interface to an interface subordinate to students was a lot of work. It’s fairly clear why nesting resources is the “right” approach in Rails—it makes the has_many/belongs_to relationship explicit on every level, not just in the model. The work in the routing and the controller establishes the changes necessary for both the regular web user interface and the RESTful web services interface to work this way. The views, unfortunately, take some additional effort to bring in line, and you may have had a few ideas of your own while reading this about how you’d like them to work.

In the abstract, nesting is a great idea, but at the same time, it requires a lot of careful work to implement correctly in the views layer. That work may or may not be your first priority, though if you’re going to nest resources, it’s easier done earlier in the implementation process rather than later.

If you’ve built nested resources, you may find situations where you need to build additional interfaces. Sometimes the supposedly subordinate model is the main one people want to work with. In the awards example, most of the time people might want to know what awards a student has received, or add an occasional award, and the nested interface will work just fine. However, if lots of awards are given out across an entire school at the end of the year, and one person has the task of entering every award into the system, that person might want a more direct interface rather than walking through the nesting. This situation could be addressed with an extra view that looked more like the ones earlier in the chapter.

Whether or not you decide to nest your own resources, you now have the information you need to do so, and you’ll know what you’re working with should you encounter Rails applications built using nested resources.

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

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