Many-to-Many: Connecting Students to Courses

The other frequent relationship between tables or models is many-to-many. A student, for example, can be taking zero or more courses, while a course can have zero or more students. (Students with zero courses might not yet have registered for anything, while courses with zero students might be awaiting registration or just unpopular.)

The relationship between the two is, from a modeling standpoint, even, so there won’t be any need for nested resources, just a lot of connections. As usual, it makes sense to move up from the database through models to controllers and views to produce the code in ch09/students003. And also as usual, while Rails provides you with a foundation, you’re still going to need to add a lot to that foundation.

Warning

Remember, don’t name a table “classes,” or you will have all kinds of strange Rails disasters because of name conflicts. “Courses” is a safer option.

Creating Tables

Building a many-to-many relationship requires creating tables—not just a single table, but a many-to-many relationship that will require adding two tables beyond the student table already in the application. One will be the actual course list, and the other the table that joins courses to students, as shown in Figure B-3 of Appendix B. Creating the course list—which will need a full set of scaffolding—is simple:

script/generate scaffold course name:string

Creating the join table requires an extra few steps. Start by creating a migration:

script/generate migration CreateCoursesStudents

Doing this will create a migration file in db/migrate with a name that ends in create_courses_students.rb. Unfortunately, when you open it, all you’ll see is:

class CreateCoursesStudents < ActiveRecord::Migration
  def self.up
  end

  def self.down
  end
end

Once again, you’ve reached the boundaries of what autogenerated code will do for you, for the present. Creating the connecting table will require coding the migration directly. A simple approach, building just on what you’ve seen in previous generated migrations, looks like:

class CreateCoursesStudents < ActiveRecord::Migration
  def self.up
    create_table :courses_students, :id => false do |t|
      t.integer :course_id, :null => false
      t.integer :student_id, :null => false
    end
  end

  def self.down
    drop_table :courses_students
  end
end

All of this depends on meeting Rails’ expectations for naming conventions. The table name is the combination of the two models being joined in alphabetical order, and the fields within the table are id values for each of the other models. Like all good migrations, it has a self.up method for creating the table and a self.down that removes it.

There is one performance-related issue to consider here. Rails has used the id value for tables as its main approach for getting data into and out of them rapidly. The id value, which you don’t have to specify, is automatically indexed. If you want your application to be able to move through the many course_id and student_id values in this table, however, you’ll need to add an index, as in:

class CreateCoursesStudents < ActiveRecord::Migration
  def self.up
    create_table :courses_students, :id => false do |t|
      t.integer :course_id, :null => false
      t.integer :student_id, :null => false
    end

    # Add index to speed up looking up the connection, and ensure
    # we only enrol a student into each course once
    add_index :courses_students, [:course_id, :student_id], :unique => true
  end

  def self.down
    remove_index :courses_students, :column => [:course_id, :student_id]
    drop_table :courses_students
  end
end

Indexes will be explained in greater detail in Chapter 13. Before moving on to the next steps, run rake db:migrate to build your tables.

Connecting the Models

Like has_many and belongs_to, has_and_belongs_to_many is a declaration that goes in the model. In app/models/student.rb, add:

# a student can be on many courses, a course can have many students
   has_and_belongs_to_many :courses

And in app/models/course.rb, add:

# a student can be on many courses, a course can have many students
   has_and_belongs_to_many :students

That’s all you need to do to establish the connection. Rails will automatically—thanks to naming conventions—use the courses_students table you built to keep track of the connections between students and courses.

You may find it useful to add some convenience methods to the model, depending on what you need in your interfaces. In the students model, it makes sense to add some logic that answers basic questions and returns some information that Rails won’t provide automatically. These build, of course, on the courses object that Rails did add to the model. First, a convenience method checks to see whether a given student is enrolled in a specified course:

def enrolled_in?(course)
    self.courses.include?(course)
end

The enrolled_in? method uses the include? method of courses to check whether a particular course is included in the list. If it is, then the student is enrolled, and include? and enrolled_in? will both return true. Otherwise, they return false.

Note

The enrolled_in? convenience method will get called many times as the number of courses grows, executing the same query repeatedly. For now, its clarity is probably more important than its performance, but as you get more familiar with how Rails interacts with databases, you will want to optimize this method for better performance.

A similarly useful convenience method returns the list of courses that a student is not yet enrolled in, making it easy to create logic and forms that will let them enroll:

def unenrolled_courses
    Course.find(:all) - self.courses
end

This one-liner does some tricky set arithmetic. First, it calls Course.find(:all) to get a full list of all the courses available. Then it calls self.courses to get a list of the courses that already apply to this particular student. Finally, it does subtraction—set subtraction—removing the courses in self.courses from the full list. The - doesn’t just have to mean subtracting one number from another.

Note

The has_and_belongs_to_many relationship is somewhat controversial, and some developers may prefer to use a has_many :through relationship, creating the intermediate table by hand.

Adding to the Controllers

Many-to-many relationships don’t demand the kinds of controller change that nested resources did. You don’t need to change method calls inside of the generated code, but you may want to add some additional methods to support functionality for both courses and students. While the added methods in the models focused on data manipulation, the methods in the controllers will add logic supporting interfaces to that data. The basic RESTful interfaces will remain, and the new interfaces will supplement them with some extra functionality specific to the combination of the two models.

In app/controllers/courses_controller.rb, the currently simple application only needs one extra method:

# GET /courses/1/roll
def roll
  @course = Course.find(params[:id])
end

The roll method, which will need a roll.html.erb view, will just provide a list of which students are in a given course, for roll call. The :id parameter will identify which course needs a list.

There’s more to add in app/controllers/students_controller.rb, as we need a way to add students to and remove them from courses. First, though, it makes sense to create a means of listing which courses a student is in:

# GET /students/1/courses
def courses
  @student = Student.find(params[:id])
  @courses = @student.courses
end

As the :get_student method did for awards, the courses method takes an id value given it by the routing and turns it into an object—in this case a pair of objects, representing a given student and the courses he or she is taking.

The next two methods are pretty different from the controller methods the book has shown so far. Instead of passing data to a view, they collect information from the routing and use it to manipulate the models, and then redirect the user to a more ordinary page with the result. The first, course_add, takes a student_id and a single course_id and adds the student to that course:

# POST /students/1/course_add?course_id=2
# (note no real query string, just
# convenient notation for parameters)

def course_add

  #Convert ids from routing to objects
  @student = Student.find(params[:id])
  @course = Course.find(params[:course])

  unless @student.enrolled_in?(@course)
    #add course to list using << operator
    @student.courses << @course
    flash[:notice] = 'Course was successfully added'
  else
    flash[:error] = 'Student was already enrolled'
  end
  redirect_to :action => :courses, :id => @student
end

The course_add method uses the enrolled_in? method defined earlier in the model to check if the student is already in the course. If not, it adds the appropriate course object to the list of courses for that student and reports that all went well using flash[:notice]. If the student was already enrolled, it blocks the enrollment and reports the problem using flash[:error]. Then it redirects to a list of courses for the student, which will show the flash message as well as the list.

The remove method, for demonstration purposes, is a little bit different. It accepts a list of courses to remove the student from. It then tests the list to see if the student was actually enrolled and deletes the record connecting the student to the course if so. It also logs the removal to the info log of the application, and then redirects to the same page as course_add, listing the courses for a student:

# POST /students/1/course_remove?courses[]=
def course_remove

  #Convert ids from routing to object
  @student = Student.find(params[:id])

  #get list of courses to remove from query string
  course_ids = params[:courses]

  unless course_ids.blank?
    course_ids.each do |course_id|
      course = Course.find(course_id)
      if @student.enrolled_in?(course)
        logger.info "Removing student from course #{course.id}"
        @student.courses.delete(course)
        flash[:notice] = 'Course was successfully deleted'
      end
    end
  end
  redirect_to :action => :courses, :id => @student
end

Adding Routing

Making those controllers work requires telling Rails that they exist and how they should be called. Again, Chapter 13 will explain routing in greater depth, but you can add extra methods to an existing REST resource through its :member named parameter. To add the roll method to the routing the scaffolding created, add a :member parameter to the line in config/routes.rb:

map.resources :courses, :member => { :roll => :get }

For students, there are more methods, so the arguments are a bit more complicated, though generally similar:

map.resources :students, :has_many => [ :awards ],
    :member => { :courses => :get, :course_add => :post,
     :course_remove => :post}

At this point, Rails knows how to find the extra methods. All that’s left is adding support for them to the views.

Supporting the Relationship Through Views

Cementing the relationship between students and courses requires giving users access to the functionality provided by the controllers and models. This can happen on several levels—application-wide navigation, showing counts in related views, and view support for the newly created controllers.

Establishing navigation

The views created by the scaffolding give basic access to both the students and the courses, but there’s no user-interface connection, or even a navigation connection, between them. A first step might add links to both the student pages and the course pages, letting users move between them. As this is moving toward navigation for the application and as it will get used across a lot of different pages, it makes sense to create a navigation partial for easy reuse.

To do that, create a new file, app/views/_navigation.html.erb. Its contents are simple, creating links to the main lists of students and courses:

<p>
  <%= link_to "Students", students_url %> |
  <%= link_to "Courses", courses_url %>
</p>

<hr />

You could reference this partial from every view, but that’s an inconvenient way to reference a partial that was meant to reduce the amount of repetition needed in the first place. Instead, add it to the layouts for each model in app/views/layouts. In each file, insert the boldfaced code below the body tag and above the paragraph for flash notices:

<body>

<%= render :partial => '/navigation' %>

<p style="color: green"><%= flash[:notice] %></p>

Every page in the application will now have links to the Students and Courses main index page, as shown in Figure 9-14.

Navigation links to Students and Courses

Figure 9-14. Navigation links to Students and Courses

Note

It may be neater to create a folder inside app/views/ to hold your partials, instead of just leaving them in app/views/.

Showing counts

The index page for students, app/views/students/index.html.erb, currently lists a count for awards, and you can add a count for courses the same way. You need to insert a heading, <th>Courses</th>, in the first tr element, just before <th>Awards</th>, and then insert:

<td><%=h student.courses.count %></td>

just before the count of awards. Figure 9-15 shows what this looks like, though the header names are abbreviated a bit to make the table fit better. Note that there aren’t any students in courses yet—the interface for adding them hasn’t yet been built.

Students list showing course counts

Figure 9-15. Students list showing course counts

Although the app/views/courses/index.html.erb file has less in it, you can add <th>Enrolled</th> in the first tr element and insert:

<td><%=h course.students.count %></td>

Figure 9-16 shows the courses list, which hasn’t been shown previously, though the RESTful interface made it easy to add the courses used in Appendix B.

Course list showing enrollment counts

Figure 9-16. Course list showing enrollment counts

Again, no one is registered for any courses yet, so adding that functionality is a natural next step.

Enrolling students in courses

The critical piece for connecting students to courses is, of course, the form for adding courses to students. That form could be linked from the main list if you wanted, but for now we’ll update the app/views/students/show.html.erb form so that it acts as the gateway to a student’s awards and courses. There are two pieces to this. First, add a list of courses, perhaps in place of the awards list:

<p>
  <b>Courses:</b>
  <% if @student.courses.count > 0 %>
    <%= @student.courses.collect {|c| link_to(c.name, c)}.join(", ")%>
  <% else %>
    Not enrolled on any courses yet.
  <% end %>
</p>

This is much more compact than the table of awards. The if checks to see whether the student is registered for any courses. If so, it builds a compact list of courses using the collect method. If not, it just says so.

Second, add a link in the cluster of link_to calls at the bottom of the file:

<%= link_to 'Edit', edit_student_path(@student) %> |
<%= link_to 'Courses', courses_student_path(@student) %> |
<%= link_to 'Awards', student_awards_path(@student) %> |
<%= link_to 'Back', students_path %>

Bear in mind that where the navigation partial called courses_url, this calls courses_student_path with a specific student. That will take the user to a page such as http://localhost:3000/students/1/courses—which hasn’t been created yet. To create that page, create a courses.html.erb file in the app/views/students directory. Example 9-4 shows one possible approach to creating a form for registering and unregistering students from courses.

Example 9-4. A courses.html.erb view for registering and removing students from courses

<h1><%= @student.name %>'s courses</h1>

<% if @courses.length > 0 %>
  <% form_tag(course_remove_student_path(@student)) do %>
  <table>
    <tr>
      <th>Course</th>
      <th>Remove?</th>
    </tr>
  <% for course in @courses do %>
    <tr>
      <td><%=h course.name %></td>
      <td><%= check_box_tag "courses[]", course.id %></td>
    </tr>
  <% end %>
  </table>
  <br />

  <%= submit_tag "Remove checked courses" %>
  <% end %>
<% else %>
<p>Not enrolled in any courses yet.</p>
<% end %>

<h2>Enroll in new course</h2>

<% if @student.courses.count < Course.count then %>
  <% form_tag(course_add_student_path(@student)) do %>
    <%= select_tag(:course,
      options_from_collection_for_select(@student.unenrolled_courses,
        :id, :name)) %>
    <%= submit_tag 'Enroll' %>
  <% end %>
<% else %>
  <p><%=h @student.name %> is enrolled in every course.</p>
<% end %>

<p><%=link_to "Back", @student %></p>

This view contains two forms. Unlike most of the previous forms, these are created with the form_tag rather than the form_for method because they aren’t bound to a particular model. The first form appears if the student is already enrolled in any courses, allowing the user to remove them from those courses. The second form appears if there are courses that the student hasn’t yet enrolled in. (More sophisticated program logic might set a different kind of limit.) Each of the forms connects to a controller method on students—course_remove for the first one and course_add for the second.

The form for removing courses uses a list of checkboxes generated from the list of courses, while the form for adding them uses the somewhat more opaque but very powerful options_from_collection_for_select method. This helper method takes a collection—here, the list of courses returned by @student.unenrolled_courses, and two values. The first, :id, is the value to return if a line in the select form is chosen, and the second, :name, is the value the user should see in the form.

Note

For information on the many more helper methods available for creating select lists, see FormHelper, FormTagHelper, and FormOptionsHelper in Appendix D.

Figure 9-17 shows the page before a student has registered for any courses, while Figure 9-18 shows the confirmation and removal options available once the student has signed up for their first course.

Adding courses, the first time around

Figure 9-17. Adding courses, the first time around

Adding or removing courses after a student has signed up

Figure 9-18. Adding or removing courses after a student has signed up

The checkboxes will create the parameters for course_remove and are a good choice when you want to operate on multiple objects at once. The select box is much slower and produces the results needed for the single-parameter course_add. You will, of course, want to choose interface components that match your users’ needs.

There’s one last component in need of finishing: the view that corresponds to the roll method on the courses controller. In app/views/courses/show.html.erb, add this link between the scaffolding’s link_to calls for Edit and Back:

<%= link_to 'Roll', roll_course_path(@course) %> |

That will add the link shown in Figure 9-19, which will let users get to the list of students.

A (very brief) course description with a link to the roll call list

Figure 9-19. A (very brief) course description with a link to the roll call list

The actual roll call list code, shown in Example 9-5, is another simple table.

Example 9-5. Generating a roll call list through the connections from courses to students

<h1>Roll for <%=h @course.name %></h1>

<% if @course.students.count > 0 %>
  <table>
    <tr>
      <th>Student</th>
      <th>GPA</th>
    </tr>
    <% for student in @course.students do %>
      <tr>
        <td><%=link_to student.name, student %></td>
        <td><%=h student.grade_point_average %></td>
      </tr>
    <% end %>
  </table>
<% else %>
  <p>No students are enrolled.</p>
<% end %>

<p><%= link_to "Back", @course %></p>

The list of students is accessible from the @course object that the roll method in the controller exposed. That method didn’t have anything specific to do with students, but because the students for the course are included in the course object, all of their information is available for display in the table, as shown in Figure 9-20. The links that link_to generated let you go directly to the student’s record, making it easy to modify students who are in a particular course.

A roll call that connects to records for students in the class

Figure 9-20. A roll call that connects to records for students in the class

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

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