Chapter 12 Following users

In this chapter, we will complete the sample application by adding a social layer that allows users to follow (and unfollow) other users, resulting in each user’s Home page displaying a status feed of the followed users’ microposts. We’ll start by learning how to model relationships between users in Section 12.1, and we’ll build the corresponding web interface in Section 12.2 (including an introduction to Ajax). We’ll end by developing a fully functional status feed in Section 12.3.

This final chapter contains some of the most challenging material in the tutorial, including some Ruby/SQL trickery to make the status feed. Through these examples, you will see how Rails can handle even rather intricate data models, which should serve you well as you go on to develop your own applications with their own specific requirements. To help with the transition from tutorial to independent development, Section 12.4 offers some pointers to more advanced resources.

Because the material in this chapter is particularly challenging, before writing any code we’ll pause for a moment and take a tour of the interface. As in previous chapters, at this early stage we’ll represent pages using mockups.1 The full page flow runs as follows: a user (John Calvin) starts at his profile page (Figure 12.1) and navigates to the Users page (Figure 12.2) to select a user to follow. Calvin navigates to the profile of a second user, Thomas Hobbes (Figure 12.3), clicking on the “Follow” button to follow that user. This changes the “Follow” button to “Unfollow” and increments Hobbes’s “followers” count by one (Figure 12.4). Navigating to his home page, Calvin now sees an incremented “following” count and finds Hobbes’s microposts in his status feed (Figure 12.5). The rest of this chapter is dedicated to making this page flow actually work.

images/figures/page_flow_profile_mockup_3rd_edition
Figure 12.1: A current user’s profile.
images/figures/page_flow_user_index_mockup_bootstrap
Figure 12.2: Finding a user to follow.
images/figures/page_flow_other_profile_follow_button_mockup_3rd_edition
Figure 12.3: The profile of a user to follow, with a follow button.
images/figures/page_flow_other_profile_unfollow_button_mockup_3rd_edition
Figure 12.4: A profile with an unfollow button and incremented followers count.
images/figures/page_flow_home_page_feed_mockup_3rd_edition
Figure 12.5: The Home page with status feed and incremented following count.

12.1 The Relationship model

Our first step in implementing following users is to construct a data model, which is not as straightforward as it seems. Naïvely, it seems that a has_many relationship would do: a user has_many followed users and has_many followers. As we will see, there is a problem with this approach, and we’ll learn how to fix it using has_many :through.

As usual, Git users should create a new topic branch:

$ git checkout master
$ git checkout -b following-users

12.1.1 A problem with the data model (and a solution)

As a first step toward constructing a data model for following users, let’s examine a typical case. For instance, consider a user who follows a second user: we could say that, e.g., Calvin is following Hobbes, and Hobbes is followed by Calvin, so that Calvin is the follower and Hobbes is followed. Using Rails’ default pluralization convention, the set of all users following a given user is that user’s followers, and hobbes.followers is an array of those users. Unfortunately, the reverse doesn’t work: by default, the set of all followed users would be called the followeds, which is ungrammatical and clumsy. We’ll adopt Twitter’s convention and call them following (as in “50 following, 75 followers”), with a corresponding calvin.following array.

This discussion suggests modeling the followed users as in Figure 12.6, with a following table and a has_many association. Since user.following should be a collection of users, each row of the following table would need to be a user, as identified by the followed_id, together with the follower_id to establish the association.2 In addition, since each row is a user, we would need to include the user’s other attributes, including the name, email, password, etc.

images/figures/naive_user_has_many_following
Figure 12.6: A naïve implementation of user following.

The problem with the data model in Figure 12.6 is that it is terribly redundant: each row contains not only each followed user’s id, but all their other information as well—all of which is already in the users table. Even worse, to model user followers we would need a separate, similarly redundant followers table. Finally, this data model is a maintainability nightmare: each time a user changed (say) their name, we would need to update not just the user’s record in the users table but also every row containing that user in both the following and followers tables.

The problem here is that we are missing an underlying abstraction. One way to find the proper model is to consider how we might implement the act of following in a web application. Recall from Section 7.1.2 that the REST architecture involves resources that are created and destroyed. This leads us to ask two questions: When a user follows another user, what is being created? When a user unfollows another user, what is being destroyed? Upon reflection, we see that in these cases the application should either create or destroy a relationship between two users. A user then has many relationships, and has many following (or followers) through these relationships.

There’s an additional detail we need to address regarding our application’s data model: unlike symmetric Facebook-style friendships, which are always reciprocal (at least at the data-model level), Twitter-style following relationships are potentially asymmetric—Calvin can follow Hobbes without Hobbes following Calvin. To distinguish between these two cases, we’ll adopt the terminology of active and passive relationships: if Calvin is following Hobbes but not vice versa, Calvin has an active relationship with Hobbes and Hobbes has a passive relationship with Calvin.3

We’ll focus now on using active relationships to generate a list of followed users, and consider the passive case in Section 12.1.5. Figure 12.6 suggests how to implement it: since each followed user is uniquely identified by followed_id, we could convert following to an active_relationships table, omit the user details, and use followed_id to retrieve the followed user from the users table. A diagram of the data model appears in Figure 12.7.

images/figures/user_has_many_following_3rd_edition
Figure 12.7: A model of followed users through active relationships.

Because we’ll end up using the same database table for both active and passive relationships, we’ll use the generic term relationship for the table name, with a corresponding Relationship model. The result is the Relationship data model shown in Figure 12.8. We’ll see starting in Section 12.1.4 how to use the Relationship model to simulate both Active Relationship and Passive Relationship models.

images/figures/relationship_model
Figure 12.8: The Relationship data model.

To get started with the implementation, we first generate a migration corresponding to Figure 12.8:

$ rails generate model Relationship follower_id:integer followed_id:integer

Because we will be finding relationships by follower_id and by followed_id, we should add an index on each column for efficiency, as shown in Listing 12.1.

Listing 12.1: Adding indices for the relationships table. db/migrate/[timestamp]_create_relationships.rb
class CreateRelationships < ActiveRecord::Migration
  def change
    create_table :relationships do |t|
      t.integer :follower_id
      t.integer :followed_id

      t.timestamps null: false
    end
    add_index :relationships, :follower_id
    add_index :relationships, :followed_id
    add_index :relationships, [:follower_id, :followed_id], unique: true
  end
end

Listing 12.1 also includes a multiple-key index that enforces uniqueness on (follower_id, followed_id) pairs, so that a user can’t follow another user more than once. (Compare to the email uniqueness index from Listing 6.28 and the multiple-key index in Listing 11.1.) As we’ll see starting in Section 12.1.4, our user interface won’t allow this to happen, but adding a unique index arranges to raise an error if a user tries to create duplicate relationships anyway (for example, by using a command-line tool such as curl).

To create the relationships table, we migrate the database as usual:

$ bundle exec rake db:migrate

12.1.2 User/relationship associations

Before implementing user following and followers, we first need to establish the association between users and relationships. A user has_many relationships, and—since relationships involve two users—a relationship belongs_to both a follower and a followed user.

As with microposts in Section 11.1.3, we will create new relationships using the user association, with code such as

user.active_relationships.build(followed_id: ...)

At this point, you might expect application code as in Section 11.1.3, and it’s similar, but there are two key differences.

First, in the case of the user/micropost association we could write

class User < ActiveRecord::Base
  has_many :microposts
  .
  .
  .
end

This works because by convention Rails looks for a Micropost model corresponding to the :microposts symbols.4 In the present case, though, we want to write

has_many :active_relationships

even though the underlying model is called Relationship. We will thus have to tell Rails the model class name to look for.

Second, before we wrote

class Micropost < ActiveRecord::Base
  belongs_to :user
  .
  .
  .
end

in the Micropost model. This works because the microposts table has a user_id attribute to identify the user (Section 11.1.1). An id used in this manner to connect two database tables is known as a foreign key, and when the foreign key for a User model object is user_id, Rails infers the association automatically: by default, Rails expects a foreign key of the form <class>_id, where <class> is the lower-case version of the class name.5 In the present case, although we are still dealing with users, the user following another user is now identified with the foreign key follower_id, so we have to tell that to Rails.

The result of the above discussion is the user/relationship association shown in Listing 12.2 and Listing 12.3.

Listing 12.2: Implementing the active relationships has_many association. app/models/user.rb
class User < ActiveRecord::Base
  has_many :microposts, dependent: :destroy
  has_many :active_relationships, class_name:  "Relationship",
                                  foreign_key: "follower_id",
                                  dependent:   :destroy
  .
  .
  .
end

(Since destroying a user should also destroy that user’s relationships, we’ve added dependent: :destroy to the association.)

Listing 12.3: Adding the follower belongs_to association to the Relationship model. app/models/relationship.rb
class Relationship < ActiveRecord::Base
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end

The followed association isn’t actually needed until Section 12.1.4, but the parallel follower/followed structure is clearer if we implement them both at the same time.

The relationships in Listing 12.2 and Listing 12.3 give rise to methods analogous to the ones we saw in Table 11.1, as shown in Table 12.1.

Method Purpose
active_relationship.follower Returns the follower
active_relationship.followed Returns the followed user
user.active_relationships.create(followed_id: other_user.id) Creates an active relationship associated with user
user.active_relationships.create!(followed_id: other_user.id) Creates an active relationship associated with user (exception on failure)
user.active_relationships.build(followed_id: other_user.id) Returns a new Relationship object associated with user
Table 12.1: A summary of user/active relationship association methods.

12.1.3 Relationship validations

Before moving on, we’ll add a couple of Relationship model validations for completeness. The tests (Listing 12.4) and application code (Listing 12.5) are straightforward. As with the generated user fixture (Listing 6.29), the generated relationship fixture also violates the uniqueness constraint imposed by the corresponding migration (Listing 12.1). The solution (removing the fixture contents as in Listing 6.30) is also the same (Listing 12.6).

Listing 12.4: Testing the Relationship model validations. test/models/relationship_test.rb
require 'test_helper'

class RelationshipTest < ActiveSupport::TestCase

  def setup
    @relationship = Relationship.new(follower_id: 1, followed_id: 2)
  end

  test "should be valid" do
    assert @relationship.valid?
  end

  test "should require a follower_id" do
    @relationship.follower_id = nil
    assert_not @relationship.valid?
  end

  test "should require a followed_id" do
    @relationship.followed_id = nil
    assert_not @relationship.valid?
  end
end
Listing 12.5: Adding the Relationship model validations. app/models/relationship.rb
class Relationship < ActiveRecord::Base
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
  validates :follower_id, presence: true
  validates :followed_id, presence: true
end
Listing 12.6: Removing the contents of the relationship fixture. test/fixtures/relationships.yml
# empty

At this point, the tests should be green:

Listing 12.7: green
$ bundle exec rake test

12.1.4 Followed users

We come now to the heart of the Relationship associations: following and followers. Here we will use has_many :through for the first time: a user has many following through relationships, as illustrated in Figure 12.7. By default, in a has_many :through association Rails looks for a foreign key corresponding to the singular version of the association. In other words, with code like

has_many :followeds, through: :active_relationships

Rails would see “followeds” and use the singular “followed”, assembling a collection using the followed_id in the relationships table. But, as noted in Section 12.1.1, user.followeds is rather awkward, so we’ll write user.following instead. Naturally, Rails allows us to override the default, in this case using the source parameter (as shown in Listing 12.8), which explicitly tells Rails that the source of the following array is the set of followed ids.

Listing 12.8: Adding the User model following association. app/models/user.rb
class User < ActiveRecord::Base
  has_many :microposts, dependent: :destroy
  has_many :active_relationships, class_name:  "Relationship",
                                  foreign_key: "follower_id",
                                  dependent:   :destroy
  has_many :following, through: :active_relationships, source: :followed
  .
  .
  .
end

The association defined in Listing 12.8 leads to a powerful combination of Active Record and array-like behavior. For example, we can check if the followed users collection includes another user with the include? method (Section 4.3.1), or find objects through the association:

user.following.include?(other_user)
user.following.find(other_user)

Although in many contexts we can effectively treat following as an array, Rails is smart about how it handles things under the hood. For example, code like

following.include?(other_user)

looks like it might have to pull all the followed users out of the database to apply the include? method, but in fact for efficiency Rails arranges for the comparison to happen directly in the database. (Compare to the code in Section 11.2.1, where we saw that

user.microposts.count

performs the count directly in the database.)

To manipulate following relationships, we’ll introduce follow and unfollow utility methods so that we can write, e.g., user.follow(other_user). We’ll also add an associated following? boolean method to test if one user is following another.6

This is exactly the kind of situation where I like to write some tests first. The reason is that we are quite far from writing a working web interface for following users, but it’s hard to proceed without some sort of client for the code we’re developing. In this case, it’s easy to write a short test for the User model, in which we use following? to make sure the user isn’t following the other user, use follow to follow another user, use following? to verify that the operation succeeded, and finally unfollow and verify that it worked. The result appears in Listing 12.9.

Listing 12.9: Tests for some “following” utility methods. red test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  .
  .
  .
  test "should follow and unfollow a user" do
    michael = users(:michael)
    archer  = users(:archer)
    assert_not michael.following?(archer)
    michael.follow(archer)
    assert michael.following?(archer)
    michael.unfollow(archer)
    assert_not michael.following?(archer)
  end
end

By referring to the methods in Table 12.1, we can write the follow, unfollow, and following? methods using the association with following, as shown in Listing 12.10. (Note that we have omitted the user self variable whenever possible.)

Listing 12.10: Utility methods for following. green app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  def feed
    .
    .
    .
  end

  # Follows a user.
  def follow(other_user)
    active_relationships.create(followed_id: other_user.id)
  end

  # Unfollows a user.
  def unfollow(other_user)
    active_relationships.find_by(followed_id: other_user.id).destroy
  end

  # Returns true if the current user is following the other user.
  def following?(other_user)
    following.include?(other_user)
  end

  private
  .
  .
  .
end

With the code in Listing 12.10, the tests should be green:

Listing 12.11: green
$ bundle exec rake test

12.1.5 Followers

The final piece of the relationships puzzle is to add a user.followers method to go with user.following. You may have noticed from Figure 12.7 that all the information needed to extract an array of followers is already present in the relationships table (which we are treating as the active_relationships table via the code in Listing 12.2). Indeed, the technique is exactly the same as for followed users, with the roles of follower_id and followed_id reversed, and with passive_relationships in place of active_relationships. The data model then appears as in Figure 12.9.

images/figures/user_has_many_followers_3rd_edition
Figure 12.9: A model for user followers through passive relationships.

The implementation of the data model in Figure 12.9 parallels Listing 12.8 exactly, as seen in Listing 12.12.

Listing 12.12: Implementing user.followers using passive relationships. app/models/user.rb
class User < ActiveRecord::Base
  has_many :microposts, dependent: :destroy
  has_many :active_relationships,  class_name:  "Relationship",
                                   foreign_key: "follower_id",
                                   dependent:   :destroy
  has_many :passive_relationships, class_name:  "Relationship",
                                   foreign_key: "followed_id",
                                   dependent:   :destroy
  has_many :following, through: :active_relationships,  source: :followed
  has_many :followers, through: :passive_relationships, source: :follower
  .
  .
  .
end

It’s worth noting that we could actually omit the :source key for followers in Listing 12.12, using simply

has_many :followers, through: :passive_relationships

This is because, in the case of a :followers attribute, Rails will singularize “followers” and automatically look for the foreign key follower_id in this case. Listing 12.8 keeps the :source key to emphasize the parallel structure with the has_many :following association.

We can conveniently test the data model above using the followers.include? method, as shown in Listing 12.13. (Listing 12.13 might have used a followed_by? method to complement the following? method, but it turns out we won’t need it in our application.)

Listing 12.13: A test for followers. green test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  .
  .
  .
  test "should follow and unfollow a user" do
    michael  = users(:michael)
    archer   = users(:archer)
    assert_not michael.following?(archer)
    michael.follow(archer)
    assert michael.following?(archer)
    assert archer.followers.include?(michael)
    michael.unfollow(archer)
    assert_not michael.following?(archer)
  end
end

Listing 12.13 adds only one line to the test from Listing 12.9, but so many things have to go right to get it to pass that it’s a very sensitive test of the code in Listing 12.12.

At this point, the full test suite should be green:

$ bundle exec rake test

12.2 A web interface for following users

Section 12.1 placed rather heavy demands on our data modeling skills, and it’s fine if it takes a while to soak in. In fact, one of the best ways to understand the associations is to use them in the web interface.

In the introduction to this chapter, we saw a preview of the page flow for user following. In this section, we will implement the basic interface and following/unfollowing functionality shown in those mockups. We will also make separate pages to show the user following and followers arrays. In Section 12.3, we’ll complete our sample application by adding the user’s status feed.

12.2.1 Sample following data

As in previous chapters, we will find it convenient to use the seed data Rake task to fill the database with sample relationships. This will allow us to design the look and feel of the web pages first, deferring the back-end functionality until later in this section.

Code to seed the following relationships appear in Listing 12.14. Here we somewhat arbitrarily arrange for the first user to follow users 3 through 51, and then have users 4 through 41 follow that user back. The resulting relationships will be sufficient for developing the application interface.

Listing 12.14: Adding following/follower relationships to the sample data. db/seeds.rb
# Users
User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar",
             admin:     true,
             activated: true,
             activated_at: Time.zone.now)

99.times do |n|
  name  = Faker::Name.name
  email = "example-#{n+1}@railstutorial.org"
  password = "password"
  User.create!(name: name,
              email: email,
              password:              password,
              password_confirmation: password,
              activated: true,
              activated_at: Time.zone.now)
end

# Microposts
users = User.order(:created_at).take(6)
50.times do
  content = Faker::Lorem.sentence(5)
  users.each { |user| user.microposts.create!(content: content) }
end

# Following relationships
users = User.all
user  = users.first
following = users[2..50]
followers = users[3..40]
following.each { |followed| user.follow(followed) }
followers.each { |follower| follower.follow(user) }

To execute the code in Listing 12.14, we reseed the database as usual:

$ bundle exec rake db:migrate:reset
$ bundle exec rake db:seed

12.2.2 Stats and a follow form

Now that our sample users have both followed users and followers, we need to update the profile page and Home page to reflect this. We’ll start by making a partial to display the following and follower statistics on the profile and home pages. We’ll next add a follow/unfollow form, and then make dedicated pages for showing “following” (followed users) and “followers”.

As noted in Section 12.1.1, we’ll adopt Twitter’s convention of using “following” as a label for followed users, as in “50 following”. This usage is reflected in the mockup sequence starting in Figure 12.1 and shown in close-up in Figure 12.10.

stats_partial_mockup
Figure 12.10: A mockup of the stats partial.

The stats in Figure 12.10 consist of the number of users the current user is following and the number of followers, each of which should be a link to its respective dedicated display page. In Chapter 5, we stubbed out such links with the dummy text ’#’, but that was before we had much experience with routes. This time, although we’ll defer the actual pages to Section 12.2.3, we’ll make the routes now, as seen in Listing 12.15. This code uses the :member method inside a resources block, which we haven’t seen before, but see if you can guess what it does.

Listing 12.15: Adding following and followers actions to the Users controller. config/routes.rb
Rails.application.routes.draw do
  root                'static_pages#home'
  get    'help'    => 'static_pages#help'
  get    'about'   => 'static_pages#about'
  get    'contact' => 'static_pages#contact'
  get    'signup'  => 'users#new'
  get    'login'   => 'sessions#new'
  post   'login'   => 'sessions#create'
  delete 'logout'  => 'sessions#destroy'
  resources :users do
    member do
      get :following, :followers
    end
  end
  resources :account_activations, only: [:edit]
  resources :password_resets,     only: [:new, :create, :edit, :update]
  resources :microposts,          only: [:create, :destroy]
end

You might suspect that the URLs for following and followers will look like /users/1/following and /users/1/followers, and that is exactly what the code in Listing 12.15 arranges. Since both pages will be showing data, the proper HTTP verb is a GET request, so we use the get method to arrange for the URLs to respond appropriately. Meanwhile, the member method arranges for the routes to respond to URLs containing the user id. The other possibility, collection, works without the id, so that

resources :users do
  collection do
    get :tigers
  end
end

would respond to the URL /users/tigers (presumably to display all the tigers in our application).7

A table of the routes generated by Listing 12.15 appears in Table 12.2. Note the named routes for the followed user and followers pages, which we’ll put to use shortly.

HTTP request URL Action Named route
GET /users/1/following following following_user_path(1)
GET /users/1/followers followers followers_user_path(1)
Table 12.2: RESTful routes provided by the custom rules in resource in Listing 12.15.

With the routes defined, we are now in a position to define the stats partial, which involves a couple of links inside a div, as shown in Listing 12.16.

Listing 12.16: A partial for displaying follower stats. app/views/shared/_stats.html.erb
<% @user ||= current_user %>
<div class="stats">
  <a href="<%= following_user_path(@user) %>">
    <strong id="following" class="stat">
      <%= @user.following.count %>
    </strong>
    following
  </a>
  <a href="<%= followers_user_path(@user) %>">
    <strong id="followers" class="stat">
      <%= @user.followers.count %>
    </strong>
    followers
  </a>
</div>

Since we will be including the stats on both the user show pages and the home page, the first line of Listing 12.16 picks the right one using

<% @user ||= current_user %>

As discussed in Box 8.1, this does nothing when @user is not nil (as on a profile page), but when it is (as on the Home page) it sets @user to the current user. Note also that the following/follower counts are calculated through the associations using

@user.following.count

and

@user.followers.count

Compare these to the microposts count from Listing 11.23, where we wrote

@user.microposts.count

to count the microposts. As in that case, Rails calculates the count directly in the database for efficiency.

One final detail worth noting is the presence of CSS ids on some elements, as in

<strong id="following" class="stat">
...
</strong>

This is for the benefit of the Ajax implementation in Section 12.2.5, which accesses elements on the page using their unique ids.

With the partial in hand, including the stats on the Home page is easy, as shown in Listing 12.17.

Listing 12.17: Adding follower stats to the Home page. app/views/static_pages/home.html.erb
<% if logged_in? %>
  <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        <%= render 'shared/user_info' %>
      </section>
      <section class="stats">
        <%= render 'shared/stats' %>
      </section>
      <section class="micropost_form">
        <%= render 'shared/micropost_form' %>
      </section>
    </aside>
    <div class="col-md-8">
      <h3>Micropost Feed</h3>
      <%= render 'shared/feed' %>
    </div>
  </div>
<% else %>
  .
  .
  .
<% end %>

To style the stats, we’ll add some SCSS, as shown in Listing 12.18 (which contains all the stylesheet code needed in this chapter). The resulting Home page appears in Figure 12.11.

Listing 12.18: SCSS for the Home page sidebar. app/assets/stylesheets/custom.css.scss
.
.
.
/* sidebar */
.
.
.
.gravatar {
  float: left;
  margin-right: 10px;
}

.gravatar_edit {
  margin-top: 15px;
}

.stats {
  overflow: auto;
  margin-top: 0;
  padding: 0;
  a {
    float: left;
    padding: 0 10px;
    border-left: 1px solid $gray-lighter;
    color: gray;
    &:first-child {
      padding-left: 0;
      border: 0;
    }
    &:hover {
      text-decoration: none;
      color: blue;
    }
  }
  strong {
    display: block;
  }
}

.user_avatars {
  overflow: auto;
  margin-top: 10px;
  .gravatar {
    margin: 1px 1px;
  }
  a {
    padding: 0;
  }
}

.users.follow {
  padding: 0;
}

/* forms */
.
.
.
images/figures/home_page_follow_stats_3rd_edition
Figure 12.11: The Home page with follow stats.

We’ll render the stats partial on the profile page in a moment, but first let’s make a partial for the follow/unfollow button, as shown in Listing 12.19.

Listing 12.19: A partial for a follow/unfollow form. app/views/users/_follow_form.html.erb
<% unless current_user?(@user) %>
  <div id="follow_form">
  <% if current_user.following?(@user) %>
    <%= render 'unfollow' %>
  <% else %>
    <%= render 'follow' %>
  <% end %>
  </div>
<% end %>

This does nothing but defer the real work to follow and unfollow partials, which need new routes for the Relationships resource, which follows the Microposts resource example (Listing 11.29), as seen in Listing 12.20.

Listing 12.20: Adding the routes for user relationships. config/routes.rb
Rails.application.routes.draw do
  root                'static_pages#home'
  get    'help'    => 'static_pages#help'
  get    'about'   => 'static_pages#about'
  get    'contact' => 'static_pages#contact'
  get    'signup'  => 'users#new'
  get    'login'   => 'sessions#new'
  post   'login'   => 'sessions#create'
  delete 'logout'  => 'sessions#destroy'
  resources :users do
    member do
      get :following, :followers
    end
  end
  resources :account_activations, only: [:edit]
  resources :password_resets,     only: [:new, :create, :edit, :update]
  resources :microposts,          only: [:create, :destroy]
  resources :relationships,       only: [:create, :destroy]
end

The follow/unfollow partials themselves are shown in Listing 12.21 and Listing 12.22.

Listing 12.21: A form for following a user. app/views/users/_follow.html.erb
<%= form_for(current_user.active_relationships.build) do |f| %>
  <div><%= hidden_field_tag :followed_id, @user.id %></div>
  <%= f.submit "Follow", class: "btn btn-primary" %>
<% end %>
Listing 12.22: A form for unfollowing a user. app/views/users/_unfollow.html.erb
<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
             html: { method: :delete }) do |f| %>
  <%= f.submit "Unfollow", class: "btn" %>
<% end %>

These two forms both use form_for to manipulate a Relationship model object; the main difference between the two is that Listing 12.21 builds a new relationship, whereas Listing 12.22 finds the existing relationship. Naturally, the former sends a POST request to the Relationships controller to create a relationship, while the latter sends a DELETE request to destroy a relationship. (We’ll write these actions in Section 12.2.4.) Finally, you’ll note that the follow form doesn’t have any content other than the button, but it still needs to send the followed_id to the controller. We accomplish this with the hidden_field_tag method in Listing 12.21, which produces HTML of the form

<input id="followed_id" name="followed_id" type="hidden" value="3" />

As we saw in Section 10.2.4 (Listing 10.50), the hidden input tag puts the relevant information on the page without displaying it in the browser.

We can now include the follow form and the following statistics on the user profile page simply by rendering the partials, as shown in Listing 12.23. Profiles with follow and unfollow buttons, respectively, appear in Figure 12.12 and Figure 12.13.

Listing 12.23: Adding the follow form and follower stats to the user profile page. app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<div class="row">
  <aside class="col-md-4">
    <section>
      <h1>
        <%= gravatar_for @user %>
        <%= @user.name %>
      </h1>
    </section>
    <section class="stats">
      <%= render 'shared/stats' %>
    </section>
  </aside>
  <div class="col-md-8">
    <%= render 'follow_form' if logged_in? %>
    <% if @user.microposts.any? %>
      <h3>Microposts (<%= @user.microposts.count %>)</h3>
      <ol class="microposts">
        <%= render @microposts %>
      </ol>
      <%= will_paginate @microposts %>
    <% end %>
  </div>
</div>
images/figures/profile_follow_button_3rd_edition
Figure 12.12: A user profile with a follow button (/users/2).
images/figures/profile_unfollow_button_3rd_edition
Figure 12.13: A user profile with an unfollow button (/users/5).

We’ll get these buttons working soon enough—in fact, we’ll do it two ways, the standard way (Section 12.2.4) and using Ajax (Section 12.2.5)—but first we’ll finish the HTML interface by making the following and followers pages.

12.2.3 Following and followers pages

Pages to display followed users and followers will resemble a hybrid of the user profile page and the user index page (Section 9.3.1), with a sidebar of user information (including the following stats) and a list of users. In addition, we’ll include a raster of smaller user profile image links in the sidebar. Mockups matching these requirements appear in Figure 12.14 (following) and Figure 12.15 (followers).

images/figures/following_mockup_bootstrap
Figure 12.14: A mockup of the user following page.
images/figures/followers_mockup_bootstrap
Figure 12.15: A mockup of the user followers page.

Our first step is to get the following and followers links to work. We’ll follow Twitter’s lead and have both pages require user login. As with most previous examples of access control, we’ll write the tests first, as shown in Listing 12.24.

Listing 12.24: Tests for the authorization of the following and followers pages. red test/controllers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionController::TestCase

  def setup
    @user = users(:michael)
    @other_user = users(:archer)
  end
  .
  .
  .
  test "should redirect following when not logged in" do
    get :following, id: @user
    assert_redirected_to login_url
  end

  test "should redirect followers when not logged in" do
    get :followers, id: @user
    assert_redirected_to login_url
  end
end

The only tricky part of the implementation is realizing that we need to add two new actions to the Users controller. Based on the routes defined in Listing 12.15, we need to call them following and followers. Each action needs to set a title, find the user, retrieve either @user.following or @user.followers (in paginated form), and then render the page. The result appears in Listing 12.25.

Listing 12.25: The following and followers actions. app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy,
                                        :following, :followers]
  .
  .
  .
  def following
    @title = "Following"
    @user  = User.find(params[:id])
    @users = @user.following.paginate(page: params[:page])
    render 'show_follow'
  end

  def followers
    @title = "Followers"
    @user  = User.find(params[:id])
    @users = @user.followers.paginate(page: params[:page])
    render 'show_follow'
  end

  private
  .
  .
  .
end

As we’ve seen throughout this tutorial, the usual Rails convention is to implicitly render the template corresponding to an action, such as rendering show.html.erb at the end of the show action. In contrast, both actions in Listing 12.25 make an explicit call to render, in this case rendering a view called show_follow, which we must create. The reason for the common view is that the ERb is nearly identical for the two cases, and Listing 12.26 covers them both.

Listing 12.26: The show_follow view used to render following and followers. app/views/users/show_follow.html.erb
<% provide(:title, @title) %>
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <%= gravatar_for @user %>
      <h1><%= @user.name %></h1>
      <span><%= link_to "view my profile", @user %></span>
      <span><b>Microposts:</b> <%= @user.microposts.count %></span>
    </section>
    <section class="stats">
      <%= render 'shared/stats' %>
      <% if @users.any? %>
        <div class="user_avatars">
          <% @users.each do |user| %>
            <%= link_to gravatar_for(user, size: 30), user %>
          <% end %>
        </div>
      <% end %>
    </section>
  </aside>
  <div class="col-md-8">
    <h3><%= @title %></h3>
    <% if @users.any? %>
      <ul class="users follow">
        <%= render @users %>
      </ul>
      <%= will_paginate %>
    <% end %>
  </div>
</div>

The actions in Listing 12.25 render the view from Listing 12.26 in two contexts, “following” and “followers”, with the results shown in Figure 12.16 and Figure 12.17. Note that nothing in the above code uses the current user, so the same links work for other users, as shown in Figure 12.18.

images/figures/user_following_3rd_edition
Figure 12.16: Showing the users the given user is following.
images/figures/user_followers_3rd_edition
Figure 12.17: Showing the given user’s followers.
images/figures/diferent_user_followers_3rd_edition
Figure 12.18: Showing a different user’s followers.

Now that we have working following and followers pages, we’ll write a couple of short integration tests to verify their behavior. They are designed to be a sanity check, not to be comprehensive. Indeed, as noted in Section 5.3.4, comprehensive tests of things like HTML structure are likely to be brittle and thus counter-productive. Our plan in the case of following/followers pages is to check the number is correctly displayed and that links with the right URLs appear on the page.

To get started, we’ll generate an integration test as usual:

$ rails generate integration_test following
      invoke  test_unit
      create    test/integration/following_test.rb

Next, we need to assemble some test data, which we can do by adding some relationships fixtures to create following/follower relationships. Recall from Section 11.2.3 that we can use code like

orange:
  content: "I just ate an orange!"
  created_at: <%= 10.minutes.ago %>
  user: michael

to associate a micropost with a given user. In particular, we can write

user: michael

instead of

user_id: 1

Applying this idea to the relationships fixtures gives the associations in Listing 12.27.

Listing 12.27: Relationships fixtures for use in following/follower tests. test/fixtures/relationships.yml
one:
  follower: michael
  followed: lana

two:
  follower: michael
  followed: malory

three:
  follower: lana
  followed: michael

four:
  follower: archer
  followed: michael

The fixtures in Listing 12.27 first arrange for Michael to follow Lana and Malory, and then arrange for Michael to be followed by Lana and Archer. To test for the right count, we can use the same assert_match method we used in Listing 11.27 to test for the display of the number of microposts on the user profile page. Adding in assertions for the right links yields the tests shown in Listing 12.28.

Listing 12.28: Tests for following/follower pages. green test/integration/following_test.rb
require 'test_helper'

class FollowingTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
    log_in_as(@user)
  end

  test "following page" do
    get following_user_path(@user)
    assert_not @user.following.empty?
    assert_match @user.following.count.to_s, response.body
    @user.following.each do |user|
      assert_select "a[href=?]", user_path(user)
    end
  end

  test "followers page" do
    get followers_user_path(@user)
    assert_not @user.followers.empty?
    assert_match @user.followers.count.to_s, response.body
    @user.followers.each do |user|
      assert_select "a[href=?]", user_path(user)
    end
  end
end

In Listing 12.28, note that we include the assertion

assert_not @user.following.empty?

which is included to make sure that

@user.following.each do |user|
  assert_select "a[href=?]", user_path(user)
end

isn’t vacuously true (and similarly for followers).

The test suite should now be green:

Listing 12.29: green
$ bundle exec rake test

12.2.4 A working follow button the standard way

Now that our views are in order, it’s time to get the follow/unfollow buttons working. Because following and unfollowing involve creating and destroying relationships, we need a Relationships controller, which we generate as usual

$ rails generate controller Relationships

As we’ll see in Listing 12.31, enforcing access control on the Relationships controller actions won’t much matter, but we’ll still follow our previous practice of enforcing the security model as early as possible. In particular, we’ll check that attempts to access actions in the Relationships controller require a logged-in user (and thus get redirected to the login page), while also not changing the Relationship count, as shown in Listing 12.30.

Listing 12.30: Basic access control tests for relationships. red test/controllers/relationships_controller_test.rb
require 'test_helper'

class RelationshipsControllerTest < ActionController::TestCase

  test "create should require logged-in user" do
    assert_no_difference 'Relationship.count' do
      post :create
    end
    assert_redirected_to login_url
  end

  test "destroy should require logged-in user" do
    assert_no_difference 'Relationship.count' do
      delete :destroy, id: relationships(:one)
    end
    assert_redirected_to login_url
  end
end

We can get the tests in Listing 12.30 to pass by adding the logged_in_user before filter (Listing 12.31).

Listing 12.31: Access control for relationships. green app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
  end

  def destroy
  end
end

To get the follow and unfollow buttons to work, all we need to do is find the user associated with the followed_id in the corresponding form (i.e., Listing 12.21 or Listing 12.22), and then use the appropriate follow or unfollow method from Listing 12.10. The full implementation appears in Listing 12.32.

Listing 12.32: The Relationships controller. app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
    user = User.find(params[:followed_id])
    current_user.follow(user)
    redirect_to user
  end

  def destroy
    user = Relationship.find(params[:id]).followed
    current_user.unfollow(user)
    redirect_to user
  end
end

We can see from Listing 12.32 why the security issue mentioned above is minor: if an unlogged-in user were to hit either action directly (e.g., using a command-line tool like curl), current_user would be nil, and in both cases the action’s second line would raise an exception, resulting in an error but no harm to the application or its data. It’s best not to rely on that, though, so we’ve taken the extra step and added an additional layer of security.

With that, the core follow/unfollow functionality is complete, and any user can follow or unfollow any other user, as you can verify by clicking the corresponding buttons in your browser. (We’ll write integration tests to verify this behavior in Section 12.2.6.) The result of following user #2 is shown in Figure 12.19 and Figure 12.20.

images/figures/unfollowed_user
Figure 12.19: An unfollowed user.
images/figures/followed_user
Figure 12.20: The result of following an unfollowed user.

12.2.5 A working follow button with Ajax

Although our user following implementation is complete as it stands, we have one bit of polish left to add before starting work on the status feed. You may have noticed in Section 12.2.4 that both the create and destroy actions in the Relationships controller simply redirect back to the original profile. In other words, a user starts on another user’s profile page, follows the other user, and is immediately redirected back to the original page. It is reasonable to ask why the user needs to leave that page at all.

This is exactly the problem solved by Ajax, which allows web pages to send requests asynchronously to the server without leaving the page.8 Because adding Ajax to web forms is a common practice, Rails makes Ajax easy to implement. Indeed, updating the follow/unfollow form partials is trivial: just change

form_for

to

form_for ..., remote: true

and Rails automagically uses Ajax. The updated partials appear in Listing 12.33 and Listing 12.34.

Listing 12.33: A form for following a user using Ajax. app/views/users/_follow.html.erb
<%= form_for(current_user.active_relationships.build, remote: true) do |f| %>
  <div><%= hidden_field_tag :followed_id, @user.id %></div>
  <%= f.submit "Follow", class: "btn btn-primary" %>
<% end %>
Listing 12.34: A form for unfollowing a user using Ajax. app/views/users/_unfollow.html.erb
<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
             html: { method: :delete },
             remote: true) do |f| %>
  <%= f.submit "Unfollow", class: "btn" %>
<% end %>

The actual HTML generated by this ERb isn’t particularly relevant, but you might be curious, so here’s a peek at a schematic view (details may differ):

<form action="/relationships/117" class="edit_relationship" data-remote="true"
      id="edit_relationship_117" method="post">
  .
  .
  .
</form>

This sets the variable data-remote="true" inside the form tag, which tells Rails to allow the form to be handled by JavaScript. By using a simple HTML property instead of inserting the full JavaScript code (as in previous versions of Rails), Rails follows the philosophy of unobtrusive JavaScript.

Having updated the form, we now need to arrange for the Relationships controller to respond to Ajax requests. We can do this using the respond_to method, responding appropriately depending on the type of request. The general pattern looks like this:

respond_to do |format|
  format.html { redirect_to user }
  format.js
end

The syntax is potentially confusing, and it’s important to understand that in the code above only one of the lines gets executed. (In this sense, respond_to is more like an if-then-else statement than a series of sequential lines.) Adapting the Relationships controller to respond to Ajax involves adding respond_to as above to the create and destroy actions from Listing 12.32. The result appears as in Listing 12.35. Note the change from the local variable user to the instance variable @user; in Listing 12.32 there was no need for an instance variable, but now such a variable is necessary for use in Listing 12.33 and Listing 12.34.

Listing 12.35: Responding to Ajax requests in the Relationships controller. app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
    @user = User.find(params[:followed_id])
    current_user.follow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end
end

The actions in Listing 12.35 degrade gracefully, which means that they work fine in browsers that have JavaScript disabled (although a small amount of configuration is necessary, as shown in Listing 12.36).

Listing 12.36: Configuration needed for graceful degradation of form submission. config/application.rb
require File.expand_path('../boot', __FILE__)
.
.
.
module SampleApp
  class Application < Rails::Application
    .
    .
    .
    # Include the authenticity token in remote forms.
    config.action_view.embed_authenticity_token_in_remote_forms = true
  end
end

On the other hand, we have yet to respond properly when JavaScript is enabled. In the case of an Ajax request, Rails automatically calls a JavaScript embedded Ruby (.js.erb) file with the same name as the action, i.e., create.js.erb or destroy.js.erb. As you might guess, such files allow us to mix JavaScript and embedded Ruby to perform actions on the current page. It is these files that we need to create and edit in order to update the user profile page upon being followed or unfollowed.

Inside a JS-ERb file, Rails automatically provides the jQuery JavaScript helpers to manipulate the page using the Document Object Model (DOM). The jQuery library (which we saw briefly in Section 11.4.2) provides a large number of methods for manipulating the DOM, but here we will need only two. First, we will need to know about the dollar-sign syntax to access a DOM element based on its unique CSS id. For example, to manipulate the follow_form element, we will use the syntax

$("#follow_form")

(Recall from Listing 12.19 that this is a div that wraps the form, not the form itself.) This syntax, inspired by CSS, uses the # symbol to indicate a CSS id. As you might guess, jQuery, like CSS, uses a dot . to manipulate CSS classes.

The second method we’ll need is html, which updates the HTML inside the relevant element with the contents of its argument. For example, to replace the entire follow form with the string "foobar", we would write

$("#follow_form").html("foobar")

Unlike plain JavaScript files, JS-ERb files also allow the use of embedded Ruby, which we apply in the create.js.erb file to update the follow form with the unfollow partial (which is what should show after a successful following) and update the follower count. The result is shown in Listing 12.37. This uses the escape_javascript method, which is needed to escape out the result when inserting HTML in a JavaScript file.

Listing 12.37: The JavaScript embedded Ruby to create a following relationship. app/views/relationships/create.js.erb
$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>");
$("#followers").html('<%= @user.followers.count %>');

Note the presence of line-ending semicolons, which are characteristic of languages with syntax descended from ALGOL.

The destroy.js.erb file is analogous (Listing 12.38).

Listing 12.38: The Ruby JavaScript (RJS) to destroy a following relationship. app/views/relationships/destroy.js.erb
$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>");
$("#followers").html('<%= @user.followers.count %>');

With that, you should navigate to a user profile page and verify that you can follow and unfollow without a page refresh.

12.2.6 Following tests

Now that the follow buttons are working, we’ll write some simple tests to prevent regressions. To follow a user, we post to the relationships path and verify that the number of followed users increases by 1:

assert_difference '@user.following.count', 1 do
  post relationships_path, followed_id: @other.id
end

This tests the standard implementation, but testing the Ajax version is almost the same, with xhr :post in place of plain post:

assert_difference '@user.following.count', 1 do
  xhr :post, relationships_path, followed_id: @other.id
end

This uses the xhr method (for XmlHttpRequest) to issue an Ajax request, which causes the respond_to block in Listing 12.35 to execute the proper JavaScript method.

The same parallel structure applies to deleting users, with delete instead of post. Here we check that the followed user count goes down by 1 and include the relationship and followed user’s id:

assert_difference '@user.following.count', -1 do
  delete relationship_path(relationship)
end

and

assert_difference '@user.following.count', -1 do
  xhr :delete, relationship_path(relationship)
end

Putting the two cases together gives the tests in Listing 12.39.

Listing 12.39: Tests for the follow and unfollow buttons. green test/integration/following_test.rb
require 'test_helper'

class FollowingTest < ActionDispatch::IntegrationTest

  def setup
    @user  = users(:michael)
    @other = users(:archer)
    log_in_as(@user)
  end
  .
  .
  .
  test "should follow a user the standard way" do
    assert_difference '@user.following.count', 1 do
      post relationships_path, followed_id: @other.id
    end
  end

  test "should follow a user with Ajax" do
    assert_difference '@user.following.count', 1 do
      xhr :post, relationships_path, followed_id: @other.id
    end
  end

  test "should unfollow a user the standard way" do
    @user.follow(@other)
    relationship = @user.active_relationships.find_by(followed_id: @other.id)
    assert_difference '@user.following.count', -1 do
      delete relationship_path(relationship)
    end
  end

  test "should unfollow a user with Ajax" do
    @user.follow(@other)
    relationship = @user.active_relationships.find_by(followed_id: @other.id)
    assert_difference '@user.following.count', -1 do
      xhr :delete, relationship_path(relationship)
    end
  end
end

At this point, the tests should be green:

Listing 12.40: green
$ bundle exec rake test

12.3 The status feed

We come now to the pinnacle of our sample application: the status feed of microposts. Appropriately, this section contains some of the most advanced material in the entire tutorial. The full status feed builds on the proto-feed from Section 11.3.3 by assembling an array of the microposts from the users being followed by the current user, along with the current user’s own microposts. Throughout this section, we’ll proceed through a series of feed implementations of increasing sophistication. To accomplish this, we will need some fairly advanced Rails, Ruby, and even SQL programming techniques.

Because of the heavy lifting ahead, it’s especially important to review where we’re going. A recap of the final status feed, shown in Figure 12.5, appears again in Figure 12.21.

images/figures/page_flow_home_page_feed_mockup_bootstrap
Figure 12.21: A mockup of a user’s Home page with a status feed.

12.3.1 Motivation and strategy

The basic idea behind the feed is simple. Figure 12.22 shows a sample microposts database table and the resulting feed. The purpose of a feed is to pull out the microposts whose user ids correspond to the users being followed by the current user (and the current user itself), as indicated by the arrows in the diagram.

images/figures/user_feed
Figure 12.22: The feed for a user (id 1) following users with ids 2, 7, 8, and 10.

Although we don’t yet know how to implement the feed, the tests are relatively straightforward, so (following the guidelines in Box 3.3) we’ll write them first. The key is to check all three requirements for the feed: microposts for both followed users and the user itself should be included in the feed, but a post from an unfollowed user should not be included. Based on the fixtures in Listing 9.43 and Listing 11.51, this means that Michael should see Lana’s posts and his own posts, but not Archer’s posts. Converting these requirements to assertions and recalling that the feed is in the User model (Listing 11.44) gives the updated User model test shown in Listing 12.41.

Listing 12.41: A test for the status feed. red test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  .
  .
  .
  test "feed should have the right posts" do
    michael = users(:michael)
    archer  = users(:archer)
    lana    = users(:lana)
    # Posts from followed user
    lana.microposts.each do |post_following|
      assert michael.feed.include?(post_following)
    end
    # Posts from self
    michael.microposts.each do |post_self|
      assert michael.feed.include?(post_self)
    end
    # Posts from unfollowed user
    archer.microposts.each do |post_unfollowed|
      assert_not michael.feed.include?(post_unfollowed)
    end
  end
end

Of course, the current implementation is just a proto-feed, so the new test is initially red:

Listing 12.42: red
$ bundle exec rake test

12.3.2 A first feed implementation

With the status feed design requirements captured in the test from Listing 12.41, we’re ready to start writing the feed. Since the final feed implementation is rather intricate, we’ll build up to it by introducing one piece at a time. The first step is to think of the kind of query we’ll need. We need to select all the microposts from the microposts table with ids corresponding to the users being followed by a given user (or the user itself). We might write this schematically as follows:

SELECT * FROM microposts
WHERE user_id IN (<list of ids>) OR user_id = <user id>

In writing this code, we’ve guessed that SQL supports an IN keyword that allows us to test for set inclusion. (Happily, it does.)

Recall from the proto-feed in Section 11.3.3 that Active Record uses the where method to accomplish the kind of select shown above, as illustrated in Listing 11.44. There, our select was very simple; we just picked out all the microposts with user id corresponding to the current user:

Micropost.where("user_id = ?", id)

Here, we expect it to be more complicated, something like this:

Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)

We see from these conditions that we’ll need an array of ids corresponding to the users being followed. One way to do this is to use Ruby’s map method, available on any “enumerable” object, i.e., any object (such as an Array or a Hash) that consists of a collection of elements.9 We saw an example of this method in Section 4.3.2; as another example, we’ll use map to convert an array of integers to an array of strings:

$ rails console
>> [1, 2, 3, 4].map { |i| i.to_s }
=> ["1", "2", "3", "4"]

Situations like the one illustrated above, where the same method gets called on each element in the collection, are common enough that there’s a shorthand notation for it (seen briefly in Section 4.3.2) that uses an ampersand & and a symbol corresponding to the method:

>> [1, 2, 3, 4].map(&:to_s)
=> ["1", "2", "3", "4"]

Using the join method (Section 4.3.1), we can create a string composed of the ids by joining them on comma-space :

>> [1, 2, 3, 4].map(&:to_s).join(', ')
=> "1, 2, 3, 4"

We can use the above method to construct the necessary array of followed user ids by calling id on each element in user.following. For example, for the first user in the database this array appears as follows:

>> User.first.following.map(&:id)
=> [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51]

In fact, because this sort of construction is so useful, Active Record provides it by default:

>> User.first.following_ids
=> [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51]

Here the following_ids method is synthesized by Active Record based on the has_many :following association (Listing 12.8); the result is that we need only append _ids to the association name to get the ids corresponding to the user.following collection. A string of followed user ids then appears as follows:

>> User.first.following_ids.join(', ')
=> "4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51"

When inserting into an SQL string, though, you don’t need to do this; the ? interpolation takes care of it for you (and in fact eliminates some database-dependent incompatibilities). This means we can use following_ids by itself.

As a result, the initial guess of

Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)

actually works! The result appears in Listing 12.43.

Listing 12.43: The initial working feed. green app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  # Returns true if a password reset has expired.
  def password_reset_expired?
    reset_sent_at < 2.hours.ago
  end

  # Returns a user's status feed.
  def feed
    Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
  end

  # Follows a user.
  def follow(other_user)
    active_relationships.create(followed_id: other_user.id)
  end
  .
  .
  .
end

The test suite should be green:

Listing 12.44: green
$ bundle exec rake test

In some applications, this initial implementation might be good enough for most practical purposes, but Listing 12.43 isn’t the final implementation; see if you can make a guess about why not before moving on to the next section. (Hint: What if a user is following 5000 other users?)

12.3.3 Subselects

As hinted at in the last section, the feed implementation in Section 12.3.2 doesn’t scale well when the number of microposts in the feed is large, as would likely happen if a user were following, say, 5000 other users. In this section, we’ll reimplement the status feed in a way that scales better with the number of followed users.

The problem with the code in Section 12.3.2 is that following_ids pulls all the followed users’ ids into memory, and creates an array the full length of the followed users array. Since the condition in Listing 12.43 actually just checks inclusion in a set, there must be a more efficient way to do this, and indeed SQL is optimized for just such set operations. The solution involves pushing the finding of followed user ids into the database using a subselect.

We’ll start by refactoring the feed with the slightly modified code in Listing 12.45.

Listing 12.45: Using key-value pairs in the feed’s where method. green app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  # Returns a user's status feed.
  def feed
    Micropost.where("user_id IN (:following_ids) OR user_id = :user_id",
                    following_ids: following_ids, user_id: id)
  end
  .
  .
  .
end

As preparation for the next step, we have replaced

Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)

with the equivalent

Micropost.where("user_id IN (:following_ids) OR user_id = :user_id",
                following_ids: following_ids, user_id: id)

The question mark syntax is fine, but when we want the same variable inserted in more than one place, the second syntax is more convenient.

The above discussion implies that we will be adding a second occurrence of user_id in the SQL query. In particular, we can replace the Ruby code

following_ids

with the SQL snippet

following_ids = "SELECT followed_id FROM relationships
                 WHERE  follower_id = :user_id"

This code contains an SQL subselect, and internally the entire select for user 1 would look something like this:

SELECT * FROM microposts
WHERE user_id IN (SELECT followed_id FROM relationships
                  WHERE  follower_id = 1)
      OR user_id = 1

This subselect arranges for all the set logic to be pushed into the database, which is more efficient.

With this foundation, we are ready for a more efficient feed implementation, as seen in Listing 12.46. Note that, because it is now raw SQL, the following_ids string is interpolated, not escaped.

Listing 12.46: The final implementation of the feed. green app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  # Returns a user's status feed.
  def feed
    following_ids = "SELECT followed_id FROM relationships
                     WHERE  follower_id = :user_id"
    Micropost.where("user_id IN (#{following_ids})
                     OR user_id = :user_id", user_id: id)
  end
  .
  .
  .
end

This code has a formidable combination of Rails, Ruby, and SQL, but it does the job, and does it well:

Listing 12.47: green
$ bundle exec rake test

Of course, even the subselect won’t scale forever. For bigger sites, you would probably need to generate the feed asynchronously using a background job, but such scaling subtleties are beyond the scope of this tutorial.

With the code in Listing 12.46, our status feed is now complete. Recall from Section 11.3.3 that the Home page already includes the feed; as a reminder, the home action appears again in Listing 12.48. In Chapter 11, the result was only a proto-feed (Figure 11.14), but with the implementation in Listing 12.46 as seen in Figure 12.23 the Home page now shows the full feed.

Listing 12.48: The home action with a paginated feed. app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController

  def home
    if logged_in?
      @micropost  = current_user.microposts.build
      @feed_items = current_user.feed.paginate(page: params[:page])
    end
  end
  .
  .
  .
end
images/figures/home_page_with_feed_3rd_edition
Figure 12.23: The Home page with a working status feed.

At this point, we’re ready to merge our changes into the master branch:

$ bundle exec rake test
$ git add -A
$ git commit -m "Add user following"
$ git checkout master
$ git merge following-users

We can then push the code to the remote repository and deploy the application to production:

$ git push
$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rake db:migrate
$ heroku run rake db:seed

The result is a working status feed on the live Web (Figure 12.24).

images/figures/live_status_feed
Figure 12.24: A working status feed on the live Web.

12.4 Conclusion

With the addition of the status feed, we’ve finished the sample application for the Ruby on Rails Tutorial. This application includes examples of all the major features of Rails, including models, views, controllers, templates, partials, filters, validations, callbacks, has_many/belongs_to and has_many :through associations, security, testing, and deployment.

Despite this impressive list, there is still much to learn about web development. As a first step in this process, this section contains some suggestions for further learning.

12.4.1 Guide to further resources

There is a wealth of Rails resources in stores and on the web—indeed, the supply is so rich that it can be overwhelming. The good news is that, having gotten this far, you’re ready for almost anything else out there. Here are some suggestions for further learning:

12.4.2 What we learned in this chapter

12.5 Exercises

For a suggestion on how to avoid conflicts between exercises and the main tutorial, see the note on exercise topic branches in Section 3.6.

  1. Write tests for the stats on the Home and profile pages. Hint: Add to the test in Listing 11.27. (Why don’t we have to test the stats on the Home page separately?)
  2. Write a test to verify that the first page of the feed appears on the Home page as required. A template appears in Listing 12.49. Note the use of HTML escaping via CGI.escapeHTML; see if you can figure out why this is necessary. (Try removing the escaping and carefully inspect the page source for the micropost content that doesn’t match.)
Listing 12.49: Testing the feed HTML. green test/integration/following_test.rb
require 'test_helper'

class FollowingTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
    log_in_as(@user)
  end
  .
  .
  .
  test "feed on Home page" do
    get root_path
    @user.feed.paginate(page: 1).each do |micropost|
      assert_match CGI.escapeHTML(FILL_IN), FILL_IN
    end
  end
end
  1. The new photographs in the mockup tour are from http://www.flickr.com/photos/john_lustig/2518452221/ and https://www.flickr.com/photos/renemensen/9187111340. 
  2. For simplicity, Figure 12.6 omits the following table’s id column. 
  3. Thanks to reader Paul Fioravanti for suggesting this terminology. 
  4. Technically, Rails converts the argument of has_many to a class name using the classify method, which converts "foo_bars" to "FooBar"
  5. Technically, Rails uses the underscore method to convert the class name to an id. For example, "FooBar".underscore is "foo_bar", so the foreign key for a FooBar object would be foo_bar_id
  6. Once you have a lot of experience modeling a particular domain, you can often guess such utility methods in advance, and even when you can’t you’ll often find yourself writing them to make the tests cleaner. In this case, though, it’s OK if you wouldn’t have guessed them. Software development is usually an iterative process—you write code until it starts getting ugly, and then you refactor it—but for brevity the tutorial presentation is streamlined a bit. 
  7. For more details on such routing options, see the Rails Guides article on “Rails Routing from the Outside In”
  8. Because it is nominally an acronym for asynchronous JavaScript and XML, Ajax is sometimes misspelled “AJAX”, even though the original Ajax article spells it as “Ajax” throughout. 
  9. The main requirement is that enumerable objects must implement an each method to iterate through the collection.