Merge pull request #448 from yoongkang/followerdev

Allow members to follow other members
This commit is contained in:
Taylor Griffin
2014-11-30 16:04:35 +11:00
19 changed files with 306 additions and 9 deletions

View File

@@ -0,0 +1,25 @@
class FollowsController < ApplicationController
# POST /follows
def create
@follow = current_member.follows.build(:followed_id => params[:followed_id])
if @follow.save
flash[:notice] = "Followed #{ @follow.followed.login_name }"
redirect_to :back
else
flash[:error] = "Already following or error while following."
redirect_to :back
end
end
# DELETE /follows/1
def destroy
@follow = current_member.follows.find(params[:id])
unfollowed_name = @follow.followed.login_name
@follow.destroy
flash[:notice] = "Unfollowed #{ unfollowed_name }"
redirect_to root_path
end
end

View File

@@ -23,7 +23,7 @@ class MembersController < ApplicationController
# it requires a garden to be passed in @garden.
# The new garden is not persisted unless Garden#save is called.
@garden = Garden.new
respond_to do |format|
format.html # show.html.haml
format.json { render :json => @member.to_json(:only => [:id, :login_name, :bio, :created_at, :slug, :location, :latitude, :longitude]) }
@@ -34,4 +34,14 @@ class MembersController < ApplicationController
end
end
def view_follows
@member = Member.confirmed.find(params[:login_name])
@follows = @member.followed.paginate(:page => params[:page])
end
def view_followers
@member = Member.confirmed.find(params[:login_name])
@followers = @member.followers.paginate(:page => params[:page])
end
end

View File

@@ -6,6 +6,8 @@ class Ability
# everyone can do these things, even non-logged in
can :read, :all
can :view_follows, Member
can :view_followers, Member
# except these, which don't make sense if you're not logged in
cannot :read, Notification
@@ -91,6 +93,11 @@ class Ability
cannot :update, OrderItem, :order => { :member_id => member.id, :completed_at => nil }
cannot :destroy, OrderItem, :order => { :member_id => member.id, :completed_at => nil }
# following/unfollowing permissions
can :create, Follow do |f|
!member.already_following?(f.followed) && f.followed_id != member.id
end
if member.has_role? :admin
can :read, :all

17
app/models/follow.rb Normal file
View File

@@ -0,0 +1,17 @@
class Follow < ActiveRecord::Base
attr_accessible :followed_id, :follower_id
belongs_to :follower, class_name: "Member"
belongs_to :followed, class_name: "Member"
validates :follower_id, uniqueness: { :scope => :followed_id }
after_create do
Notification.create(
:recipient_id => self.followed_id,
:sender_id => self.follower_id,
:subject => "#{self.follower.login_name} is now following you",
:body => "#{self.follower.login_name} just followed you on #{ENV["GROWSTUFF_SITE_NAME"]}. "
)
end
end

View File

@@ -27,6 +27,12 @@ class Member < ActiveRecord::Base
has_many :photos
has_many :follows, :class_name => "Follow", :foreign_key => "follower_id"
has_many :followed, :through => :follows
has_many :inverse_follows, :class_name => "Follow", :foreign_key => "followed_id"
has_many :followers, :through => :inverse_follows, :source => :follower
default_scope order("lower(login_name) asc")
scope :confirmed, where('confirmed_at IS NOT NULL')
scope :located, where("location <> '' and latitude IS NOT NULL and longitude IS NOT NULL")
@@ -255,4 +261,12 @@ class Member < ActiveRecord::Base
})
end
def already_following?(member)
self.follows.exists?(:followed_id => member.id)
end
def get_follow(member)
self.follows.where(:followed_id => member.id).first if already_following?(member)
end
end

View File

@@ -9,4 +9,3 @@
= member.account_type
account

View File

@@ -3,21 +3,34 @@
%ul
%li
- if member.plantings.count > 0
= link_to pluralize(member.plantings.count, "planting"), plantings_by_owner_path(:owner => member)
= link_to pluralize(member.plantings.size, "planting"), plantings_by_owner_path(:owner => member)
- else
0 plantings
%li
- if member.harvests.count > 0
= link_to pluralize(member.harvests.count, "harvest"), harvests_by_owner_path(:owner => member)
= link_to pluralize(member.harvests.size, "harvest"), harvests_by_owner_path(:owner => member)
- else
0 harvests
%li
- if member.seeds.count > 0
= link_to pluralize(member.seeds.count, "seeds"), seeds_by_owner_path(:owner => member)
= link_to pluralize(member.seeds.size, "seeds"), seeds_by_owner_path(:owner => member)
- else
0 seeds
%li
- if member.posts.count > 0
= link_to pluralize(member.posts.count, "post"), posts_by_author_path(:author => member)
= link_to pluralize(member.posts.size, "post"), posts_by_author_path(:author => member)
- else
0 posts
%li
- if member.followed.count > 0
= link_to pluralize(member.followed.size, "follow"), member_follows_path(member)
- else
0 following
%li
- if member.followers.count > 0
= link_to pluralize(member.followers.size, "follower"), member_followers_path(member)
- else
0 followers

View File

@@ -7,7 +7,12 @@
= link_to "Upgrade account", shop_path, :class => 'btn btn-default'
-if can? :create, Notification and current_member != @member
=link_to 'Send message', new_notification_path(:recipient_id => @member.id), :class => 'btn btn-default'
- if can? :create, Follow and current_member != @member
- if !current_member.already_following?(@member)
= link_to 'Follow', follows_path(:followed_id => @member.id), :method => :post, :class => 'btn btn-default'
- else
- follow = current_member.get_follow(@member)
= link_to 'Unfollow', follow_path(follow), :method => :delete, :class => 'btn btn-default'
- content_for :member_rss_login_name, @member.login_name
- content_for :member_rss_slug, @member.slug

View File

@@ -0,0 +1,16 @@
- content_for :title, "#{@member.login_name}'s followers"
%div.pagination
= page_entries_info @followers, :model => "members"
= will_paginate @followers
.row
.col-md-12
- @followers.each do |f|
.col-md-4.three-across
.thumbnail
= render :partial => "members/thumbnail", :locals => { :member => f }
%div.pagination
= page_entries_info @followers, :model => "members"
= will_paginate @followers

View File

@@ -0,0 +1,16 @@
- content_for :title, "#{@member.login_name}'s follows"
%div.pagination
= page_entries_info @follows, :model => "members"
= will_paginate @follows
.row
.col-md-12
- @follows.each do |f|
.col-md-4.three-across
.thumbnail
= render :partial => "members/thumbnail", :locals => { :member => f }
%div.pagination
= page_entries_info @follows, :model => "members"
= will_paginate @follows

View File

@@ -4,7 +4,7 @@ Growstuff::Application.routes.draw do
devise_for :members, :controllers => { :registrations => "registrations", :passwords => "passwords" }
resources :members
resources :members
resources :photos
@@ -41,6 +41,11 @@ Growstuff::Application.routes.draw do
resources :forums
resources :notifications
resources :follows, :only => [:create, :destroy]
get '/members/:login_name/follows' => 'members#view_follows', :as => 'member_follows'
get '/members/:login_name/followers' => 'members#view_followers', :as => 'member_followers'
get '/places' => 'places#index'
get '/places/search' => 'places#search', :as => 'search_places'
get '/places/:place' => 'places#show', :as => 'place'
@@ -79,4 +84,6 @@ Growstuff::Application.routes.draw do
match '/admin/newsletter' => 'admin#newsletter', :as => :admin_newsletter
match '/admin/:action' => 'admin#:action'
end

View File

@@ -0,0 +1,10 @@
class CreateFollows < ActiveRecord::Migration
def change
create_table :follows do |t|
t.integer :member_id
t.integer :followed_id
t.timestamps
end
end
end

View File

@@ -0,0 +1,5 @@
class ChangeFollowsMemberIdToFollowerId < ActiveRecord::Migration
def change
rename_column :follows, :member_id, :follower_id
end
end

View File

@@ -11,7 +11,7 @@
#
# It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(:version => 20141018111015) do
ActiveRecord::Schema.define(:version => 20141119130555) do
create_table "account_types", :force => true do |t|
t.string "name", :null => false
@@ -80,6 +80,13 @@ ActiveRecord::Schema.define(:version => 20141018111015) do
add_index "crops_posts", ["crop_id", "post_id"], :name => "index_crops_posts_on_crop_id_and_post_id"
add_index "crops_posts", ["crop_id"], :name => "index_crops_posts_on_crop_id"
create_table "follows", :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
create_table "forums", :force => true do |t|
t.string "name", :null => false
t.text "description", :null => false

View File

@@ -0,0 +1,7 @@
FactoryGirl.define do
factory :follow do
follower
followed
end
end

View File

@@ -13,6 +13,8 @@ feature "member profile" do
expect(page).to have_content "Account type: Free account"
expect(page).to have_content "#{member.login_name}'s gardens"
expect(page).to have_link "More about this garden...", :href => garden_path(member.gardens.first)
expect(page).to_not have_link "Follow"
expect(page).to_not have_link "Unfollow"
end
scenario "no bio" do
@@ -143,6 +145,53 @@ feature "member profile" do
scenario "has a private message button" do
expect(page).to have_link "Send message", :href => new_notification_path(:recipient_id => other_member.id)
end
scenario "has a follow button" do
expect(page).to have_link "Follow", :href => follows_path(:followed_id => other_member.id)
end
end
context "following another member" do
background do
visit member_path(other_member)
click_link 'Follow'
end
scenario "has correct message and unfollow button" do
expect(page).to have_content "Followed #{other_member.login_name}"
expect(page).to have_link "Unfollow", :href => follow_path(member.get_follow(other_member))
end
scenario "has a followed member listed in the following page" do
visit member_follows_path(member)
expect(page).to have_content "#{other_member.login_name}"
end
scenario "has correct message and follow button after unfollow" do
click_link 'Unfollow'
expect(page).to have_content "Unfollowed #{other_member.login_name}"
visit member_path(other_member) # unfollowing redirects to root
expect(page).to have_link "Follow", :href => follows_path(:followed_id => other_member.id)
end
scenario "has member in following list" do
visit member_follows_path(member)
expect(page).to have_content "#{other_member.login_name}"
end
scenario "appears in in followed member's followers list" do
visit member_followers_path(other_member)
expect(page).to have_content "#{member.login_name}"
end
scenario "removes members from following and followers lists after unfollow" do
click_link 'Unfollow'
visit member_follows_path(member)
expect(page).not_to have_content "#{other_member.login_name}"
visit member_followers_path(other_member)
expect(page).not_to have_content "#{member.login_name}"
end
end
context "home page" do

View File

@@ -0,0 +1,45 @@
require 'spec_helper'
describe Follow do
before(:each) do
@member1 = FactoryGirl.create(:member)
@member2 = FactoryGirl.create(:member)
end
it "sends a notification when a follow is created" do
expect {
Follow.create(:follower_id => @member1.id, :followed_id => @member2.id)
}.to change(Notification, :count).by(1)
end
it "does not delete any members when follow is deleted" do
expect {
follow = Follow.create(:follower_id => @member1.id, :followed_id => @member2.id)
follow.destroy
}.not_to change(Member, :count)
end
context "when follow is created" do
before (:each) do
@follow = Follow.create(:follower_id => @member1.id, :followed_id => @member2.id)
end
it "should not duplicate follows" do
expect(Follow.create(:follower_id => @member1.id, :followed_id => @member2.id)).not_to be_valid
end
it "should list users in following/follower collections when follow is created" do
expect(@member1.followed).to include(@member2)
expect(@member2.followers).to include(@member1)
end
it "should no longer list users in following/follower collections when follow is deleted" do
@follow.destroy
expect(@member1.followed).not_to include(@member2)
expect(@member2.followers).not_to include(@member1)
end
end
end

View File

@@ -381,4 +381,34 @@ describe 'member' do
end
end
context 'member who followed another member' do
before(:each) do
@member1 = FactoryGirl.create(:member)
@member2 = FactoryGirl.create(:member)
@member3 = FactoryGirl.create(:member)
@follow = @member1.follows.create(:follower_id => @member1.id, :followed_id => @member2.id)
end
context 'already_following' do
it 'detects that member is already following a member' do
expect(@member1.already_following?(@member2)).to eq true
end
it 'detects that member is not already following a member' do
expect(@member1.already_following?(@member3)).to eq false
end
end
context 'get_follow' do
it 'gets the correct follow for a followed member' do
expect(@member1.get_follow(@member2).id).to eq @follow.id
end
it 'returns nil for a member that is not followed' do
expect(@member1.get_follow(@member3)).to be_nil
end
end
end
end

View File

@@ -0,0 +1,15 @@
require "spec_helper"
describe FollowsController do
describe "routing" do
it "routes to #create" do
post("/follows").should route_to("follows#create")
end
it "routes to #destroy" do
delete("/follows/1").should route_to("follows#destroy", :id => "1")
end
end
end