Lets begin with Lab 7: Tracker

This week we will start out somewhat differently than usual. We will all begin with the tracker app that we started last week:

$ cd tracker
$ git pull
$ git tag -a lab7

Next, we need to verify that the connection with the database still works. Run the command

$ rake db:version

If it returns a long version number (which is actually the date of the last lab), then it worked correctly. If it returns an error, then you have a configuration problem in config/database.yml that you need to fix.

This week we are going to modify our lab7 to include a many-to-many relationship. If you remember, lab7 was our simple attempt at Facebook: you can enter a list of people (your “friends”) and upload pictures of these people.

Today, we are going to do two things. First, we are going to complete the person resource by adding the ability to delete people. And second, we are going to add a many-to-many relationship: courses. This way you can tell what courses all your friends are in.

Deleting People

To delete people, we need to create a new action in the controller to do this. Traditionally, we name this action destroy:

  def destroy
    @person = Person.find params[:id]
    redirect_to people_url

This gives us the ability to destroy people. Next, we need to add a link to this action to our user interface, so the user can choose to do this. We can add our link anywhere, but right now I choose to add it to the index view. But, if you remember lab 7, we used a partial to render our index view, so the file we need to edit is app/views/people/_person.html.erb:

<% if person.picture.exists? %>
  <%= image_tag person.picture.url(:thumb) %>
<% end %>
<%= person.name %> (<%= link_to"View", person %>, <%= link_to "Edit", edit_person_url(person) %>, 
  <%= link_to "Delete", person_url(person), method: 'delete' %>)

Notice the new link_to line that I added. it links to the same URL as view, but uses a different method. It uses the DELETE method, which will tell the controller to use the destroy action rather than the show action.

Go and test this and see if it works.

Exercise: Add Support for Creating Courses

OK, this week’s exercise is to add a new resource, and all of the corresponding code (controller actions, view pages, etc.). Let’s add support for a new type of data: a course. As in, a course in school where you learn things and take tests… This is an exercise for you to create a new resource in the same application as your people resource. You should do basically the same things as you did for people:

You can do one new thing with courses. A normal string in the database is limited to 255 characters. This isn’t really enough for a course description. So we’ll call it a text column instead. Text fields can be as long as you need them to be. (They are just slower to access; that’s why we don’t make everything a text field. Also, you can’t sort on a text field.) To do this, in your migration, you use a line like this:

t.text :description

Also, in the form where the data about courses is inputted by the user, we should use a text area rather than a text field:

Course Description: <br />
<%= f.text_area :description, cols: 80, rows: 6 %><br />

Go ahead and do this. Make it so you have a user interface for adding and viewing courses. Run your server and test out your code to make sure it works. DO NOT move forward with the lab until you have it working; always test what you have written before trying to add more functionality.

(Also, you will notice that it is a pain to edit the URL to move around; don’t forget to add links back and forth between the people views and the courses views.)

Swap Drivers

Now, it is time to swap drivers, so the person that was navigating will now drive, and the person was was driving will become navigator and have a chance to think more about the lab. Don’t forget to git add ., git commit, and git push the code.

Registrations: A Many to Many Relationship

Remember from lecture that many-to-many relationships are done in Rails by creating a new model that has a one-to-many relationship with each of the two models. This often makes sense because an instance of a many-to-many relationship usually has a name. In our case, we want to create a many-to-many relationship between people and courses – person A is taking course 1. What is this relationship usually called? It is called a registration – person A has registered for course 1.

Using a Registration Resource

So, let’s create a new registration resource:

 $ rails generate resource registration person_id:integer course_id:integer

This line will generate a new model – which means a new model file in app/models and a new migration under db/migrate. Check the file under db/migrate to make sure that it has the correct fields. It should simply contain two fields: the ID of the a person, and the ID of a course. An entry in this table, therefore, signifies that a person is registered for a course. Don’t forget to rake db:migrate after verifying the migration.

Next, we need to add belongs_to and has_many commands to our models:

In app/models/person.rb:

has_many :registrations

In app/models/course.rb:

has_many :registrations

In app/models/registration.rb:

belongs_to :person
belongs_to :course

In addition, we can add two more helpful commands. Right now, we can access a Person’s registrations, but not his or her list of courses. So we can add one more command to make that work:

class Person < ActiveRecord::Base
  has_many :registrations
  has_many :courses, through: :registrations

And we can add the same command so that we can access the people who are in a course:

class Course < ActiveRecord::Base
  has_many :registrations
  has_many :people, through: :registrations

Adding a User Interface for Registering People for Courses

Now that we’ve configured the data structures – the database and the rails models – correctly, we can move on to the user interface.

There are a couple of ways to do this user interface. One method is to use the new controller – the registrations controller – and add your typical new and create actions. However, you already know how to do that. So we are going to do something slightly different, and more useful. We are going to add a “Register this student for a course” dropdown box on each student’s view page.

A dropdown is really a form with only one entry in the form. To create a form like this, we first need a blank registration object. Well, not really blank; we know what person we want to link the course to. So we can modify our show action in our people controller to create a new registration object that has the person filled in:

  def show
    @person = Person.find params[:id]
    @registration = @person.registrations.new

That last line – @person.registrations.new creates a new registration object associated with the person currently stored in the @person variable.

Next, we can create a form. Personally, I like to isolate these kinds of forms onto a partial so that we can use them anywhere we like. Let’s call this partial app/views/registrations/_form.html.erb:

<h4> Add a class for this person </h4>
<%= form_for(@registration) do |f| %>
  <%= f.hidden_field :person_id %>
  <%= f.collection_select :course_id, Course.all, :id, :name %>
  <%= f.submit %>
<% end %>

Notice that this form contains a field for BOTH of the columns in the registrations table. The first field is hidden (and therefore, not displayed to the user). The second field is a collection select dropdown box.

Next, we need to put this form somewhere on our current people/show view:

<%= render "registrations/form" %>

(Notice how I was able to render a partial from a different controller by specifying the controller and then a slash and then the form name (without the underscore).)

Test our the form display. Click on the “view” link for one of your people, and see if it displays a list of the possible courses to register for. (Of course, you need to make sure that some courses exist…) Now, when you click on the submit button, where will this submit to?

The answer is the registrations controller, create action. So let’s open up the registrations controller and write our fairly standard create action:

class RegistrationsController < ApplicationController
  def create
    registration_params = params.require(:registration).permit(:person_id, :course_id)
    @registration = Registration.new registration_params
    redirect_to person_url(@registration.person)

The only thing different about this is where we redirect to. We could redirect the user anywhere we want, but in this case, we want to redirect them back to the person’s page that they came from. That person is stored in @registration.person, so we can just use that variable to redirect the person back.

This will allow us to register students for classes.

Displaying Registrations

Next, we can add our list of classes to our person view, and our list of people to our course view. We can do this the same way we display any other relationship:

In app/views/people/show.html.erb, add:

<h3> Courses </h3>
<% @person.courses.each do |course| %>
  <li> <%= link_to "#{course.number} #{course.name}", course_url(course) %> </li>
<% end %>

In app/views/courses/show.html.erb, add:

<h3> People in this Course </h3>
<% @course.people.each do |person| %>
  <li> <%= link_to person.name, person_url(person) %>
<% end %>

Notice how we use @course.people and @person.courses to get at the list of people and courses (respectively), and then call .each on the list to loop through it.

OK, now lets go try it and see if it works. Don’t forget to test all of the following:

Also verify that you can have multiple people registered for a course, and multiple courses associated with each person.

Optional Exercise: Add Grades to Registrations

One of the nice things about using a separate model for the many-to-many relationship is that you can store additional fields in that model. For example, say you wanted to store grades for people in courses. The registration model would be the perfect place to store the grades. As an optional exercise for this lab, add the ability to store a grade for each person in a course. To do this, you will need to: