Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Add feed

  • Loading branch information...
commit c511ed4c7f44df6ccbdac14ad05a797f1c3214c8 1 parent be159fe
@etrainey authored
Showing with 470 additions and 48 deletions.
  1. +21 −0 app/controllers/relationships_controller.rb
  2. +15 −5 app/controllers/users_controller.rb
  3. +16 −2 app/models/micropost.rb
  4. +9 −0 app/models/relationship.rb
  5. +22 −2 app/models/user.rb
  6. +2 −0  app/views/relationships/create.js.erb
  7. +2 −0  app/views/relationships/destroy.js.erb
  8. +1 −1  app/views/shared/_feed_item.html.erb
  9. +21 −0 app/views/shared/_stats.html.erb
  10. +1 −0  app/views/static_pages/home.html.erb
  11. +5 −0 app/views/users/_follow.html.erb
  12. +9 −0 app/views/users/_follow_form.html.erb
  13. +5 −0 app/views/users/_unfollow.html.erb
  14. +2 −0  app/views/users/show.html.erb
  15. +25 −0 app/views/users/show_follow.html.erb
  16. +8 −2 config/routes.rb
  17. +14 −0 db/migrate/20120215045003_create_relationships.rb
  18. +12 −1 db/schema.rb
  19. +36 −19 lib/tasks/sample_data.rake
  20. +29 −0 spec/controllers/relationships_controller_spec.rb
  21. +19 −0 spec/models/micropost_spec.rb
  22. +33 −0 spec/models/relationship_spec.rb
  23. +41 −2 spec/models/user_spec.rb
  24. +33 −11 spec/requests/authentication_pages_spec.rb
  25. +12 −2 spec/requests/static_pages_spec.rb
  26. +77 −1 spec/requests/user_pages_spec.rb
View
21 app/controllers/relationships_controller.rb
@@ -0,0 +1,21 @@
+class RelationshipsController < ApplicationController
+ before_filter :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
View
20 app/controllers/users_controller.rb
@@ -1,12 +1,8 @@
class UsersController < ApplicationController
- before_filter :signed_in_user, only: [:index, :edit, :update, :destroy]
+ before_filter :signed_in_user, only: [:index, :edit, :update, :following, :followers]
before_filter :correct_user, only: [:edit, :update]
before_filter :admin_user, only: :destroy
- def show
- @user = User.find(params[:id])
- end
-
def new
@user = User.new
end
@@ -49,6 +45,20 @@ def show
@microposts = @user.microposts.paginate(page: params[:page])
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 correct_user
View
18 app/models/micropost.rb
@@ -5,6 +5,20 @@ class Micropost < ActiveRecord::Base
validates :content, presence: true, length: { maximum: 140 }
validates :user_id, presence: true
-
- default_scope order: 'microposts.created_at DESC'
+
+ default_scope :order => 'microposts.created_at DESC'
+
+ # Returns microposts from the users being followed by the given user.
+ scope :from_users_followed_by, lambda { |user| followed_by(user) }
+
+ private
+
+ # Returns an SQL condition for users followed by the given user.
+ # We include the user's own id as well.
+ def self.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 })
+ end
end
View
9 app/models/relationship.rb
@@ -0,0 +1,9 @@
+class Relationship < ActiveRecord::Base
+ attr_accessible :followed_id
+
+ belongs_to :follower, class_name: "User"
+ belongs_to :followed, class_name: "User"
+
+ validates :follower_id, presence: true
+ validates :followed_id, presence: true
+end
View
24 app/models/user.rb
@@ -1,7 +1,16 @@
class User < ActiveRecord::Base
attr_accessible :name, :email, :password, :password_confirmation
+
has_secure_password
+
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 :create_remember_token
validates :name, presence: true, length: { maximum: 50 }
@@ -12,8 +21,19 @@ class User < ActiveRecord::Base
validates :password, length: { minimum: 6}
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)
+ relationships.find_by_followed_id(other_user.id)
+ end
+
+ def follow!(other_user)
+ relationships.create!(followed_id: other_user.id)
+ end
+
+ def unfollow!(other_user)
+ relationships.find_by_followed_id(other_user.id).destroy
end
private
View
2  app/views/relationships/create.js.erb
@@ -0,0 +1,2 @@
+$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>")
+$("#followers").html('<%= pluralize(@user.followers.count, "follower") %>')
View
2  app/views/relationships/destroy.js.erb
@@ -0,0 +1,2 @@
+$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>")
+$("#followers").html('<%= pluralize(@user.followers.count, "follower") %>')
View
2  app/views/shared/_feed_item.html.erb
@@ -1,4 +1,4 @@
-<tr>
+<tr id="<%= feed_item.id %>">
<td class="gravatar">
<%= link_to gravatar_for(feed_item.user), feed_item.user %>
</td>
View
21 app/views/shared/_stats.html.erb
@@ -0,0 +1,21 @@
+<% @user ||= current_user %>
+<div class="stats">
+ <table>
+ <tr>
+ <td>
+ <a href="<%= following_user_path(@user) %>">
+ <span id="following" class="stat">
+ <%= @user.followed_users.count %> following
+ </span>
+ </a>
+ </td>
+ <td>
+ <a href="<%= followers_user_path(@user) %>">
+ <span id="followers" class="stat">
+ <%= pluralize(@user.followers.count, "follower") %>
+ </span>
+ </a>
+ </td>
+ </tr>
+ </table>
+</div>
View
1  app/views/static_pages/home.html.erb
@@ -9,6 +9,7 @@
</td>
<td class="sidebar round">
<%= render 'shared/user_info' %>
+ <%= render 'shared/stats' %>
</td>
</tr>
</table>
View
5 app/views/users/_follow.html.erb
@@ -0,0 +1,5 @@
+<%= form_for current_user.relationships.build(followed_id: @user.id),
+ remote: true do |f| %>
+ <div><%= f.hidden_field :followed_id %></div>
+ <div class="actions"><%= f.submit "Follow" %></div>
+<% end %>
View
9 app/views/users/_follow_form.html.erb
@@ -0,0 +1,9 @@
+<% unless current_user?(@user) %>
+ <div id="follow_form">
+ <% if current_user.following?(@user) %>
+ <%= render 'unfollow' %>
+ <% else %>
+ <%= render 'follow' %>
+ <% end %>
+ </div>
+<% end %>
View
5 app/views/users/_unfollow.html.erb
@@ -0,0 +1,5 @@
+<%= form_for current_user.relationships.find_by_followed_id(@user),
+ html: { method: :delete },
+ remote: true do |f| %>
+ <div class="actions"><%= f.submit "Unfollow" %></div>
+<% end %>
View
2  app/views/users/show.html.erb
@@ -6,6 +6,7 @@
<%= gravatar_for @user %>
<%= @user.name %>
</h1>
+ <%= render 'follow_form' if signed_in? %>
<% if @user.microposts.any? %>
<table class="microposts">
<%= render @microposts %>
@@ -17,6 +18,7 @@
<strong>Name</strong> <%= @user.name %><br />
<strong>URL</strong> <%= link_to user_path(@user), @user %><br />
<strong>Microposts</strong> <%= @user.microposts.count %>
+ <%= render 'shared/stats' %>
</td>
</tr>
</table>
View
25 app/views/users/show_follow.html.erb
@@ -0,0 +1,25 @@
+<table>
+ <tr>
+ <td class="main">
+ <h1><%= @title %></h1>
+
+ <% if @users.any? %>
+ <ul class="users">
+ <%= render @users %>
+ </ul>
+ <%= will_paginate @users %>
+ <% end %>
+ </td>
+ <td class="sidebar round">
+ <strong>Name</strong> <%= @user.name %><br />
+ <strong>URL</strong> <%= link_to user_path(@user), @user %><br />
+ <strong>Microposts</strong> <%= @user.microposts.count %>
+ <%= render 'shared/stats' %>
+ <% unless @users.empty? %>
+ <% @users.each do |user| %>
+ <%= link_to gravatar_for(user, size: 30), user %>
+ <% end %>
+ <% end %>
+ </td>
+ </tr>
+</table>
View
10 config/routes.rb
@@ -1,7 +1,13 @@
KibanaApp::Application.routes.draw do
resources :users
- resources :sessions, only: [:new, :create, :destroy]
- resources :microposts, only: [:create, :destroy]
+ 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]
match '/signup', to: 'users#new'
match '/signin', to: 'sessions#new'
View
14 db/migrate/20120215045003_create_relationships.rb
@@ -0,0 +1,14 @@
+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
View
13 db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended to check this file into your version control system.
-ActiveRecord::Schema.define(:version => 20120214043347) do
+ActiveRecord::Schema.define(:version => 20120215045003) 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", :null => false
+ t.datetime "updated_at", :null => false
+ 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"
View
55 lib/tasks/sample_data.rake
@@ -2,25 +2,42 @@ namespace :db do
desc "Fill database with sample data"
task populate: :environment do
Rake::Task['db:reset'].invoke
- admin = User.create!(name: "Example User",
- email: "example@railstutorial.org",
- password: "foobar",
- password_confirmation: "foobar")
- admin.toggle!(:admin)
- 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
+ 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.toggle!(:admin)
+ 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
- users = User.all(limit: 6)
- 50.times do
- content = Faker::Lorem.sentences(5)
- users.each { |user| user.microposts.create!(content: content) }
- 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[1..50]
+ followers = users[3..40]
+ followed_users.each { |followed| user.follow!(followed) }
+ followers.each { |follower| follower.follow!(user) }
end
View
29 spec/controllers/relationships_controller_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe RelationshipsController do
+
+ let(:user) { FactoryGirl.create(:user) }
+ let(:other_user) { FactoryGirl.create(:user) }
+
+ before { sign_in(user) }
+
+ describe "creating a relationship with Ajax" do
+ it "decrement the Relationship count" do
+ expect do
+ xhr :post, :create, relationship: { followed_id: other_user.id }
+ end.should change(Relationship, :count).by(1)
+ 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 destroy a relationship using Ajax" do
+ expect do
+ xhr :delete, :destroy, id: relationship.id
+ end.should change(Relationship, :count).by(-1)
+ end
+ end
+end
View
19 spec/models/micropost_spec.rb
@@ -28,4 +28,23 @@
before {@micropost.content = "a" *141 }
it {should_not be_valid }
end
+
+ describe "from_users_followed_by" do
+
+ let(:user) { FactoryGirl.create(:user) }
+ let(:other_user) { FactoryGirl.create(:user) }
+ let(:third_user) { FactoryGirl.create(:user) }
+
+ before { user.follow!(other_user) }
+
+ let(:own_post) { user.microposts.create!(content: "foo") }
+ let(:followed_post) { other_user.microposts.create!(content: "bar") }
+ let(:unfollowed_post) { third_user.microposts.create!(content: "baz") }
+
+ subject { Micropost.from_users_followed_by(user) }
+
+ it { should include(own_post) }
+ it { should include(followed_post) }
+ it { should_not include(unfollowed_post) }
+ end
end
View
33 spec/models/relationship_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Relationship do
+
+ let(:follower) { FactoryGirl.create(:user) }
+ let(:followed) { FactoryGirl.create(:user) }
+ let(:relationship) do
+ follower.relationships.build(followed_id: followed.id)
+ end
+
+ subject { relationship }
+
+ it { should be_valid }
+
+ describe "follower methods" do
+ before { relationship.save }
+
+ it { should respond_to(:follower) }
+ it { should respond_to(:followed) }
+ its(:follower) { should == follower }
+ its(:followed) { should == 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
View
43 spec/models/user_spec.rb
@@ -23,6 +23,12 @@
it { should respond_to(:authenticate) }
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(:following?) }
+ it { should respond_to(:follow!) }
+ it { should respond_to(:reverse_relationships) }
+ it { should respond_to(:followers) }
it { should be_valid }
it { should_not be_admin }
@@ -111,7 +117,6 @@
end
describe "micropost associations" do
-
before { @user.save }
let!(:older_micropost) do
FactoryGirl.create(:micropost, user: @user, created_at: 1.day.ago)
@@ -136,10 +141,44 @@
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
+end
View
44 spec/requests/authentication_pages_spec.rb
@@ -54,21 +54,21 @@
describe "for non-signed-in users" do
let(:user) { FactoryGirl.create(:user) }
- describe "in the Microposts controller" do
+ describe "in the Microposts controller" do
- describe "submitting to the create action" do
- before { post microposts_path }
- specify { response.should redirect_to(signin_path) }
- end
+ describe "submitting to the create action" do
+ before { post microposts_path }
+ specify { response.should redirect_to(signin_path) }
+ end
- describe "submitting to the destroy action" do
- before do
- micropost = FactoryGirl.create(:micropost)
- delete micropost_path(micropost)
- end
- specify { response.should redirect_to(signin_path) }
+ describe "submitting to the destroy action" do
+ before do
+ micropost = FactoryGirl.create(:micropost)
+ delete micropost_path(micropost)
end
+ specify { response.should redirect_to(signin_path) }
end
+ end
describe "in the Users controller" do
@@ -81,12 +81,34 @@
before { put user_path(user) }
specify { response.should redirect_to(signin_path) }
end
+
+ describe "visiting the following page" do
+ before { visit following_user_path(user) }
+ it { should have_selector('title', text: 'Sign in') }
+ end
+
+ describe "visiting the followers page" do
+ before { visit followers_user_path(user) }
+ it { should have_selector('title', text: 'Sign in') }
+ end
end
describe "visiting user index" do
before { visit users_path }
it { should have_selector('title', text: 'Sign in') }
end
+
+ describe "in the Relationships controller" do
+ describe "submitting to the create action" do
+ before { post relationships_path }
+ specify { response.should redirect_to(signin_path) }
+ end
+
+ describe "submitting to the destroy action" do
+ before { delete relationship_path(1) }
+ specify { response.should redirect_to(signin_path) }
+ end
+ end
end
describe "as wrong user" do
View
14 spec/requests/static_pages_spec.rb
@@ -13,8 +13,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
@@ -24,6 +24,16 @@
page.should have_selector("tr##{item.id}", text: item.content)
end
end
+
+ describe "follower/following counts" do
+ let(:other_user) { FactoryGirl.create(:user) }
+ before { user.follow!(other_user) }
+
+ it { should have_selector('a', href: following_user_path(user),
+ content: "0 following") }
+ it { should have_selector('a', href: followers_user_path(user),
+ content: "1 follower") }
+ end
end
end
View
78 spec/requests/user_pages_spec.rb
@@ -60,8 +60,58 @@
it { should have_content(m2.content) }
it { should have_content(user.microposts.count) }
end
- 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_selector('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_selector('input', value: 'Follow') }
+ end
+ end
+ end
+ end
+
describe "signup page" do
before { visit signup_path }
@@ -140,4 +190,30 @@
specify { user.reload.email.should == 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_selector('a', href: user_path(other_user),
+ text: other_user.name) }
+ end
+
+ describe "followers" do
+ before do
+ sign_in(other_user)
+ visit followers_user_path(other_user)
+ end
+
+ it { should have_selector('a', href: user_path(user),
+ text: user.name) }
+ end
+ end
end
Please sign in to comment.
Something went wrong with that request. Please try again.