Skip to content

Commit

Permalink
Add user following
Browse files Browse the repository at this point in the history
  • Loading branch information
mhartl committed Mar 8, 2012
1 parent bf8ec15 commit 8a0eaeb
Show file tree
Hide file tree
Showing 26 changed files with 494 additions and 29 deletions.
38 changes: 34 additions & 4 deletions app/assets/stylesheets/custom.css.scss
Original file line number Diff line number Diff line change
Expand Up @@ -196,19 +196,49 @@ input, textarea, select, .uneditable-input {
border-top: 1px solid #e8e8e8;
}
}

.content {
display: block;
}

.timestamp {
color: $grayLight;
}
.gravatar {
float: left;
margin-right: 10px;
}

aside {
textarea {
height: 100px;
margin-bottom: 5px;
}
}

/* sidebar */

.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;
}
}
21 changes: 21 additions & 0 deletions app/controllers/relationships_controller.rb
Original file line number Diff line number Diff line change
@@ -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
17 changes: 16 additions & 1 deletion app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
class UsersController < ApplicationController
before_filter :signed_in_user, only: [:index, :edit, :update]
before_filter :signed_in_user,
only: [:index, :edit, :update, :following, :followers]
before_filter :correct_user, only: [:edit, :update]
before_filter :admin_user, only: :destroy

Expand Down Expand Up @@ -46,6 +47,20 @@ def destroy
redirect_to users_path
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
Expand Down
14 changes: 14 additions & 0 deletions app/models/micropost.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,18 @@ class Micropost < ActiveRecord::Base
validates :user_id, presence: true

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
9 changes: 9 additions & 0 deletions app/models/relationship.rb
Original file line number Diff line number Diff line change
@@ -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
21 changes: 19 additions & 2 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ 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

Expand All @@ -12,9 +18,20 @@ class User < ActiveRecord::Base
validates :password, length: { minimum: 6 }
validates :password_confirmation, presence: true


def feed
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
Expand Down
2 changes: 2 additions & 0 deletions app/views/relationships/create.js.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>")
$("#followers").html('<%= @user.followers.count %>')
2 changes: 2 additions & 0 deletions app/views/relationships/destroy.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>")
$("#followers").html('<%= @user.followers.count %>')
15 changes: 15 additions & 0 deletions app/views/shared/_stats.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<% @user ||= current_user %>
<div class="stats">
<a href="<%= following_user_path(@user) %>">
<strong id="following" class="stat">
<%= @user.followed_users.count %>
</strong>
following
</a>
<a href="<%= followers_user_path(@user) %>">
<strong id="followers" class="stat">
<%= @user.followers.count %>
</strong>
followers
</a>
</div>
3 changes: 3 additions & 0 deletions app/views/static_pages/home.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
<section>
<%= render 'shared/user_info' %>
</section>
<section>
<%= render 'shared/stats' %>
</section>
<section>
<%= render 'shared/micropost_form' %>
</section>
Expand Down
5 changes: 5 additions & 0 deletions app/views/users/_follow.html.erb
Original file line number Diff line number Diff line change
@@ -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>
<%= f.submit "Follow", :class => "btn btn-large btn-primary" %>
<% end %>
9 changes: 9 additions & 0 deletions app/views/users/_follow_form.html.erb
Original file line number Diff line number Diff line change
@@ -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 %>
5 changes: 5 additions & 0 deletions app/views/users/_unfollow.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<%= form_for(current_user.relationships.find_by_followed_id(@user),
html: { method: :delete },
remote: true) do |f| %>
<%= f.submit "Unfollow", :class => "btn btn-large" %>
<% end %>
1 change: 1 addition & 0 deletions app/views/users/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
</section>
</aside>
<div class="span8">
<%= render 'follow_form' if signed_in? %>
<% if @user.microposts.any? %>
<h3>Microposts (<%= @user.microposts.count %>)</h3>
<ol class="microposts">
Expand Down
29 changes: 29 additions & 0 deletions app/views/users/show_follow.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<div class="row">
<aside class="span4">
<section>
<%= 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>
<%= render 'shared/stats' %>
<% unless @users.empty? %>
<div class="user_avatars">
<% @users.each do |user| %>
<%= link_to gravatar_for(user, :size => 30), user %>
<% end %>
</div>
<% end %>
</section>
</aside>
<div class="span8">
<h3><%= yield(:title) %></h3>
<% if @users.any? %>
<ul class="users">
<%= render @users %>
</ul>
<%= will_paginate @users %>
<% end %>
</div>
</div>
9 changes: 7 additions & 2 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
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 to: 'static_pages#home'

match '/signup', to: 'users#new'
Expand Down
14 changes: 14 additions & 0 deletions db/migrate/20120308215846_create_relationships.rb
Original file line number Diff line number Diff line change
@@ -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
13 changes: 12 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
#
# It's strongly recommended to check this file into your version control system.

ActiveRecord::Schema.define(:version => 20120308210452) do
ActiveRecord::Schema.define(:version => 20120308215846) do

create_table "microposts", :force => true do |t|
t.string "content"
Expand All @@ -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"
Expand Down
55 changes: 36 additions & 19 deletions lib/tasks/sample_data.rake
Original file line number Diff line number Diff line change
Expand Up @@ -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: "[email protected]",
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: "[email protected]",
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.sentence(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[2..50]
followers = users[3..40]
followed_users.each { |followed| user.follow!(followed) }
followers.each { |follower| follower.follow!(user) }
end
Loading

2 comments on commit 8a0eaeb

@idoan
Copy link

@idoan idoan commented on 8a0eaeb Mar 24, 2012

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Michael.

Don't you think you should add :destroy in before_filter :signed_in_user, only: [:index, :edit, :update] array, as you did in the first sample_app?
Otherwise, only :admin_user filter will be called and that will call current_user.admin? in return , which is an error of " undefined method `admin?' for nil:NilClass ". Do you think this error could be disregarded?

Thanks for your generosity.

@mhartl
Copy link
Collaborator Author

@mhartl mhartl commented on 8a0eaeb Mar 25, 2012

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error you mention should never happen, because only signed-in users can see the 'delete' links. For completeness, though, I agree that :destroy should be included in the signed_in_user before filter, and I've amended the tutorial accordingly. Thanks!

Please sign in to comment.