diff --git a/app/assets/stylesheets/custom.css.scss b/app/assets/stylesheets/custom.css.scss
index 11290ad..a16eab4 100644
--- a/app/assets/stylesheets/custom.css.scss
+++ b/app/assets/stylesheets/custom.css.scss
@@ -145,6 +145,35 @@ aside {
margin-right: 10px;
}
+.stats {
+ overflow: auto;
+ a {
+ float: left;
+ padding: 0 10px;
+ border-left: 1px solid $grayLighter;
+ 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;
+ }
+}
+
/* forms */
input, textarea, select, .uneditable-input {
diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb
new file mode 100644
index 0000000..fbadfec
--- /dev/null
+++ b/app/controllers/relationships_controller.rb
@@ -0,0 +1,21 @@
+class RelationshipsController < ApplicationController
+ before_action :signed_in_user
+
+ def create
+ @user = User.find(params[:relationship][: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
\ No newline at end of file
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 7f81eff..7db17c8 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -1,5 +1,6 @@
class UsersController < ApplicationController
- before_action :signed_in_user, only: [:index, :edit, :update, :destroy]
+ before_action :signed_in_user,
+ only: [:index, :edit, :update, :destroy, :following, :followers]
before_action :correct_user, only: [:edit, :update]
before_action :admin_user, only: :destroy
def index
@@ -40,6 +41,21 @@ def destroy
flash[:success] = "User deleted."
redirect_to users_url
end
+
+ def following
+ @title = "Following"
+ @user = User.find(params[:id])
+ @users = @user.followed_users.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/models/micropost.rb b/app/models/micropost.rb
index 70493fe..fd3a147 100644
--- a/app/models/micropost.rb
+++ b/app/models/micropost.rb
@@ -3,4 +3,11 @@ class Micropost < ActiveRecord::Base
default_scope -> { order('created_at DESC') }
validates :content, presence: true, length: { maximum: 140 }
validates :user_id, presence: true
+
+ def self.from_users_followed_by(user)
+ followed_user_ids = "SELECT followed_id FROM relationships
+ WHERE follower_id = :user_id"
+ where("user_id IN (#{followed_user_ids}) OR user_id = :user_id",
+ user_id: user.id)
+ end
end
\ No newline at end of file
diff --git a/app/models/relationship.rb b/app/models/relationship.rb
new file mode 100644
index 0000000..deb76a8
--- /dev/null
+++ b/app/models/relationship.rb
@@ -0,0 +1,6 @@
+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
diff --git a/app/models/user.rb b/app/models/user.rb
index 3dede30..bc29e56 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,5 +1,12 @@
class User < ActiveRecord::Base
has_many :microposts, dependent: :destroy
+ has_many :relationships, foreign_key: "follower_id", dependent: :destroy
+ has_many :followed_users, through: :relationships, source: :followed
+ has_many :reverse_relationships, foreign_key: "followed_id",
+ class_name: "Relationship",
+ dependent: :destroy
+ has_many :followers, through: :reverse_relationships, source: :follower
+
before_save { self.email = email.downcase }
before_create :create_remember_token
validates :name, presence: true, length: { maximum: 50 }
@@ -19,8 +26,19 @@ def User.encrypt(token)
end
def feed
- # This is preliminary. See "Following users" for the full implementation.
- Micropost.where("user_id = ?", id)
+ Micropost.from_users_followed_by(self)
+ end
+
+ def following?(other_user)
+ self.relationships.find_by(followed_id: other_user.id)
+ end
+
+ def follow!(other_user)
+ self.relationships.create!(followed_id: other_user.id)
+ end
+
+ def unfollow!(other_user)
+ self.relationships.find_by(followed_id: other_user.id).destroy!
end
private
diff --git a/app/views/relationships/create.js.erb b/app/views/relationships/create.js.erb
new file mode 100644
index 0000000..d5509e8
--- /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 %>')
\ No newline at end of file
diff --git a/app/views/relationships/destroy.js.erb b/app/views/relationships/destroy.js.erb
new file mode 100644
index 0000000..fa23b16
--- /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 %>')
\ No newline at end of file
diff --git a/app/views/shared/_stats.html.erb b/app/views/shared/_stats.html.erb
new file mode 100644
index 0000000..e68c491
--- /dev/null
+++ b/app/views/shared/_stats.html.erb
@@ -0,0 +1,15 @@
+<% @user ||= current_user %>
+
\ No newline at end of file
diff --git a/app/views/static_pages/home.html.erb b/app/views/static_pages/home.html.erb
index eaf406c..9fa38d7 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 0000000..021ed24
--- /dev/null
+++ b/app/views/users/_follow.html.erb
@@ -0,0 +1,4 @@
+<%= form_for(current_user.relationships.build(followed_id: @user.id)) do |f| %>
+ <%= f.hidden_field :followed_id %>
+ <%= f.submit "Follow", class: "btn btn-large btn-primary" %>
+<% end %>
\ No newline at end of file
diff --git a/app/views/users/_follow_form.html.erb b/app/views/users/_follow_form.html.erb
new file mode 100644
index 0000000..3bc7433
--- /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 %>
\ No newline at end of file
diff --git a/app/views/users/_unfollow.html.erb b/app/views/users/_unfollow.html.erb
new file mode 100644
index 0000000..7773024
--- /dev/null
+++ b/app/views/users/_unfollow.html.erb
@@ -0,0 +1,4 @@
+<%= form_for(current_user.relationships.find_by(followed_id: @user),
+ html: { method: :delete }) do |f| %>
+ <%= f.submit "Unfollow", class: "btn btn-large" %>
+<% end %>
\ No newline at end of file
diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb
index 7b2c264..26f2074 100644
--- a/app/views/users/show.html.erb
+++ b/app/views/users/show.html.erb
@@ -7,9 +7,13 @@
<%= @user.name %>
+
+ <%= render 'shared/stats' %>
+
<% if @user.microposts.any? %>
+ <%= render 'follow_form' if signed_in? %>
Microposts (<%= @user.microposts.count %>)
<%= render @microposts %>
diff --git a/app/views/users/show_follow.html.erb b/app/views/users/show_follow.html.erb
new file mode 100644
index 0000000..c60e3d6
--- /dev/null
+++ b/app/views/users/show_follow.html.erb
@@ -0,0 +1,30 @@
+<% provide(:title, @title) %>
+
+
+
+
<%= @title %>
+ <% if @users.any? %>
+
+ <%= will_paginate %>
+ <% end %>
+
+
\ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index 60f7c41..a4891f6 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,7 +1,12 @@
SampleApp::Application.routes.draw do
- resources :users
+ resources :users do
+ member do
+ get :following, :followers
+ end
+ end
resources :sessions, only: [:new, :create, :destroy]
resources :microposts, only: [:create, :destroy]
+ resources :relationships, only: [:create, :destroy]
root 'static_pages#home'
match '/signup', to: 'users#new', via: 'get'
match '/signin', to: 'sessions#new', via: 'get'
diff --git a/db/migrate/20131230170729_create_relationships.rb b/db/migrate/20131230170729_create_relationships.rb
new file mode 100644
index 0000000..fe36b5a
--- /dev/null
+++ b/db/migrate/20131230170729_create_relationships.rb
@@ -0,0 +1,13 @@
+class CreateRelationships < ActiveRecord::Migration
+ 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 ef92f40..3dd8102 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20131227202627) do
+ActiveRecord::Schema.define(version: 20131230170729) do
create_table "microposts", force: true do |t|
t.string "content"
@@ -22,6 +22,17 @@
add_index "microposts", ["user_id", "created_at"], name: "index_microposts_on_user_id_and_created_at"
+ create_table "relationships", force: true do |t|
+ t.integer "follower_id"
+ t.integer "followed_id"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "relationships", ["followed_id"], name: "index_relationships_on_followed_id"
+ add_index "relationships", ["follower_id", "followed_id"], name: "index_relationships_on_follower_id_and_followed_id", unique: true
+ add_index "relationships", ["follower_id"], name: "index_relationships_on_follower_id"
+
create_table "users", force: true do |t|
t.string "name"
t.string "email"
diff --git a/lib/tasks/sample_data.rake b/lib/tasks/sample_data.rake
index a5ce24d..9263350 100644
--- a/lib/tasks/sample_data.rake
+++ b/lib/tasks/sample_data.rake
@@ -1,28 +1,42 @@
namespace :db do
desc "Fill database with sample data"
task populate: :environment do
- admin = User.create!(name: "Behdad Baniani",
- email: "baniani@gmail.com",
- password: "foobar",
- password_confirmation: "foobar",
- admin: true)
- User.create!(name: "Example User",
- email: "example@railstutorial.org",
- password: "foobar",
- password_confirmation: "foobar")
- 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)
- end
- users = User.all(limit: 6)
- 50.times do
- content = Faker::Lorem.sentence(5)
- users.each { |user| user.microposts.create!(content: content) }
- end
+ make_users
+ make_microposts
+ make_relationships
end
+end
+
+def make_users
+ admin = User.create!(name: "Example User",
+ email: "example@railstutorial.org",
+ password: "foobar",
+ password_confirmation: "foobar",
+ admin: true)
+ 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)
+ end
+end
+
+def make_microposts
+ users = User.all(limit: 6)
+ 50.times do
+ content = Faker::Lorem.sentence(5)
+ users.each { |user| user.microposts.create!(content: content) }
+ end
+end
+
+def make_relationships
+ users = User.all
+ user = users.first
+ followed_users = users[2..50]
+ followers = users[3..40]
+ followed_users.each { |followed| user.follow!(followed) }
+ followers.each { |follower| follower.follow!(user) }
end
\ No newline at end of file
diff --git a/spec/controllers/relationships_controller_spec.rb b/spec/controllers/relationships_controller_spec.rb
new file mode 100644
index 0000000..e08d550
--- /dev/null
+++ b/spec/controllers/relationships_controller_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe RelationshipsController do
+
+ let(:user) { FactoryGirl.create(:user) }
+ let(:other_user) { FactoryGirl.create(:user) }
+
+ before { sign_in user, no_capybara: true }
+
+ describe "creating a relationship with Ajax" do
+
+ it "should increment the Relationship count" do
+ expect do
+ xhr :post, :create, relationship: { followed_id: other_user.id }
+ end.to change(Relationship, :count).by(1)
+ end
+
+ it "should respond with success" do
+ xhr :post, :create, relationship: { followed_id: other_user.id }
+ expect(response).to be_success
+ end
+ end
+
+ describe "destroying a relationship with Ajax" do
+
+ before { user.follow!(other_user) }
+ let(:relationship) { user.relationships.find_by(followed_id: other_user) }
+
+ it "should decrement the Relationship count" do
+ expect do
+ xhr :delete, :destroy, id: relationship.id
+ end.to change(Relationship, :count).by(-1)
+ end
+
+ it "should respond with success" do
+ xhr :delete, :destroy, id: relationship.id
+ expect(response).to be_success
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/models/relationships_spec.rb b/spec/models/relationships_spec.rb
new file mode 100644
index 0000000..00f9d9a
--- /dev/null
+++ b/spec/models/relationships_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Relationships do
+
+ let(:follower) { FactoryGirl.create(:user) }
+ let(:followed) { FactoryGirl.create(:user) }
+ let(:relationship) { follower.relationships.build(followed_id: followed.id) }
+
+ subject { relationship }
+
+ it { should be_valid }
+
+ describe "follower methods" do
+ it { should respond_to(:follower) }
+ it { should respond_to(:followed) }
+ its(:follower) { should eq follower }
+ its(:followed) { should eq followed }
+ end
+
+ describe "when followed id is not present" do
+ before { relationship.followed_id = nil }
+ it { should_not be_valid }
+ end
+
+ describe "when follower id is not present" do
+ before { relationship.follower_id = nil }
+ it { should_not be_valid }
+ end
+end
\ No newline at end of file
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 2b63187..7312a68 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -17,6 +17,10 @@
it { should respond_to(:admin) }
it { should respond_to(:microposts) }
it { should respond_to(:feed) }
+ it { should respond_to(:relationships) }
+ it { should respond_to(:followed_users) }
+ it { should respond_to(:reverse_relationships) }
+ it { should respond_to(:followers) }
it { should be_valid }
it { should_not be_admin }
@@ -148,5 +152,48 @@
its(:feed) { should include(older_micropost) }
its(:feed) { should_not include(unfollowed_post) }
end
+
+ describe "status" do
+ let(:unfollowed_post) do
+ FactoryGirl.create(:micropost, user: FactoryGirl.create(:user))
+ end
+ let(:followed_user) { FactoryGirl.create(:user) }
+
+ before do
+ @user.follow!(followed_user)
+ 3.times { followed_user.microposts.create!(content: "Lorem ipsum") }
+ end
+
+ its(:feed) { should include(newer_micropost) }
+ its(:feed) { should include(older_micropost) }
+ its(:feed) { should_not include(unfollowed_post) }
+ its(:feed) do
+ followed_user.microposts.each do |micropost|
+ should include(micropost)
+ end
+ end
+ end
+ end
+ describe "following" do
+ let(:other_user) { FactoryGirl.create(:user) }
+ before do
+ @user.save
+ @user.follow!(other_user)
+ end
+
+ it { should be_following(other_user) }
+ its(:followed_users) { should include(other_user) }
+
+ describe "followed user" do
+ subject { other_user }
+ its(:followers) { should include(@user) }
+ end
+
+ describe "and unfollowing" do
+ before { @user.unfollow!(other_user) }
+
+ it { should_not be_following(other_user) }
+ its(:followed_users) { should_not include(other_user) }
+ end
end
end
\ No newline at end of file
diff --git a/spec/requests/authentication_pages_spec.rb b/spec/requests/authentication_pages_spec.rb
index 53e174e..690e53c 100644
--- a/spec/requests/authentication_pages_spec.rb
+++ b/spec/requests/authentication_pages_spec.rb
@@ -58,6 +58,18 @@
expect(page).to have_title('Edit user')
end
end
+
+ describe "in the Relationships controller" do
+ describe "submitting to the create action" do
+ before { post relationships_path }
+ specify { expect(response).to redirect_to(signin_path) }
+ end
+
+ describe "submitting to the destroy action" do
+ before { delete relationship_path(1) }
+ specify { expect(response).to redirect_to(signin_path) }
+ end
+ end
end
describe "in the Microposts controller" do
@@ -89,6 +101,16 @@
before { visit users_path }
it { should have_title('Sign in') }
end
+
+ describe "visiting the following page" do
+ before { visit following_user_path(user) }
+ it { should have_title('Sign in') }
+ end
+
+ describe "visiting the followers page" do
+ before { visit followers_user_path(user) }
+ it { should have_title('Sign in') }
+ end
end
end
describe "as wrong user" do
diff --git a/spec/requests/static_pages_spec.rb b/spec/requests/static_pages_spec.rb
index 4e6df49..56bf8aa 100644
--- a/spec/requests/static_pages_spec.rb
+++ b/spec/requests/static_pages_spec.rb
@@ -14,8 +14,8 @@
describe "for signed-in users" do
let(:user) { FactoryGirl.create(:user) }
before do
- FactoryGirl.create(:micropost, user: user, content: "Lorem ipsum")
- FactoryGirl.create(:micropost, user: user, content: "Dolor sit amet")
+ FactoryGirl.create(:micropost, user: user, content: "Lorem")
+ FactoryGirl.create(:micropost, user: user, content: "Ipsum")
sign_in user
visit root_path
end
@@ -25,7 +25,19 @@
expect(page).to have_selector("li##{item.id}", text: item.content)
end
end
+
+ describe "follower/following counts" do
+ let(:other_user) { FactoryGirl.create(:user) }
+ before do
+ other_user.follow!(user)
+ visit root_path
+ end
+
+ it { should have_link("0 following", href: following_user_path(user)) }
+ it { should have_link("1 followers", href: followers_user_path(user)) }
+ end
end
+
end
describe "Help page" do
diff --git a/spec/requests/user_pages_spec.rb b/spec/requests/user_pages_spec.rb
index ffec756..9a3aa9c 100644
--- a/spec/requests/user_pages_spec.rb
+++ b/spec/requests/user_pages_spec.rb
@@ -72,6 +72,55 @@
it { should have_content(m2.content) }
it { should have_content(user.microposts.count) }
end
+ describe "follow/unfollow buttons" do
+ let(:other_user) { FactoryGirl.create(:user) }
+ before { sign_in user }
+
+ describe "following a user" do
+ before { visit user_path(other_user) }
+
+ it "should increment the followed user count" do
+ expect do
+ click_button "Follow"
+ end.to change(user.followed_users, :count).by(1)
+ end
+
+ it "should increment the other user's followers count" do
+ expect do
+ click_button "Follow"
+ end.to change(other_user.followers, :count).by(1)
+ end
+
+ describe "toggling the button" do
+ before { click_button "Follow" }
+ it { should have_xpath("//input[@value='Unfollow']") }
+ end
+ end
+
+ describe "unfollowing a user" do
+ before do
+ user.follow!(other_user)
+ visit user_path(other_user)
+ end
+
+ it "should decrement the followed user count" do
+ expect do
+ click_button "Unfollow"
+ end.to change(user.followed_users, :count).by(-1)
+ end
+
+ it "should decrement the other user's followers count" do
+ expect do
+ click_button "Unfollow"
+ end.to change(other_user.followers, :count).by(-1)
+ end
+
+ describe "toggling the button" do
+ before { click_button "Unfollow" }
+ it { should have_xpath("//input[@value='Follow']") }
+ end
+ end
+ end
end
describe "signup page" do
@@ -149,4 +198,33 @@
specify { expect(user.reload.email).to eq new_email }
end
end
+
+ describe "following/followers" do
+ let(:user) { FactoryGirl.create(:user) }
+ let(:other_user) { FactoryGirl.create(:user) }
+ before { user.follow!(other_user) }
+
+ describe "followed users" do
+ before do
+ sign_in user
+ visit following_user_path(user)
+ end
+
+ it { should have_title(full_title('Following')) }
+ it { should have_selector('h3', text: 'Following') }
+ it { should have_link(other_user.name, href: user_path(other_user)) }
+ end
+
+ describe "followers" do
+ before do
+ sign_in other_user
+ visit followers_user_path(other_user)
+ end
+
+ it { should have_title(full_title('Followers')) }
+ it { should have_selector('h3', text: 'Followers') }
+ it { should have_link(user.name, href: user_path(user)) }
+ end
+ end
+
end
\ No newline at end of file