Ch 14: Following users Flashcards
Create a new topic branch
Create a new topic branch
git checkout -b following-users
Generate migration for relationship model
Generate migration for relationship model
$ rails generate model Relationship follower_id:integer followed_id:integer
add an index on each column for efficiency
add an index on each column for efficiency
db/migrate/[timestamp]_create_relationships.rbclass CreateRelationships < ActiveRecord::Migration[5.0] def change create_table :relationships do |t| t.integer :follower_id t.integer :followed_id t.timestamps end add_index :relationships, :follower_id add_index :relationships, :followed_id add_index :relationships, [:follower_id, :followed_id], unique: true endend
- This code 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.
- migrate with rails db:migrate
establish the association between users and relationships.
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.
create new relationships using the user association
create new relationships using the user association
user.active_relationships.build(followed_id: ...)
minipost vs relationships
minipost vs relationships
- minipost: This works because by convention Rails looks for a Micropost model corresponding to the :microposts symbol.
class User < ApplicationRecord has_many :microposts .end
- relationship: even though the underlying model is called Relationship. We will thus have to tell Rails the model class name to look for.
has_many :active_relationships
- minipost: This works because the microposts table has a user_id attribute to identify the user. 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 _id, where is the lower-case version of the class name.”
class Micropost < ApplicationRecord belongs_to :user . end
- relationship: 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.
app/models/user.rb class User < ApplicationRecord 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.)
Add the follower belongs_to association to the Relationship model.
Add the follower belongs_to association to the Relationship model.
app/models/relationship.rbclass Relationship < ApplicationRecord belongs_to :follower, class_name: "User" belongs_to :followed, class_name: "User"end
- The followed association isn’t actually needed until later, but the parallel follower/followed structure is clearer if we implement them both at the same time.
A summary of user/active relationship association methods.
A summary of user/active relationship association methods.
Method | Purposeactive_relationship.follower | Returns the followeractive_relationship.followed | Returns the followed useruser.active_relationships.create(followed_id: other_user.id) | Creates an active relationship associated with useruser.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
Add test for relationship model validations
Add test for relationship model validations
test/models/relationship_test.rbrequire 'test_helper'class RelationshipTest < ActiveSupport::TestCase def setup @relationship = Relationship.new(follower_id: users(:michael).id, followed_id: users(:archer).id) 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? endend
Add the Relationship model validations.
Add the Relationship model validations.
Adding the Relationship model validations.app/models/relationship.rbclass Relationship < ApplicationRecord belongs_to :follower, class_name: "User" belongs_to :followed, class_name: "User" validates :follower_id, presence: true validates :followed_id, presence: trueend
Remove content of relationship fixtures
Remove content of relationship fixtures
test/fixtures/relationships.yml# empty
has_many :through
has_many :through
app/models/user.rbclass User < ApplicationRecord 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
- 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
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, 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, which explicitly tells Rails that the source of the following array is the set of followed ids.
- The association defined in this code 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, or find objects through the association:”
user.following.include?(other_user) user.following.find(other_user)
- We can also add and delete elements just as with arrays:
user.following << other_useruser.following.delete(other_user)
write a short test for the User model to test following mechanisms
write a short test for the User model to test following mechanisms
test/models/user_test.rbrequire '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) endend
- 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
- finally unfollow and verify that it worked.
write the follow, unfollow, and following? methods
write the follow, unfollow, and following? methods
app/models/user.rbclass User < ApplicationRecord . . . def feed . . . end # Follows a user. def follow(other_user) following << other_user end # Unfollows a user. def unfollow(other_user) following.delete(other_user) end # Returns true if the current user is following the other user. def following?(other_user) following.include?(other_user) end private . . .end
add a user.followers method to go with user.following
add a user.followers method to go with user.following
app/models/user.rbclass User < ApplicationRecord 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
- 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.
- It’s worth noting that we could actually omit the :source key for followers in the code, 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. But this code keeps the :source key to emphasize the parallel structure with the has_many :following association.
Add assertion to see if archers followers include michael in the test
Add assertion to see if archers followers include michael in the test
test/models/user_test.rbrequire '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) endend
arrange for the first user to follow users 3 through 51, and then have users 4 through 41 follow that user back in seed file
arrange for the first user to follow users 3 through 51, and then have users 4 through 41 follow that user back in seed file
db/seeds.rb# UsersUser.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# Micropostsusers = User.order(:created_at).take(6)50.times do content = Faker::Lorem.sentence(5) users.each { |user| user.microposts.create!(content: content) }end# Following relationshipsusers = User.alluser = users.firstfollowing = users[2..50]followers = users[3..40]following.each { |followed| user.follow(followed) }followers.each { |follower| follower.follow(user) }
Create routes for the followers and following links
Create routes for the followers and following links
Rails.application.routes.draw do root 'static_pages#home' get '/help', to: 'static_pages#help' get '/about', to: 'static_pages#about' get '/contact', to: 'static_pages#contact' get '/signup', to: 'users#new' get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: '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
- The URLs for following and followers will look like /users/1/following and /users/1/followers
- Since both pages will be showing data, the proper HTTP verb is a GET request, so we use the getmethod 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 the code below would respond to the URL /users/tigers (presumably to display all the tigers in our application).
resources :users do collection do get :tigers endend
Table of routes generated
Table of routes generated
HTTP | request | URL | Action | Named routeGET | /users/1/following | following | following_user_path(1)GET | /users/1/followers | followers | followers_user_path(1)
define the stats partial
define the stats partial
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 the code picks the right one using
<% @user ||= current_user %>
- 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
- 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 later, which will accesses elements on the page using their unique ids.
Add follower Stats to the homepage
Add follower Stats to the homepage
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 %>
style the stats
style the stats
app/assets/stylesheets/custom.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 */...
make a partial for the follow/unfollow button
make a partial for the follow/unfollow button
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