diff --git a/app/assets/javascripts/relationships.coffee b/app/assets/javascripts/relationships.coffee
new file mode 100644
index 0000000000000000000000000000000000000000..24f83d18bbd38c24c4f7c3c2fc360cd68e857a2a
--- /dev/null
+++ b/app/assets/javascripts/relationships.coffee
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://coffeescript.org/
diff --git a/app/assets/stylesheets/custom.scss b/app/assets/stylesheets/custom.scss
index 8a48231ec731e52f8b29ea89b2d1a4a37b6efbaa..83536672a299f270fe15c90bc8436a6a7e3c9676 100644
--- a/app/assets/stylesheets/custom.scss
+++ b/app/assets/stylesheets/custom.scss
@@ -150,6 +150,44 @@ aside {
margin-right: 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 */
input, textarea, select, .uneditable-input {
diff --git a/app/assets/stylesheets/relationships.scss b/app/assets/stylesheets/relationships.scss
new file mode 100644
index 0000000000000000000000000000000000000000..ca5c640489227bc717f3cd00e8fac8fe4c321a40
--- /dev/null
+++ b/app/assets/stylesheets/relationships.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the Relationships controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e9dbff98766071045f36e005060c139bd63f5c88
--- /dev/null
+++ b/app/controllers/relationships_controller.rb
@@ -0,0 +1,21 @@
+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
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 70dc406446b95ca44af51353d63261e4ba2bf1c0..a85da675a69a3cb2777da423bbe0b3306994b9bc 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -1,5 +1,6 @@
class UsersController < ApplicationController
- before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
+ before_action :logged_in_user, only: [:index, :edit, :update, :destroy,
+ :following, :followers]
before_action :correct_user, only: [:edit, :update]
before_action :admin_user, only: :destroy
@@ -48,6 +49,20 @@ class UsersController < ApplicationController
redirect_to users_url
end
+ 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
def user_params
diff --git a/app/helpers/relationships_helper.rb b/app/helpers/relationships_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3b96a9c022f134dc01c3b3dc085173ee6c95db72
--- /dev/null
+++ b/app/helpers/relationships_helper.rb
@@ -0,0 +1,2 @@
+module RelationshipsHelper
+end
diff --git a/app/models/relationship.rb b/app/models/relationship.rb
new file mode 100644
index 0000000000000000000000000000000000000000..027ce413bd8861db4eb56a721500b56a608531c7
--- /dev/null
+++ b/app/models/relationship.rb
@@ -0,0 +1,6 @@
+class Relationship < ApplicationRecord
+ belongs_to :follower, class_name: "User"
+ belongs_to :followed, class_name: "User"
+ validates :follower_id, presence: true
+ validates :followed_id, presence: true
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index da2bd215f1a309c04fb5747d1eb74f4341049194..88ac923245c3696cbebba1d861ec377bdaa05c43 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,5 +1,13 @@
class 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
attr_accessor :remember_token, :activation_token, :activation_token, :reset_token
before_save :downcase_email
@@ -60,7 +68,20 @@ class User < ApplicationRecord
end
def feed
- Micropost.where("user_id = ?", id)
+ 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
+
+ def follow(other_user)
+ active_relationships.create(followed_id: other_user.id)
+ end
+
+ def unfollow(other_user)
+ active_relationships.find_by(followed_id: other_user.id).destroy
+ end
+
+ def following?(other_user)
+ following.include?(other_user)
end
private
diff --git a/app/views/relationships/create.js.erb b/app/views/relationships/create.js.erb
new file mode 100644
index 0000000000000000000000000000000000000000..b9f9f7aefbf3701ae7595ec377ebd6c9700249ff
--- /dev/null
+++ b/app/views/relationships/create.js.erb
@@ -0,0 +1,2 @@
+$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>");
+$("#followers").html('<%= @user.followers.count %>');
diff --git a/app/views/relationships/destroy.js.erb b/app/views/relationships/destroy.js.erb
new file mode 100644
index 0000000000000000000000000000000000000000..3c45f4d79bc285e223cb8cb5d64f64fcddc1493f
--- /dev/null
+++ b/app/views/relationships/destroy.js.erb
@@ -0,0 +1,2 @@
+$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>");
+$("#followers").html('<%= @user.followers.count %>');
diff --git a/app/views/shared/_stats.html.erb b/app/views/shared/_stats.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..d60c3f4f05df6cdea6644d031049121ebfd1fc52
--- /dev/null
+++ b/app/views/shared/_stats.html.erb
@@ -0,0 +1,15 @@
+<% @user ||= current_user %>
+
diff --git a/app/views/static_pages/home.html.erb b/app/views/static_pages/home.html.erb
index 63d50b959b57f88b215b779315880925ee6f8677..804b9b136338ea24007c4e688211635c34165389 100644
--- a/app/views/static_pages/home.html.erb
+++ b/app/views/static_pages/home.html.erb
@@ -4,6 +4,9 @@
<%= render 'shared/user_info' %>
+
+ <%= render 'shared/stats' %>
+
<%= render 'shared/micropost_form' %>
diff --git a/app/views/users/_follow.html.erb b/app/views/users/_follow.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..bcd4859b6bb079c73d0e17d16a21aa1cb51e93a8
--- /dev/null
+++ b/app/views/users/_follow.html.erb
@@ -0,0 +1,4 @@
+<%= form_for(current_user.active_relationships.build, remote: true) do |f| %>
+ <%= hidden_field_tag :followed_id, @user.id %>
+ <%= f.submit 'Follow', class: "btn btn-primary" %>
+<% end %>
diff --git a/app/views/users/_follow_form.html.erb b/app/views/users/_follow_form.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..0b17f7bca11f264d714b19126479cfa53d114a9d
--- /dev/null
+++ b/app/views/users/_follow_form.html.erb
@@ -0,0 +1,9 @@
+<% unless current_user?(@user) %>
+
+ <% if current_user.following?(@user) %>
+ <%= render 'unfollow' %>
+ <% else %>
+ <%= render 'follow' %>
+ <% end %>
+
+<% end %>
diff --git a/app/views/users/_unfollow.html.erb b/app/views/users/_unfollow.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..8ecf1f8016df4c33ae7f5684e09619699a6661ef
--- /dev/null
+++ b/app/views/users/_unfollow.html.erb
@@ -0,0 +1,5 @@
+<%= 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 %>
diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb
index 9065d9a113fa0b875d40bd447e9e22ea98639b3d..27c2cae2a0b7ceebb60e2291d4062133f7fbb999 100644
--- a/app/views/users/show.html.erb
+++ b/app/views/users/show.html.erb
@@ -7,8 +7,12 @@
<%= @user.name %>
+
+ <%= render 'shared/stats' %>
+
+ <%= render 'follow_form' if logged_in? %>
<% if @user.microposts.any? %>
Microposts (<%= @user.microposts.count %>)
diff --git a/app/views/users/show_follow.html.erb b/app/views/users/show_follow.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..3690cbb56c68bb151ccdf0f3dfe08cc83867734e
--- /dev/null
+++ b/app/views/users/show_follow.html.erb
@@ -0,0 +1,30 @@
+<% provide(:title, @title) %>
+
+
+
+
<%= @title %>
+ <% if @users.any? %>
+
+ <%= will_paginate %>
+ <% end %>
+
+
diff --git a/config/application.rb b/config/application.rb
index 4b92804e0cec1660b3d0262d91503085b9991214..c9e7a2ea7b358037b1e5b932d126b63984557204 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -11,5 +11,6 @@ module SampleApp
# Settings in config/environments/* take precedence over those specified here.
# Application configuration should go into files in config/initializers
# -- all .rb files in that directory are automatically loaded.
+ config.action_view.embed_authenticity_token_in_remote_forms = true
end
end
diff --git a/config/routes.rb b/config/routes.rb
index 3992b3c13a266cd21ae608edc617432171a45ee6..231cf4dd8d4bb09dec5a3cf225ef6f20101c0c6a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -14,8 +14,13 @@ Rails.application.routes.draw do
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
delete '/logout', to: 'sessions#destroy'
- resources :users
+ 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
diff --git a/db/migrate/20170303152632_create_relationships.rb b/db/migrate/20170303152632_create_relationships.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4f0648f313efb6d7964e1f4a785c4bd54b24daa5
--- /dev/null
+++ b/db/migrate/20170303152632_create_relationships.rb
@@ -0,0 +1,13 @@
+class 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
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index e0cb80dbb09cd60058b35ad867cc4d0535970910..bc1da47efd0df4ee4a82a563d56305e32de95e78 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20170302144658) do
+ActiveRecord::Schema.define(version: 20170303152632) do
create_table "microposts", force: :cascade do |t|
t.text "content"
@@ -22,6 +22,16 @@ ActiveRecord::Schema.define(version: 20170302144658) do
t.index ["user_id"], name: "index_microposts_on_user_id"
end
+ create_table "relationships", force: :cascade do |t|
+ t.integer "follower_id"
+ t.integer "followed_id"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["followed_id"], name: "index_relationships_on_followed_id"
+ t.index ["follower_id", "followed_id"], name: "index_relationships_on_follower_id_and_followed_id", unique: true
+ t.index ["follower_id"], name: "index_relationships_on_follower_id"
+ end
+
create_table "users", force: :cascade do |t|
t.string "name"
t.string "email"
diff --git a/db/seeds.rb b/db/seeds.rb
index c13912411b6b296aa8fd1a56d2065b2b6568fcd4..2db0ecbf363a6b7e9e78b28de2ade2570f0fc613 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -1,11 +1,4 @@
-# This file should contain all the record creation needed to seed the database with its default values.
-# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup).
-#
-# Examples:
-#
-# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
-# Character.create(name: 'Luke', movie: movies.first)
-
+# User
User.create!(name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
@@ -26,8 +19,17 @@ User.create!(name: "Example User",
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) }
diff --git a/test/controllers/relationships_controller_test.rb b/test/controllers/relationships_controller_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4db36216c6ef5baec8064af97a2b27e5fcb24015
--- /dev/null
+++ b/test/controllers/relationships_controller_test.rb
@@ -0,0 +1,17 @@
+require 'test_helper'
+
+class RelationshipsControllerTest < ActionDispatch::IntegrationTest
+ test "create should require logged-in user" do
+ assert_no_difference 'Relationship.count' do
+ post relationships_path
+ end
+ assert_redirected_to login_url
+ end
+
+ test "destroy should require logged-in user" do
+ assert_no_difference 'Relationship.count' do
+ delete relationship_path(relationships(:one))
+ end
+ assert_redirected_to login_url
+ end
+end
diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb
index b3e2762d839a3db5eab85f71b7618344d32bc83f..67339377871336ae5bbd51d4611679bfb27198fd 100644
--- a/test/controllers/users_controller_test.rb
+++ b/test/controllers/users_controller_test.rb
@@ -47,4 +47,14 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
end
assert_redirected_to root_url
end
+
+ test "should redirect following when not logged in" do
+ get following_user_path(@user)
+ assert_redirected_to login_url
+ end
+
+ test "should redirect followers when not logged in" do
+ get followers_user_path(@user)
+ assert_redirected_to login_url
+ end
end
diff --git a/test/fixtures/relationships.yml b/test/fixtures/relationships.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b567e7152c10b1d622cd2e101ca36a2f027c84dd
--- /dev/null
+++ b/test/fixtures/relationships.yml
@@ -0,0 +1,15 @@
+one:
+ follower: michael
+ followed: lana
+
+two:
+ follower: michael
+ followed: malory
+
+three:
+ follower: lana
+ followed: michael
+
+four:
+ follower: archer
+ followed: michael
diff --git a/test/integration/following_test.rb b/test/integration/following_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5daa8c08b03965a6227b4255bbe70eca20470ae5
--- /dev/null
+++ b/test/integration/following_test.rb
@@ -0,0 +1,55 @@
+require 'test_helper'
+
+class FollowingTest < ActionDispatch::IntegrationTest
+ def setup
+ @user = users(:michael)
+ @other = users(:archer)
+ 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
+
+ test "should follow a user the standard way" do
+ assert_difference '@user.following.count', 1 do
+ post relationships_path, params: { followed_id: @other.id }
+ end
+ end
+
+ test "should follow a user with Ajax" do
+ assert_difference '@user.following.count', 1 do
+ post relationships_path, xhr: true, params: { 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
+ delete relationship_path(relationship), xhr: true
+ end
+ end
+end
diff --git a/test/models/relationship_test.rb b/test/models/relationship_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bbd5f22fbbe1ebb09bf583f4a40f09fe0e7ebb92
--- /dev/null
+++ b/test/models/relationship_test.rb
@@ -0,0 +1,22 @@
+require '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?
+ end
+end
diff --git a/test/models/user_test.rb b/test/models/user_test.rb
index 97f3ce984b0ebefceafe6b8c2827c5878da24380..0204fe4891a349bd2c0b85271ffb499fcfb6dd44 100644
--- a/test/models/user_test.rb
+++ b/test/models/user_test.rb
@@ -83,4 +83,32 @@ class UserTest < ActiveSupport::TestCase
@user.destroy
end
end
+
+ 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
+
+ test "feed should have the right posts" do
+ michael = users(:michael)
+ archer = users(:archer)
+ lana = users(:lana)
+ lana.microposts.each do |post_following|
+ assert michael.feed.include?(post_following)
+ end
+
+ michael.microposts.each do |post_self|
+ assert michael.feed.include?(post_self)
+ end
+
+ archer.microposts.each do |post_unfollowed|
+ assert_not michael.feed.include?(post_unfollowed)
+ end
+ end
end