Commit 8b945809d1d522c39798b10d96ecca9eccd13328

Authored by Marcos Pereira
Committed by Larissa Reis
1 parent fceeac68

Profile followers feature

Signed-off-by: Alessandro Caetano <alessandro.caetanob@gmail.com>
Signed-off-by: Artur Bersan de Faria <arturbersan@gmail.com>
Signed-off-by: Gabriel Silva <gabriel93.silva@gmail.com>
Signed-off-by: Marcos Ronaldo <marcos.rpj2@gmail.com>
Signed-off-by: Matheus Miranda <matheusmirandalacerda@gmail.com>
Signed-off-by: Sabryna Sousa <sabryna.sousa1323@gmail.com>
Signed-off-by: Victor Matias Navarro <victor.matias.navarro@gmail.com>
Signed-off-by: Vitor Barbosa <vitormga15@gmail.com>
Showing 58 changed files with 1448 additions and 92 deletions   Show diff stats
app/controllers/application_controller.rb
@@ -115,6 +115,10 @@ class ApplicationController &lt; ActionController::Base @@ -115,6 +115,10 @@ class ApplicationController &lt; ActionController::Base
115 115
116 protected 116 protected
117 117
  118 + def accept_only_post
  119 + return render_not_found if !request.post?
  120 + end
  121 +
118 def verified_request? 122 def verified_request?
119 super || form_authenticity_token == request.headers['X-XSRF-TOKEN'] 123 super || form_authenticity_token == request.headers['X-XSRF-TOKEN']
120 end 124 end
app/controllers/my_profile/circles_controller.rb 0 → 100644
@@ -0,0 +1,58 @@ @@ -0,0 +1,58 @@
  1 +class CirclesController < MyProfileController
  2 +
  3 + before_action :accept_only_post, :only => [:create, :update, :destroy]
  4 +
  5 + def index
  6 + @circles = current_person.circles
  7 + end
  8 +
  9 + def new
  10 + @circle = Circle.new
  11 + end
  12 +
  13 + def create
  14 + @circle = Circle.new(params[:circle].merge({ :person => current_person }))
  15 + if @circle.save
  16 + redirect_to :action => 'index'
  17 + else
  18 + render :action => 'new'
  19 + end
  20 + end
  21 +
  22 + def xhr_create
  23 + if request.xhr?
  24 + circle = Circle.new(params[:circle].merge({:person => current_person }))
  25 + if circle.save
  26 + render :partial => "circle_checkbox", :locals => { :circle => circle },
  27 + :status => 201
  28 + else
  29 + render :text => _('The circle could not be saved'), :status => 400
  30 + end
  31 + else
  32 + render_not_found
  33 + end
  34 + end
  35 +
  36 + def edit
  37 + @circle = Circle.find_by_id(params[:id])
  38 + render_not_found if @circle.nil?
  39 + end
  40 +
  41 + def update
  42 + @circle = Circle.find_by_id(params[:id])
  43 + return render_not_found if @circle.nil?
  44 +
  45 + if @circle.update(params[:circle])
  46 + redirect_to :action => 'index'
  47 + else
  48 + render :action => 'edit'
  49 + end
  50 + end
  51 +
  52 + def destroy
  53 + @circle = Circle.find_by_id(params[:id])
  54 + return render_not_found if @circle.nil?
  55 + @circle.destroy
  56 + redirect_to :action => 'index'
  57 + end
  58 +end
app/controllers/my_profile/followers_controller.rb 0 → 100644
@@ -0,0 +1,43 @@ @@ -0,0 +1,43 @@
  1 +class FollowersController < MyProfileController
  2 +
  3 + before_action :only_for_person, :only => :index
  4 + before_action :accept_only_post, :only => [:update_category]
  5 +
  6 + def index
  7 + @followed_people = current_person.followed_profiles.order(:type)
  8 + @profile_types = {_('All profiles') => nil}.merge(Circle.profile_types).to_a
  9 +
  10 + if params['filter'].present?
  11 + @followed_people = @followed_people.where(:type => params['filter'])
  12 + @active_filter = params['filter']
  13 + end
  14 +
  15 + @followed_people = @followed_people.paginate(:per_page => 15, :page => params[:npage])
  16 + end
  17 +
  18 + def set_category_modal
  19 + profile = Profile.find(params[:followed_profile_id])
  20 + circles = Circle.where(:person => current_person, :profile_type => profile.class.name)
  21 + render :partial => 'followers/edit_circles_modal', :locals => { :circles => circles, :profile => profile }
  22 + end
  23 +
  24 + def update_category
  25 + followed_profile = Profile.find_by(:id => params["followed_profile_id"])
  26 +
  27 + selected_circles = params[:circles].map{|circle_name, circle_id| Circle.find_by(:id => circle_id)}.select{|c|not c.nil?}
  28 +
  29 + if followed_profile
  30 + current_person.update_profile_circles(followed_profile, selected_circles)
  31 + render :text => _("Circles of %s updated successfully") % followed_profile.name, :status => 200
  32 + else
  33 + render :text => _("Error: No profile to follow."), :status => 400
  34 + end
  35 + end
  36 +
  37 + protected
  38 +
  39 + def only_for_person
  40 + render_not_found unless profile.person?
  41 + end
  42 +
  43 +end
app/controllers/public/content_viewer_controller.rb
@@ -128,9 +128,9 @@ class ContentViewerController &lt; ApplicationController @@ -128,9 +128,9 @@ class ContentViewerController &lt; ApplicationController
128 end 128 end
129 129
130 unless @page.display_to?(user) 130 unless @page.display_to?(user)
131 - if !profile.visible? || profile.secret? || (user && user.follows?(profile)) || user.blank? 131 + if !profile.visible? || profile.secret? || (user && profile.in_social_circle?(user)) || user.blank?
132 render_access_denied 132 render_access_denied
133 - else #!profile.public? 133 + else
134 private_profile_partial_parameters 134 private_profile_partial_parameters
135 render :template => 'profile/_private_profile', :status => 403, :formats => [:html] 135 render :template => 'profile/_private_profile', :status => 403, :formats => [:html]
136 end 136 end
app/controllers/public/profile_controller.rb
@@ -3,7 +3,8 @@ class ProfileController &lt; PublicController @@ -3,7 +3,8 @@ class ProfileController &lt; PublicController
3 needs_profile 3 needs_profile
4 before_filter :check_access_to_profile, :except => [:join, :join_not_logged, :index, :add] 4 before_filter :check_access_to_profile, :except => [:join, :join_not_logged, :index, :add]
5 before_filter :store_location, :only => [:join, :join_not_logged, :report_abuse, :send_mail] 5 before_filter :store_location, :only => [:join, :join_not_logged, :report_abuse, :send_mail]
6 - before_filter :login_required, :only => [:add, :join, :leave, :unblock, :leave_scrap, :remove_scrap, :remove_activity, :view_more_activities, :view_more_network_activities, :report_abuse, :register_report, :leave_comment_on_activity, :send_mail] 6 + before_filter :login_required, :only => [:add, :join, :leave, :unblock, :leave_scrap, :remove_scrap, :remove_activity, :view_more_activities, :view_more_network_activities, :report_abuse, :register_report, :leave_comment_on_activity, :send_mail, :follow, :unfollow]
  7 + before_filter :allow_followers?, :only => [:follow, :unfollow]
7 8
8 helper TagsHelper 9 helper TagsHelper
9 helper ActionTrackerHelper 10 helper ActionTrackerHelper
@@ -65,6 +66,14 @@ class ProfileController &lt; PublicController @@ -65,6 +66,14 @@ class ProfileController &lt; PublicController
65 end 66 end
66 end 67 end
67 68
  69 + def following
  70 + @followed_people = profile.followed_profiles.paginate(:per_page => per_page, :page => params[:npage], :total_entries => profile.followed_profiles.count)
  71 + end
  72 +
  73 + def followed
  74 + @followed_by = profile.followers.paginate(:per_page => per_page, :page => params[:npage], :total_entries => profile.followers.count)
  75 + end
  76 +
68 def members 77 def members
69 if is_cache_expired?(profile.members_cache_key(params)) 78 if is_cache_expired?(profile.members_cache_key(params))
70 sort = (params[:sort] == 'desc') ? params[:sort] : 'asc' 79 sort = (params[:sort] == 'desc') ? params[:sort] : 'asc'
@@ -151,6 +160,37 @@ class ProfileController &lt; PublicController @@ -151,6 +160,37 @@ class ProfileController &lt; PublicController
151 end 160 end
152 end 161 end
153 162
  163 + def follow
  164 + if request.post?
  165 + if profile.followed_by?(current_person)
  166 + render :text => _("You are already following %s.") % profile.name, :status => 400
  167 + else
  168 + selected_circles = params[:circles].map{|circle_name, circle_id| Circle.find_by(:id => circle_id)}.select{|c|not c.nil?}
  169 + if selected_circles.present?
  170 + current_person.follow(profile, selected_circles)
  171 + render :text => _("You are now following %s") % profile.name, :status => 200
  172 + else
  173 + render :text => _("Select at least one circle to follow %s.") % profile.name, :status => 400
  174 + end
  175 + end
  176 + else
  177 + render_not_found
  178 + end
  179 + end
  180 +
  181 + def find_profile_circles
  182 + circles = Circle.where(:person => current_person, :profile_type => profile.class.name)
  183 + render :partial => 'blocks/profile_info_actions/circles', :locals => { :circles => circles, :profile_types => Circle.profile_types.to_a }
  184 + end
  185 +
  186 + def unfollow
  187 + if current_person.follows?(profile)
  188 + current_person.unfollow(profile)
  189 + end
  190 + redirect_url = params["redirect_to"] ? params["redirect_to"] : profile.url
  191 + redirect_to redirect_url
  192 + end
  193 +
154 def check_friendship 194 def check_friendship
155 unless logged_in? 195 unless logged_in?
156 render :text => '' 196 render :text => ''
@@ -437,4 +477,8 @@ class ProfileController &lt; PublicController @@ -437,4 +477,8 @@ class ProfileController &lt; PublicController
437 [:image, :domains, :preferred_domain, :environment] 477 [:image, :domains, :preferred_domain, :environment]
438 end 478 end
439 479
  480 + def allow_followers?
  481 + render_not_found unless profile.allow_followers?
  482 + end
  483 +
440 end 484 end
app/helpers/action_tracker_helper.rb
@@ -14,13 +14,23 @@ module ActionTrackerHelper @@ -14,13 +14,23 @@ module ActionTrackerHelper
14 } 14 }
15 end 15 end
16 16
  17 + def new_follower_description ta
  18 + n_('has 1 new follower:<br />%{name}', 'has %{num} new followers:<br />%{name}', ta.get_follower_name.size).html_safe % {
  19 + num: ta.get_follower_name.size,
  20 + name: safe_join(ta.collect_group_with_index(:follower_name) do |n,i|
  21 + link_to image_tag(ta.get_follower_profile_custom_icon[i] || default_or_themed_icon("/images/icons-app/person-icon.png")),
  22 + ta.get_follower_url[i], title: n
  23 + end)
  24 + }
  25 + end
  26 +
17 def join_community_description ta 27 def join_community_description ta
18 n_('has joined 1 community:<br />%{name}', 'has joined %{num} communities:<br />%{name}', ta.get_resource_name.size).html_safe % { 28 n_('has joined 1 community:<br />%{name}', 'has joined %{num} communities:<br />%{name}', ta.get_resource_name.size).html_safe % {
19 num: ta.get_resource_name.size, 29 num: ta.get_resource_name.size,
20 - name: ta.collect_group_with_index(:resource_name) do |n,i| 30 + name: safe_join(ta.collect_group_with_index(:resource_name) do |n,i|
21 link = link_to image_tag(ta.get_resource_profile_custom_icon[i] || default_or_themed_icon("/images/icons-app/community-icon.png")), 31 link = link_to image_tag(ta.get_resource_profile_custom_icon[i] || default_or_themed_icon("/images/icons-app/community-icon.png")),
22 ta.get_resource_url[i], title: n 32 ta.get_resource_url[i], title: n
23 - end.join.html_safe 33 + end)
24 } 34 }
25 end 35 end
26 36
@@ -68,9 +78,9 @@ module ActionTrackerHelper @@ -68,9 +78,9 @@ module ActionTrackerHelper
68 end 78 end
69 79
70 def favorite_enterprise_description ta 80 def favorite_enterprise_description ta
71 - _('favorited enterprise %{title}') % { 81 + (_('favorited enterprise %{title}') % {
72 title: link_to(truncate(ta.get_enterprise_name), ta.get_enterprise_url), 82 title: link_to(truncate(ta.get_enterprise_name), ta.get_enterprise_url),
73 - } 83 + }).html_safe
74 end 84 end
75 85
76 end 86 end
app/helpers/profile_helper.rb
@@ -11,7 +11,7 @@ module ProfileHelper @@ -11,7 +11,7 @@ module ProfileHelper
11 PERSON_CATEGORIES[:location] = [:address, :address_reference, :zip_code, :city, :state, :district, :country, :nationality] 11 PERSON_CATEGORIES[:location] = [:address, :address_reference, :zip_code, :city, :state, :district, :country, :nationality]
12 PERSON_CATEGORIES[:work] = [:organization, :organization_website, :professional_activity] 12 PERSON_CATEGORIES[:work] = [:organization, :organization_website, :professional_activity]
13 PERSON_CATEGORIES[:study] = [:schooling, :formation, :area_of_study] 13 PERSON_CATEGORIES[:study] = [:schooling, :formation, :area_of_study]
14 - PERSON_CATEGORIES[:network] = [:friends, :communities, :enterprises] 14 + PERSON_CATEGORIES[:network] = [:friends, :followers, :followed_profiles, :communities, :enterprises]
15 PERSON_CATEGORIES.merge!(COMMON_CATEGORIES) 15 PERSON_CATEGORIES.merge!(COMMON_CATEGORIES)
16 16
17 ORGANIZATION_CATEGORIES = {} 17 ORGANIZATION_CATEGORIES = {}
@@ -42,7 +42,8 @@ module ProfileHelper @@ -42,7 +42,8 @@ module ProfileHelper
42 :created_at => _('Profile created at'), 42 :created_at => _('Profile created at'),
43 :members_count => _('Members'), 43 :members_count => _('Members'),
44 :privacy_setting => _('Privacy setting'), 44 :privacy_setting => _('Privacy setting'),
45 - :article_tags => _('Tags') 45 + :article_tags => _('Tags'),
  46 + :followed_profiles => _('Following')
46 } 47 }
47 48
48 EXCEPTION = { 49 EXCEPTION = {
@@ -144,6 +145,14 @@ module ProfileHelper @@ -144,6 +145,14 @@ module ProfileHelper
144 link_to(n_('One picture', '%{num} pictures', gallery.images.published.count) % { :num => gallery.images.published.count }, gallery.url) 145 link_to(n_('One picture', '%{num} pictures', gallery.images.published.count) % { :num => gallery.images.published.count }, gallery.url)
145 end 146 end
146 147
  148 + def treat_followers(followers)
  149 + link_to(profile.followers.count, {:action=>"followed", :controller=>"profile", :profile=>"#{profile.identifier}"})
  150 + end
  151 +
  152 + def treat_followed_profiles(followed_profiles)
  153 + link_to(profile.followed_profiles.count, {:action=>"following", :controller=>"profile", :profile=>"#{profile.identifier}"})
  154 + end
  155 +
147 def treat_events(events) 156 def treat_events(events)
148 link_to events.published.count, :controller => 'events', :action => 'events' 157 link_to events.published.count, :controller => 'events', :action => 'events'
149 end 158 end
app/jobs/notify_activity_to_profiles_job.rb
@@ -19,8 +19,8 @@ class NotifyActivityToProfilesJob &lt; Struct.new(:tracked_action_id) @@ -19,8 +19,8 @@ class NotifyActivityToProfilesJob &lt; Struct.new(:tracked_action_id)
19 # Notify the user 19 # Notify the user
20 ActionTrackerNotification.create(:profile_id => tracked_action.user.id, :action_tracker_id => tracked_action.id) 20 ActionTrackerNotification.create(:profile_id => tracked_action.user.id, :action_tracker_id => tracked_action.id)
21 21
22 - # Notify all friends  
23 - ActionTrackerNotification.connection.execute("insert into action_tracker_notifications(profile_id, action_tracker_id) select f.friend_id, #{tracked_action.id} from friendships as f where person_id=#{tracked_action.user.id} and f.friend_id not in (select atn.profile_id from action_tracker_notifications as atn where atn.action_tracker_id = #{tracked_action.id})") 22 + # Notify all followers
  23 + ActionTrackerNotification.connection.execute("INSERT INTO action_tracker_notifications(profile_id, action_tracker_id) SELECT DISTINCT c.person_id, #{tracked_action.id} FROM profiles_circles AS p JOIN circles as c ON c.id = p.circle_id WHERE p.profile_id = #{tracked_action.user.id} AND (c.person_id NOT IN (SELECT atn.profile_id FROM action_tracker_notifications AS atn WHERE atn.action_tracker_id = #{tracked_action.id}))")
24 24
25 if tracked_action.user.is_a? Organization 25 if tracked_action.user.is_a? Organization
26 ActionTrackerNotification.connection.execute "insert into action_tracker_notifications(profile_id, action_tracker_id) " + 26 ActionTrackerNotification.connection.execute "insert into action_tracker_notifications(profile_id, action_tracker_id) " +
app/models/add_member.rb
@@ -22,6 +22,7 @@ class AddMember &lt; Task @@ -22,6 +22,7 @@ class AddMember &lt; Task
22 self.roles = [Profile::Roles.member(organization.environment.id).id] 22 self.roles = [Profile::Roles.member(organization.environment.id).id]
23 end 23 end
24 target.affiliate(requestor, self.roles.select{|r| !r.to_i.zero? }.map{|i| Role.find(i)}) 24 target.affiliate(requestor, self.roles.select{|r| !r.to_i.zero? }.map{|i| Role.find(i)})
  25 + person.follow(organization, Circle.find_or_create_by(:person => person, :name =>_('memberships'), :profile_type => 'Community'))
25 end 26 end
26 27
27 def title 28 def title
app/models/article.rb
@@ -538,13 +538,13 @@ class Article &lt; ApplicationRecord @@ -538,13 +538,13 @@ class Article &lt; ApplicationRecord
538 538
539 scope :display_filter, lambda {|user, profile| 539 scope :display_filter, lambda {|user, profile|
540 return published if (user.nil? && profile && profile.public?) 540 return published if (user.nil? && profile && profile.public?)
541 - return [] if user.nil? || (profile && !profile.public? && !user.follows?(profile)) 541 + return [] if user.nil? || (profile && !profile.public? && !profile.in_social_circle?(user))
542 where( 542 where(
543 [ 543 [
544 "published = ? OR last_changed_by_id = ? OR profile_id = ? OR ? 544 "published = ? OR last_changed_by_id = ? OR profile_id = ? OR ?
545 OR (show_to_followers = ? AND ? AND profile_id IN (?))", true, user.id, user.id, 545 OR (show_to_followers = ? AND ? AND profile_id IN (?))", true, user.id, user.id,
546 profile.nil? ? false : user.has_permission?(:view_private_content, profile), 546 profile.nil? ? false : user.has_permission?(:view_private_content, profile),
547 - true, (profile.nil? ? true : user.follows?(profile)), ( profile.nil? ? (user.friends.select('profiles.id')) : [profile.id]) 547 + true, (profile.nil? ? true : profile.in_social_circle?(user)), ( profile.nil? ? (user.friends.select('profiles.id')) : [profile.id])
548 ] 548 ]
549 ) 549 )
550 } 550 }
app/models/block.rb
@@ -89,7 +89,7 @@ class Block &lt; ApplicationRecord @@ -89,7 +89,7 @@ class Block &lt; ApplicationRecord
89 end 89 end
90 90
91 def display_to_user?(user) 91 def display_to_user?(user)
92 - display_user == 'all' || (user.nil? && display_user == 'not_logged') || (user && display_user == 'logged') || (user && display_user == 'followers' && user.follows?(owner)) 92 + display_user == 'all' || (user.nil? && display_user == 'not_logged') || (user && display_user == 'logged') || (user && display_user == 'followers' && owner.in_social_circle?(user))
93 end 93 end
94 94
95 def display_always(context) 95 def display_always(context)
app/models/circle.rb 0 → 100644
@@ -0,0 +1,37 @@ @@ -0,0 +1,37 @@
  1 +class Circle < ApplicationRecord
  2 + has_many :profile_followers
  3 + belongs_to :person
  4 +
  5 + attr_accessible :name, :person, :profile_type
  6 +
  7 + validates :name, presence: true
  8 + validates :person_id, presence: true
  9 + validates :profile_type, presence: true
  10 + validates :person_id, :uniqueness => {:scope => :name, :message => "can't add two circles with the same name"}
  11 +
  12 + validate :profile_type_must_be_in_list
  13 +
  14 + scope :by_owner, -> person{
  15 + where(:person => person)
  16 + }
  17 +
  18 + scope :with_name, -> name{
  19 + where(:name => name)
  20 + }
  21 +
  22 + def self.profile_types
  23 + {
  24 + _("Person") => Person.name,
  25 + _("Community") => Community.name,
  26 + _("Enterprise") => Enterprise.name
  27 + }
  28 + end
  29 +
  30 + def profile_type_must_be_in_list
  31 + valid_profile_types = Circle.profile_types.values
  32 + unless self.profile_type.in? valid_profile_types
  33 + self.errors.add(:profile_type, "invalid profile type")
  34 + end
  35 + end
  36 +
  37 +end
app/models/enterprise.rb
@@ -174,8 +174,4 @@ class Enterprise &lt; Organization @@ -174,8 +174,4 @@ class Enterprise &lt; Organization
174 '' 174 ''
175 end 175 end
176 176
177 - def followed_by? person  
178 - super or self.fans.where(id: person.id).count > 0  
179 - end  
180 -  
181 end 177 end
app/models/favorite_enterprise_person.rb
@@ -7,6 +7,10 @@ class FavoriteEnterprisePerson &lt; ApplicationRecord @@ -7,6 +7,10 @@ class FavoriteEnterprisePerson &lt; ApplicationRecord
7 belongs_to :enterprise 7 belongs_to :enterprise
8 belongs_to :person 8 belongs_to :person
9 9
  10 + after_create do |favorite|
  11 + favorite.person.follow(favorite.enterprise, Circle.find_or_create_by(:person => favorite.person, :name =>_('favorites'), :profile_type => 'Enterprise'))
  12 + end
  13 +
10 protected 14 protected
11 15
12 def is_trackable? 16 def is_trackable?
app/models/friendship.rb
@@ -9,11 +9,15 @@ class Friendship &lt; ApplicationRecord @@ -9,11 +9,15 @@ class Friendship &lt; ApplicationRecord
9 after_create do |friendship| 9 after_create do |friendship|
10 Friendship.update_cache_counter(:friends_count, friendship.person, 1) 10 Friendship.update_cache_counter(:friends_count, friendship.person, 1)
11 Friendship.update_cache_counter(:friends_count, friendship.friend, 1) 11 Friendship.update_cache_counter(:friends_count, friendship.friend, 1)
  12 + friendship.person.follow(friendship.friend, Circle.find_or_create_by(:person => friendship.person, :name => (friendship.group.blank? ? 'friendships': friendship.group), :profile_type => 'Person'))
12 end 13 end
13 14
14 after_destroy do |friendship| 15 after_destroy do |friendship|
15 Friendship.update_cache_counter(:friends_count, friendship.person, -1) 16 Friendship.update_cache_counter(:friends_count, friendship.person, -1)
16 Friendship.update_cache_counter(:friends_count, friendship.friend, -1) 17 Friendship.update_cache_counter(:friends_count, friendship.friend, -1)
  18 +
  19 + circle = Circle.find_by(:person => friendship.person, :name => (friendship.group.blank? ? 'friendships': friendship.group) )
  20 + friendship.person.remove_profile_from_circle(friendship.friend, circle) if circle
17 end 21 end
18 22
19 def self.remove_friendship(person1, person2) 23 def self.remove_friendship(person1, person2)
app/models/person.rb
@@ -8,7 +8,6 @@ class Person &lt; Profile @@ -8,7 +8,6 @@ class Person &lt; Profile
8 :display => %w[compact] 8 :display => %w[compact]
9 } 9 }
10 10
11 -  
12 def self.type_name 11 def self.type_name
13 _('Person') 12 _('Person')
14 end 13 end
@@ -93,6 +92,7 @@ class Person &lt; Profile @@ -93,6 +92,7 @@ class Person &lt; Profile
93 has_many :following_articles, :class_name => 'Article', :through => :article_followers, :source => :article 92 has_many :following_articles, :class_name => 'Article', :through => :article_followers, :source => :article
94 has_many :friendships, :dependent => :destroy 93 has_many :friendships, :dependent => :destroy
95 has_many :friends, :class_name => 'Person', :through => :friendships 94 has_many :friends, :class_name => 'Person', :through => :friendships
  95 + has_many :circles
96 96
97 scope :online, -> { 97 scope :online, -> {
98 joins(:user).where("users.chat_status != '' AND users.chat_status_at >= ?", DateTime.now - User.expires_chat_status_every.minutes) 98 joins(:user).where("users.chat_status != '' AND users.chat_status_at >= ?", DateTime.now - User.expires_chat_status_every.minutes)
@@ -200,6 +200,33 @@ class Person &lt; Profile @@ -200,6 +200,33 @@ class Person &lt; Profile
200 end 200 end
201 end 201 end
202 202
  203 + def follow (profile, circles)
  204 + circles = [circles] unless circles.is_a?(Array)
  205 + circles.each do |new_circle|
  206 + ProfileFollower.create(profile: profile, circle: new_circle)
  207 + end
  208 + end
  209 +
  210 + def update_profile_circles (profile, new_circles)
  211 + profile_circles = ProfileFollower.with_profile(profile).with_follower(self).map(&:circle)
  212 + circles_to_add = new_circles - profile_circles
  213 + circles_to_remove = profile_circles - new_circles
  214 + circles_to_add.each do |new_circle|
  215 + ProfileFollower.create(profile: profile, circle: new_circle)
  216 + end
  217 +
  218 + ProfileFollower.where('circle_id IN (?) AND profile_id = ?',
  219 + circles_to_remove.map(&:id), profile.id).destroy_all
  220 + end
  221 +
  222 + def unfollow(profile)
  223 + ProfileFollower.with_follower(self).with_profile(profile).destroy_all
  224 + end
  225 +
  226 + def remove_profile_from_circle(profile, circle)
  227 + ProfileFollower.with_profile(profile).with_circle(circle).destroy_all
  228 + end
  229 +
203 def already_request_friendship?(person) 230 def already_request_friendship?(person)
204 person.tasks.where(requestor_id: self.id, type: 'AddFriend', status: Task::Status::ACTIVE).first 231 person.tasks.where(requestor_id: self.id, type: 'AddFriend', status: Task::Status::ACTIVE).first
205 end 232 end
@@ -580,9 +607,12 @@ class Person &lt; Profile @@ -580,9 +607,12 @@ class Person &lt; Profile
580 person.has_permission?(:manage_friends, self) 607 person.has_permission?(:manage_friends, self)
581 end 608 end
582 609
583 - protected 610 + def followed_profiles
  611 + Profile.followed_by self
  612 + end
584 613
585 - def followed_by?(profile)  
586 - self == profile || self.is_a_friend?(profile) 614 + def in_social_circle?(person)
  615 + self.is_a_friend?(person) || super
587 end 616 end
  617 +
588 end 618 end
app/models/profile.rb
@@ -5,7 +5,7 @@ class Profile &lt; ApplicationRecord @@ -5,7 +5,7 @@ class Profile &lt; ApplicationRecord
5 5
6 attr_accessible :name, :identifier, :public_profile, :nickname, :custom_footer, :custom_header, :address, :zip_code, :contact_phone, :image_builder, :description, :closed, :template_id, :environment, :lat, :lng, :is_template, :fields_privacy, :preferred_domain_id, :category_ids, :country, :city, :state, :national_region_code, :email, :contact_email, :redirect_l10n, :notification_time, 6 attr_accessible :name, :identifier, :public_profile, :nickname, :custom_footer, :custom_header, :address, :zip_code, :contact_phone, :image_builder, :description, :closed, :template_id, :environment, :lat, :lng, :is_template, :fields_privacy, :preferred_domain_id, :category_ids, :country, :city, :state, :national_region_code, :email, :contact_email, :redirect_l10n, :notification_time,
7 :redirection_after_login, :custom_url_redirection, 7 :redirection_after_login, :custom_url_redirection,
8 - :email_suggestions, :allow_members_to_invite, :invite_friends_only, :secret, :profile_admin_mail_notification 8 + :email_suggestions, :allow_members_to_invite, :invite_friends_only, :secret, :profile_admin_mail_notification, :allow_followers
9 9
10 # use for internationalizable human type names in search facets 10 # use for internationalizable human type names in search facets
11 # reimplement on subclasses 11 # reimplement on subclasses
@@ -206,6 +206,23 @@ class Profile &lt; ApplicationRecord @@ -206,6 +206,23 @@ class Profile &lt; ApplicationRecord
206 scope :more_active, -> { order 'activities_count DESC' } 206 scope :more_active, -> { order 'activities_count DESC' }
207 scope :more_recent, -> { order "created_at DESC" } 207 scope :more_recent, -> { order "created_at DESC" }
208 208
  209 + scope :followed_by, -> person{
  210 + distinct.select('profiles.*').
  211 + joins('left join profiles_circles ON profiles_circles.profile_id = profiles.id').
  212 + joins('left join circles ON circles.id = profiles_circles.circle_id').
  213 + where('circles.person_id = ?', person.id)
  214 + }
  215 +
  216 + scope :in_circle, -> circle{
  217 + distinct.select('profiles.*').
  218 + joins('left join profiles_circles ON profiles_circles.profile_id = profiles.id').
  219 + joins('left join circles ON circles.id = profiles_circles.circle_id').
  220 + where('circles.id = ?', circle.id)
  221 + }
  222 +
  223 + settings_items :allow_followers, :type => :boolean, :default => true
  224 + alias_method :allow_followers?, :allow_followers
  225 +
209 acts_as_trackable :dependent => :destroy 226 acts_as_trackable :dependent => :destroy
210 227
211 has_many :profile_activities 228 has_many :profile_activities
@@ -218,6 +235,9 @@ class Profile &lt; ApplicationRecord @@ -218,6 +235,9 @@ class Profile &lt; ApplicationRecord
218 235
219 has_many :email_templates, :foreign_key => :owner_id 236 has_many :email_templates, :foreign_key => :owner_id
220 237
  238 + has_many :profile_followers
  239 + has_many :followers, :class_name => 'Person', :through => :profile_followers, :source => :person
  240 +
221 # Although this should be a has_one relation, there are no non-silly names for 241 # Although this should be a has_one relation, there are no non-silly names for
222 # a foreign key on article to reference the template to which it is 242 # a foreign key on article to reference the template to which it is
223 # welcome_page... =P 243 # welcome_page... =P
@@ -769,6 +789,7 @@ private :generate_url, :url_options @@ -769,6 +789,7 @@ private :generate_url, :url_options
769 else 789 else
770 self.affiliate(person, Profile::Roles.admin(environment.id), attributes) if members.count == 0 790 self.affiliate(person, Profile::Roles.admin(environment.id), attributes) if members.count == 0
771 self.affiliate(person, Profile::Roles.member(environment.id), attributes) 791 self.affiliate(person, Profile::Roles.member(environment.id), attributes)
  792 + person.follow(self, Circle.find_or_create_by(:person => person, :name =>_('memberships'), :profile_type => 'Community'))
772 end 793 end
773 person.tasks.pending.of("InviteMember").select { |t| t.data[:community_id] == self.id }.each { |invite| invite.cancel } 794 person.tasks.pending.of("InviteMember").select { |t| t.data[:community_id] == self.id }.each { |invite| invite.cancel }
774 remove_from_suggestion_list person 795 remove_from_suggestion_list person
@@ -1112,7 +1133,11 @@ private :generate_url, :url_options @@ -1112,7 +1133,11 @@ private :generate_url, :url_options
1112 end 1133 end
1113 1134
1114 def followed_by?(person) 1135 def followed_by?(person)
1115 - person.is_member_of?(self) 1136 + (person == self) || (person.in? self.followers)
  1137 + end
  1138 +
  1139 + def in_social_circle?(person)
  1140 + (person == self) || (person.is_member_of?(self))
1116 end 1141 end
1117 1142
1118 def display_private_info_to?(user) 1143 def display_private_info_to?(user)
@@ -1153,4 +1178,8 @@ private :generate_url, :url_options @@ -1153,4 +1178,8 @@ private :generate_url, :url_options
1153 def allow_destroy?(person = nil) 1178 def allow_destroy?(person = nil)
1154 person.kind_of?(Profile) && person.has_permission?('destroy_profile', self) 1179 person.kind_of?(Profile) && person.has_permission?('destroy_profile', self)
1155 end 1180 end
  1181 +
  1182 + def in_circle?(circle, follower)
  1183 + ProfileFollower.with_follower(follower).with_circle(circle).with_profile(self).present?
  1184 + end
1156 end 1185 end
app/models/profile_follower.rb 0 → 100644
@@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
  1 +class ProfileFollower < ApplicationRecord
  2 + self.table_name = :profiles_circles
  3 + track_actions :new_follower, :after_create, :keep_params => ["follower.name", "follower.url", "follower.profile_custom_icon"], :custom_user => :profile
  4 +
  5 + attr_accessible :profile, :circle
  6 +
  7 + belongs_to :profile
  8 + belongs_to :circle
  9 +
  10 + has_one :person, through: :circle
  11 + alias follower person
  12 +
  13 + validates_presence_of :profile_id, :circle_id
  14 + validates :profile_id, :uniqueness => {:scope => :circle_id, :message => "can't put a profile in the same circle twice"}
  15 +
  16 + scope :with_follower, -> person{
  17 + joins(:circle).where('circles.person_id = ?', person.id)
  18 + }
  19 +
  20 + scope :with_profile, -> profile{
  21 + where(:profile => profile)
  22 + }
  23 +
  24 + scope :with_circle, -> circle{
  25 + where(:circle => circle)
  26 + }
  27 +
  28 +end
app/views/blocks/profile_info_actions/_circles.html.erb 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 +<div class="circles" id='circles-list'>
  2 +
  3 + <%= form_for :circles, :url => {:controller => 'profile', :action => 'follow'}, :html => {:id => "follow-circles-form"} do |f|%>
  4 + <%= render partial: "blocks/profile_info_actions/select_circles", :locals => {:circles => circles} %>
  5 +
  6 + <div id="circle-actions">
  7 + <%= submit_button :ok, _("Follow") %>
  8 + <input type="button" value="<%= _("Cancel") %>" id="cancel-set-circle" class="button with-text icon-cancel"/>
  9 + </div>
  10 + <% end %>
  11 +</div>
app/views/blocks/profile_info_actions/_common.html.erb
1 <li><%= report_abuse(profile, :button) %></li> 1 <li><%= report_abuse(profile, :button) %></li>
  2 +<%if logged_in? && (user != profile) && profile.allow_followers?%>
  3 +<li>
  4 + <% follow = user.follows?(profile) %>
  5 + <%= button(:unfollow, content_tag('span', _('Unfollow')), {:profile => profile.identifier, :controller => 'profile', :action => 'unfollow'}, :id => 'action-unfollow', :title => _("Unfollow"), :style => follow ? "" : "display: none;") %>
  6 + <%= button(:ok, content_tag('span', _('Follow')), {:profile => profile.identifier, :controller => 'profile', :action => 'find_profile_circles'}, :id => 'action-follow', :title => _("Follow"), :style => follow ? "display: none;" : "") %>
  7 + <div id="circles-container" style="display: none;">
  8 + </div>
  9 +</li>
  10 +<%end%>
2 <%= render_environment_features(:profile_actions) %> 11 <%= render_environment_features(:profile_actions) %>
app/views/blocks/profile_info_actions/_select_circles.html.erb 0 → 100644
@@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
  1 +<div class="circles" id='circles-list'>
  2 + <p><%= _("Select the circles for %s") % profile.name %></p>
  3 + <div id="circles-checkboxes">
  4 + <% circles.each do |circle| %>
  5 + <div class="circle">
  6 + <%= labelled_check_box circle.name, "circles[#{circle.name}]", circle.id, profile.in_circle?(circle, current_person) %>
  7 + </div>
  8 + <% end %>
  9 + </div>
  10 +
  11 + <a href="#" id="new-circle">
  12 + <span><%= _("New Circle") %></span>
  13 + </a>
  14 +
  15 + <div id="new-circle-form" style="display: none;">
  16 + <%= labelled_text_field _('Circle name') , 'circle[name]', "",:id => 'text-field-name-new-circle'%>
  17 + <%= hidden_field_tag('circle[profile_type]', profile.class.name) %>
  18 +
  19 + <%= button_bar do %>
  20 + <%= button(:save, _('Create'), {:profile => profile.identifier, :controller => 'circles', :action => 'xhr_create'}, :id => "new-circle-submit") %>
  21 + <%= button(:cancel, _('Cancel'), '#', :id => "new-circle-cancel") %>
  22 + <% end %>
  23 + </div>
  24 +</div>
app/views/circles/_circle_checkbox.html.erb 0 → 100644
@@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
  1 +<div class="circle">
  2 + <%= labelled_check_box circle.name, "circles[#{circle.name}]", circle.id %>
  3 +</div>
app/views/circles/_form.html.erb 0 → 100644
@@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
  1 +<%= error_messages_for :circle %>
  2 +
  3 +<%= labelled_form_for :circle, :url => (mode == :edit) ? {:action => 'update', :id => circle} : {:action => 'create'} do |f| %>
  4 +
  5 + <%= required_fields_message %>
  6 +
  7 + <%= required f.text_field(:name) %>
  8 +
  9 + <%= required labelled_form_field _("Profile type"), f.select(:profile_type, Circle.profile_types.to_a) %>
  10 +
  11 + <%= button_bar do %>
  12 + <%= submit_button('save', (mode == :edit) ? _('Save changes') : _('Create circle'), :cancel => {:action => 'index'} ) %>
  13 + <% end %>
  14 +<% end %>
app/views/circles/edit.html.erb 0 → 100644
@@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
  1 +<h1><%= _("Edit circle") %></h1>
  2 +
  3 +<%= render :partial => 'form', :locals => { :mode => :edit, :circle => @circle, :profile_types => @profile_types } %>
app/views/circles/index.html.erb 0 → 100644
@@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
  1 +<h1><%= _('Manage circles') %></h1>
  2 +
  3 +<table>
  4 + <tr>
  5 + <th><%= _('Circle name') %></th>
  6 + <th><%= _('Profile type') %></th>
  7 + <th><%= _('Actions') %></th>
  8 + </tr>
  9 + <% @circles.each do |circle| %>
  10 + <tr>
  11 + <td>
  12 + <%= circle.name %>
  13 + </td>
  14 + <td>
  15 + <%= _(circle.profile_type) %>
  16 + </td>
  17 + <td>
  18 + <div style="text-align: center;">
  19 + <%= button_without_text :edit, _('Edit'), :action => 'edit', :id => circle %>
  20 + <%= button_without_text :delete, _('Delete'), { :action => 'destroy', :id => circle }, { "data-method" => "POST" } %>
  21 + </div>
  22 + </td>
  23 + </tr>
  24 + <% end %>
  25 +</table>
  26 +
  27 +<%= button_bar do %>
  28 + <%= button :add, _('Create a new circle'), :action => 'new' %>
  29 + <%= button :back, _('Back to control panel'), :controller => 'profile_editor' %>
  30 +<% end %>
app/views/circles/new.html.erb 0 → 100644
@@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
  1 +<h1><%= _("New circle") %></h1>
  2 +
  3 +<%= render :partial => 'form', :locals => { :mode => :new, :circle => @circle, :profile_types => @profile_types } %>
app/views/followers/_edit_circles_modal.html.erb 0 → 100644
@@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
  1 +<div class="circles" id='circles-list'>
  2 + <%= form_for :circles, :url => {:controller => 'followers', :action => 'update_category'}, :html => {:id => "follow-circles-form"} do |f|%>
  3 + <%= render partial: "blocks/profile_info_actions/select_circles", :locals => {:circles => circles, :profile => profile} %>
  4 +
  5 + <%= hidden_field_tag('followed_profile_id', profile.id) %>
  6 +
  7 + <div id="circle-actions">
  8 + <div id="actions-container">
  9 + <%= submit_button('save', _('Save')) %>
  10 + <%= modal_close_button _("Cancel") %>
  11 + </div>
  12 + </div>
  13 + <% end %>
  14 +</div>
app/views/followers/_profile_list.html.erb 0 → 100644
@@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
  1 +<ul class="profile-list">
  2 + <% profiles.each do |profile| %>
  3 + <li>
  4 + <%= link_to_profile profile_image(profile) + tag('br') + profile.short_name,
  5 + profile.identifier, :class => 'profile-link' %>
  6 + <div class="controll">
  7 + <%= button_without_text :remove, content_tag('span',_('unfollow')),
  8 + { :controller => "profile", :profile => profile.identifier , :action => 'unfollow', :redirect_to => url_for({:controller => "followers", :profile => user.identifier}) },
  9 + :title => _('remove') %>
  10 + <%= modal_icon_button :edit, _('change category'),
  11 + url_for(:controller => 'followers', :action => 'set_category_modal',
  12 + :followed_profile_id => profile.id) %>
  13 + </div><!-- end class="controll" -->
  14 + </li>
  15 + <% end %>
  16 +</ul>
app/views/followers/index.html.erb 0 → 100644
@@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
  1 +<div id="manage_followed people">
  2 +
  3 +<h1><%= _("%s following") % profile.name %></h1>
  4 +
  5 +<% cache_timeout(profile.manage_friends_cache_key(params), 4.hours) do %>
  6 + <% if @followed_people.empty? %>
  7 + <p>
  8 + <em>
  9 + <%= _("You don't follow anybody yet.") %>
  10 + </em>
  11 + </p>
  12 + <% end %>
  13 +
  14 + <%= button_bar do %>
  15 + <%= button(:back, _('Back to control panel'), :controller => 'profile_editor') %>
  16 + <%= button(:search, _('Find people'), :controller => 'search', :action => 'assets', :asset => 'people') %>
  17 + <% end %>
  18 +
  19 + <%= labelled_select(_('Profile type')+': ', :filter_profile_type, :last, :first, @active_filter, @profile_types, :id => "profile-type-filter") %>
  20 +
  21 + <%= render :partial => 'profile_list', :locals => { :profiles => @followed_people } %>
  22 +
  23 + <br style="clear:both" />
  24 + <%= pagination_links @followed_people, :param_name => 'npage' %>
  25 +<% end %>
  26 +
  27 +</div>
app/views/person_notifier/mailer/_new_follower.html.erb 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +<%= render :partial => 'default_activity', :locals => { :activity => activity } %>
app/views/profile/_follow.html.erb 0 → 100644
@@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
  1 +<% cache_timeout(profile.friends_cache_key(params), 4.hours) do %>
  2 + <ul class='profile-list'>
  3 + <% follow.each do |follower| %>
  4 + <%= profile_image_link(follower) %>
  5 + <% end%>
  6 + </ul>
  7 +
  8 + <div id='pagination-profiles'>
  9 + <%= pagination_links follow, :param_name => 'npage' %>
  10 + </div>
  11 +<% end %>
  12 +
  13 +<%= button_bar do %>
  14 + <%= button :back, _('Go back'), { :controller => 'profile' } %>
  15 + <% if user == profile %>
  16 + <%= button :edit, _('Manage followed people'), :controller => 'friends', :action => 'index', :profile => profile.identifier %>
  17 + <% end %>
  18 +<% end %>
app/views/profile/_new_follower.html.erb 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +<%= render :partial => 'default_activity', :locals => { :activity => activity, :tab_action => tab_action } %>
app/views/profile/followed.html.erb 0 → 100644
@@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
  1 +<div class="common-profile-list-block">
  2 +
  3 +<h1><%= _("%s is followed by") % profile.name %></h1>
  4 +
  5 +<%= render :partial => 'follow', :locals => {:follow => @followed_by} %>
  6 +
  7 +</div>
app/views/profile/following.html.erb 0 → 100644
@@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
  1 +<div class="common-profile-list-block">
  2 +
  3 +<h1><%= _("%s is following") % profile.name %></h1>
  4 +
  5 +<%= render :partial => 'follow', :locals => {:follow => @followed_people} %>
  6 +
  7 +</div>
app/views/profile_editor/edit.html.erb
@@ -23,9 +23,11 @@ @@ -23,9 +23,11 @@
23 </div> 23 </div>
24 24
25 <h2><%= _('Privacy options') %></h2> 25 <h2><%= _('Privacy options') %></h2>
26 - 26 + <div>
  27 + <%= labelled_check_box _("Allow other users to follow me"), 'profile_data[allow_followers]', true, @profile.allow_followers?, :class => "person-can-be-followed" %>
  28 + </div>
27 <% if profile.person? %> 29 <% if profile.person? %>
28 - <div> 30 + <div id="profile_allow_follows">
29 <%= labelled_radio_button _('Public &mdash; show my contents to all internet users').html_safe, 'profile_data[public_profile]', true, @profile.public_profile? %> 31 <%= labelled_radio_button _('Public &mdash; show my contents to all internet users').html_safe, 'profile_data[public_profile]', true, @profile.public_profile? %>
30 </div> 32 </div>
31 <div> 33 <div>
app/views/profile_editor/index.html.erb
@@ -72,6 +72,11 @@ @@ -72,6 +72,11 @@
72 72
73 <%= control_panel_button(_('Email Templates'), 'email-templates', :controller => :profile_email_templates) if profile.organization? %> 73 <%= control_panel_button(_('Email Templates'), 'email-templates', :controller => :profile_email_templates) if profile.organization? %>
74 74
  75 + <% if profile.person? %>
  76 + <%= control_panel_button(_('Manage followed profiles'), 'edit-profile', :controller => :followers) %>
  77 + <%= control_panel_button(_('Manage circles'), 'edit-profile-group', :controller => :circles) %>
  78 + <% end %>
  79 +
75 <% @plugins.dispatch(:control_panel_buttons).each do |button| %> 80 <% @plugins.dispatch(:control_panel_buttons).each do |button| %>
76 <%= control_panel_button(button[:title], button[:icon], button[:url], button[:html_options]) %> 81 <%= control_panel_button(button[:title], button[:icon], button[:url], button[:html_options]) %>
77 <% end %> 82 <% end %>
config/initializers/action_tracker.rb
@@ -12,6 +12,10 @@ ActionTrackerConfig.verbs = { @@ -12,6 +12,10 @@ ActionTrackerConfig.verbs = {
12 type: :groupable 12 type: :groupable
13 }, 13 },
14 14
  15 + new_follower: {
  16 + type: :groupable
  17 + },
  18 +
15 join_community: { 19 join_community: {
16 type: :groupable 20 type: :groupable
17 }, 21 },
db/migrate/20160608123748_create_profile_followers_table.rb 0 → 100644
@@ -0,0 +1,42 @@ @@ -0,0 +1,42 @@
  1 +class CreateProfileFollowersTable < ActiveRecord::Migration
  2 + def up
  3 + create_table :profiles_circles do |t|
  4 + t.column :profile_id, :integer
  5 + t.column :circle_id, :integer
  6 + t.timestamps
  7 + end
  8 +
  9 + create_table :circles do |t|
  10 + t.column :name, :string
  11 + t.belongs_to :person
  12 + t.column :profile_type, :string, :null => false
  13 + end
  14 +
  15 + add_foreign_key :profiles_circles, :circles, :on_delete => :nullify
  16 +
  17 + add_index :profiles_circles, [:profile_id, :circle_id], :name => "profiles_circles_composite_key_index", :unique => true
  18 + add_index :circles, [:person_id, :name], :name => "circles_composite_key_index", :unique => true
  19 +
  20 + #insert one category for each friend group a person has
  21 + execute("INSERT INTO circles(name, person_id, profile_type) SELECT DISTINCT (CASE WHEN (f.group IS NULL OR f.group = '') THEN 'friendships' ELSE f.group END), f.person_id, 'Person' FROM friendships as f")
  22 + #insert 'memberships' category if a person is in a community as a member, moderator or profile admin
  23 + execute("INSERT INTO circles(name, person_id, profile_type) SELECT DISTINCT 'memberships', ra.accessor_id, 'Community' FROM role_assignments as ra JOIN roles ON ra.role_id = roles.id WHERE roles.name IN ('Member','Moderator','Profile Administrator')")
  24 + #insert 'favorites' category if a person has any favorited enterprise
  25 + execute("INSERT INTO circles(name, person_id, profile_type) SELECT DISTINCT 'favorites', person_id, 'Enterprise' FROM favorite_enterprise_people")
  26 +
  27 + #insert a follower entry for each friend, with the category the same as the friendship group or equals 'friendships'
  28 + execute("INSERT INTO profiles_circles(profile_id, circle_id) SELECT DISTINCT f.friend_id, c.id FROM friendships as f JOIN circles as c ON f.person_id = c.person_id WHERE c.name = f.group OR c.name = 'friendships'")
  29 + #insert a follower entry for each favorited enterprise, with the category 'favorites'
  30 + execute("INSERT INTO profiles_circles(profile_id, circle_id) SELECT DISTINCT f.enterprise_id, c.id FROM favorite_enterprise_people AS f JOIN circles as c ON f.person_id = c.person_id WHERE c.name = 'favorites' ")
  31 + #insert a follower entry for each community a person participates as a member, moderator or admininstrator
  32 + execute("INSERT INTO profiles_circles(profile_id, circle_id) SELECT DISTINCT ra.resource_id, c.id FROM role_assignments as ra JOIN roles ON ra.role_id = roles.id JOIN circles as c ON ra.accessor_id = c.person_id WHERE roles.name IN ('Member','Moderator','Profile Administrator') AND c.name = 'memberships'")
  33 + end
  34 +
  35 + def down
  36 + remove_foreign_key :profiles_circles, :circles
  37 + remove_index :profiles_circles, :name => "profiles_circles_composite_key_index"
  38 + remove_index :circles, :name => "circles_composite_key_index"
  39 + drop_table :circles
  40 + drop_table :profiles_circles
  41 + end
  42 +end
features/follow_profile.feature 0 → 100644
@@ -0,0 +1,114 @@ @@ -0,0 +1,114 @@
  1 +Feature: follow profile
  2 + As a noosfero user
  3 + I want to follow a profile
  4 + So I can receive notifications from it
  5 +
  6 + Background:
  7 + Given the following community
  8 + | identifier | name |
  9 + | nightswatch | Nights Watch |
  10 + And the following users
  11 + | login |
  12 + | johnsnow |
  13 + And the user "johnsnow" has the following circles
  14 + | name | profile_type |
  15 + | Family | Person |
  16 + | Work | Community |
  17 + | Favorites | Community |
  18 +
  19 + @selenium
  20 + Scenario: Common noofero user follow a community
  21 + Given I am logged in as "johnsnow"
  22 + When I go to nightswatch's homepage
  23 + When I follow "Follow"
  24 + When I check "Work"
  25 + When I press "Follow"
  26 + And I wait 1 second
  27 + Then "johnsnow" should be a follower of "nightswatch" in circle "Work"
  28 +
  29 + @selenium
  30 + Scenario: Common noofero user follow a community in more than one circle
  31 + Given I am logged in as "johnsnow"
  32 + When I go to nightswatch's homepage
  33 + When I follow "Follow"
  34 + When I check "Work"
  35 + When I check "Favorites"
  36 + When I press "Follow"
  37 + And I wait 1 second
  38 + Then "johnsnow" should be a follower of "nightswatch" in circle "Work"
  39 + And "johnsnow" should be a follower of "nightswatch" in circle "Favorites"
  40 +
  41 + @selenium
  42 + Scenario: No see another profile type circle when following a community
  43 + Given I am logged in as "johnsnow"
  44 + When I go to nightswatch's homepage
  45 + When I follow "Follow"
  46 + Then I should not see "Family"
  47 + And I should see "Favorites"
  48 + And I should see "Work"
  49 +
  50 + @selenium
  51 + Scenario: Common noofero user follow a community then cancel the action
  52 + Given I am logged in as "johnsnow"
  53 + When I go to nightswatch's homepage
  54 + When I follow "Follow"
  55 + When I press "Cancel"
  56 + And I wait 1 second
  57 + Then I should not see "Family"
  58 + And I should not see "Favorites"
  59 + And I should not see "Work"
  60 + And I should not see "New Circle"
  61 + Then "johnsnow" should not be a follower of "nightswatch"
  62 +
  63 + @selenium
  64 + Scenario: Common noofero user cancel the circle creation action
  65 + Given I am logged in as "johnsnow"
  66 + When I go to nightswatch's homepage
  67 + When I follow "Follow"
  68 + When I follow "New Circle"
  69 + When I press "Cancel"
  70 + And I wait 1 second
  71 + Then I should not see "Circle name"
  72 + And I should not see "Create"
  73 +
  74 + @selenium
  75 + Scenario: Noosfero user see new circle option when following a community
  76 + Given I am logged in as "johnsnow"
  77 + When I go to nightswatch's homepage
  78 + When I follow "Follow"
  79 + Then I should see "New Circle"
  80 +
  81 + @selenium
  82 + Scenario: Common noofero user follow a community with a new circle
  83 + Given I am logged in as "johnsnow"
  84 + When I go to nightswatch's homepage
  85 + When I follow "Follow"
  86 + When I follow "New Circle"
  87 + And I fill in "text-field-name-new-circle" with "Winterfell"
  88 + When I follow "Create"
  89 + When I check "Winterfell"
  90 + When I press "Follow"
  91 + And I wait 1 second
  92 + Then "johnsnow" should be a follower of "nightswatch" in circle "Winterfell"
  93 +
  94 + @selenium
  95 + Scenario: Common noofero user create a new circle when following a community
  96 + Given I am logged in as "johnsnow"
  97 + When I go to nightswatch's homepage
  98 + When I follow "Follow"
  99 + When I follow "New Circle"
  100 + And I fill in "text-field-name-new-circle" with "Winterfell"
  101 + When I follow "Create"
  102 + And I wait 1 second
  103 + Then "johnsnow" should have the circle "Winterfell" with profile type "Community"
  104 + Then I should not see "Circle name"
  105 + Then I should not see "Create"
  106 +
  107 + @selenium
  108 + Scenario: Common noofero user unfollow a community
  109 + Given "johnsnow" is a follower of "nightswatch" in circle "Work"
  110 + And I am logged in as "johnsnow"
  111 + When I go to nightswatch's homepage
  112 + When I follow "Unfollow"
  113 + Then "johnsnow" should not be a follower of "nightswatch"
  114 +
features/step_definitions/followers_steps.rb 0 → 100644
@@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
  1 +Given /^the user "(.+)" has the following circles$/ do |user_name,table|
  2 + person = User.find_by(:login => user_name).person
  3 + table.hashes.each do |circle|
  4 + Circle.create!(:person => person, :name => circle[:name], :profile_type => circle[:profile_type])
  5 + end
  6 +end
  7 +
  8 +Then /^"(.+)" should be a follower of "(.+)" in circle "(.+)"$/ do |person, profile, circle|
  9 + profile = Profile.find_by(identifier: profile)
  10 + followers = profile.followers
  11 + person = Person.find_by(identifier: person)
  12 + followers.should include(person)
  13 +
  14 + circle = Circle.find_by(:name => circle, :person => person)
  15 + ProfileFollower.find_by(:circle => circle, :profile => profile).should_not == nil
  16 +end
  17 +
  18 +Then /^"(.+)" should not be a follower of "(.+)"$/ do |person, profile|
  19 + profile = Profile.find_by(identifier: profile)
  20 + followers = profile.followers
  21 + person = Person.find_by(identifier: person)
  22 + followers.should_not include(person)
  23 +end
  24 +
  25 +Given /^"(.+)" is a follower of "(.+)" in circle "(.+)"$/ do |person, profile, circle|
  26 + profile = Profile.find_by(identifier: profile)
  27 + person = Person.find_by(identifier: person)
  28 + circle = Circle.find_by(:name => circle, :person => person)
  29 + ProfileFollower.create!(:circle => circle, :profile => profile)
  30 +end
  31 +
  32 +Then /^"(.+)" should have the circle "(.+)" with profile type "(.+)"$/ do |user_name, circle, profile_type|
  33 + person = User.find_by(:login => user_name).person
  34 + Circle.find_by(:name => circle, :person => person, :profile_type => profile_type).should_not == nil
  35 +end
public/javascripts/application.js
@@ -26,6 +26,8 @@ @@ -26,6 +26,8 @@
26 *= require pagination.js 26 *= require pagination.js
27 * views speficics 27 * views speficics
28 *= require add-and-join.js 28 *= require add-and-join.js
  29 +*= require followers.js
  30 +*= require manage-followers.js
29 *= require report-abuse.js 31 *= require report-abuse.js
30 *= require autogrow.js 32 *= require autogrow.js
31 *= require require_login.js 33 *= require require_login.js
@@ -550,6 +552,11 @@ function loading_for_button(selector) { @@ -550,6 +552,11 @@ function loading_for_button(selector) {
550 jQuery(selector).css('cursor', 'progress'); 552 jQuery(selector).css('cursor', 'progress');
551 } 553 }
552 554
  555 +function hide_loading_for_button(selector) {
  556 + selector.css("cursor","");
  557 + $(".small-loading").remove();
  558 +}
  559 +
553 function new_qualifier_row(selector, select_qualifiers, delete_button) { 560 function new_qualifier_row(selector, select_qualifiers, delete_button) {
554 index = jQuery(selector + ' tr').size() - 1; 561 index = jQuery(selector + ' tr').size() - 1;
555 jQuery(selector).append("<tr><td>" + select_qualifiers + "</td><td id='certifier-area-" + index + "'><select></select>" + delete_button + "</td></tr>"); 562 jQuery(selector).append("<tr><td>" + select_qualifiers + "</td><td id='certifier-area-" + index + "'><select></select>" + delete_button + "</td></tr>");
public/javascripts/followers.js 0 → 100644
@@ -0,0 +1,81 @@ @@ -0,0 +1,81 @@
  1 +$("#action-follow").live("click", function() {
  2 + var button = $(this);
  3 + var url = button.attr("href");
  4 + loading_for_button(button);
  5 +
  6 + $.post(url, function(data) {
  7 + button.fadeOut("fast", function() {
  8 + $("#circles-container").html(data);
  9 + $("#circles-container").fadeIn();
  10 + });
  11 + }).always(function() {
  12 + hide_loading_for_button(button);
  13 + });
  14 + return false;
  15 +});
  16 +
  17 +$("#cancel-set-circle").live("click", function() {
  18 + $("#circles-container").fadeOut("fast", function() {
  19 + $("#action-follow").fadeIn();
  20 + });
  21 + return false;
  22 +});
  23 +
  24 +$("#new-circle").live("click", function() {
  25 + $(this).fadeOut();
  26 + $("#circle-actions").fadeOut("fast", function() {
  27 + $("#new-circle-form").fadeIn();
  28 + });
  29 + return false;
  30 +});
  31 +
  32 +$("#new-circle-cancel").live("click", function() {
  33 + $("#new-circle-form").fadeOut("fast", function() {
  34 + $("#circle-actions").fadeIn();
  35 + $("#new-circle").fadeIn();
  36 + $("#text-field-name-new-circle").val('')
  37 + });
  38 + return false;
  39 +});
  40 +
  41 +$('#follow-circles-form').live("submit", function() {
  42 + var valuesToSubmit = $(this).serialize();
  43 + $.ajax({
  44 + type: "POST",
  45 + url: $(this).attr('action'),
  46 + data: valuesToSubmit,
  47 + dataType: "JSON",
  48 + statusCode: {
  49 + 200: function(response){
  50 + $("#circles-container").fadeOut();
  51 + $("#action-unfollow").fadeIn();
  52 + $.colorbox.close();
  53 + display_notice(response.responseText);
  54 + },
  55 + 400: function(response) {
  56 + display_notice(response.responseText);
  57 + }
  58 + }
  59 + })
  60 + return false;
  61 +});
  62 +
  63 +$("#new-circle-submit").live("click", function() {
  64 + $.ajax({
  65 + method: 'POST',
  66 + url: $(this).attr("href"),
  67 + data: {'circle[name]': $("#text-field-name-new-circle").val(),
  68 + 'circle[profile_type]': $("#circle_profile_type").val()},
  69 + success: function(response) {
  70 + $('#circles-checkboxes').append(response);
  71 + },
  72 + error: function(response) {
  73 + display_notice(response.responseText);
  74 + },
  75 + complete: function(response) {
  76 + $("#text-field-name-new-circle").val('')
  77 + $("#new-circle-cancel").trigger("click");
  78 + }
  79 + })
  80 + return false;
  81 +});
public/javascripts/manage-followers.js 0 → 100644
@@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
  1 +$('#profile-type-filter').live('change', function() {
  2 + var filter_type = $(this).val();
  3 + $(".profile-list").addClass("fetching");
  4 + $.get(window.location.pathname, {filter: filter_type}, function(data) {
  5 + $(".main-content").html(data);
  6 + }).fail(function(data) {
  7 + $(".profile-list").removeClass("fetching");
  8 + });
  9 +});
public/stylesheets/blocks/profile-info.scss
@@ -99,3 +99,44 @@ @@ -99,3 +99,44 @@
99 margin: 0px 0px 5px 0px; 99 margin: 0px 0px 5px 0px;
100 padding: 2px; 100 padding: 2px;
101 } 101 }
  102 +#circles-container {
  103 + background-color: #eee;
  104 + padding: 5px;
  105 + display: flex;
  106 +}
  107 +#circles-container p {
  108 + font-size: 12px;
  109 + margin-bottom: 5px;
  110 +}
  111 +#circle-actions {
  112 + margin-top: 15px;
  113 +}
  114 +#new-category-field-actions-block {
  115 + float: left;
  116 + width: 80%;
  117 + margin-bottom: 10px;
  118 +}
  119 +#new-circle-form {
  120 + margin-top: 10px;
  121 +}
  122 +#new-circle-form input {
  123 + width: 90px;
  124 +}
  125 +#new-circle-form select {
  126 + margin-top: 2px;
  127 + width: 95px;
  128 +}
  129 +#new-circle-form label {
  130 + font-size: 10px;
  131 + margin-right: 5px;
  132 +}
  133 +#new-circle-form .button-bar {
  134 + padding-top: 0px;
  135 +}
  136 +#new-circle-form .button {
  137 + width: 60px;
  138 +}
  139 +#new-circle-form .button-bar .button {
  140 + width: 40px;
  141 + font-size: 10px;
  142 +}
public/stylesheets/profile-activity.scss
@@ -167,7 +167,9 @@ li.profile-activity-item.upload_image .activity-gallery-images-count-1 img { @@ -167,7 +167,9 @@ li.profile-activity-item.upload_image .activity-gallery-images-count-1 img {
167 167
168 #profile-wall li.profile-activity-item.join_community .profile-activity-text a img, 168 #profile-wall li.profile-activity-item.join_community .profile-activity-text a img,
169 #profile-wall li.profile-activity-item.new_friendship .profile-activity-text a img, 169 #profile-wall li.profile-activity-item.new_friendship .profile-activity-text a img,
  170 +#profile-wall li.profile-activity-item.new_follower .profile-activity-text a img,
170 #profile-network li.profile-activity-item.join_community .profile-activity-text a img, 171 #profile-network li.profile-activity-item.join_community .profile-activity-text a img,
  172 +#profile-network li.profile-activity-item.new_follower .profile-activity-text a img,
171 #profile-network li.profile-activity-item.new_friendship .profile-activity-text a img { 173 #profile-network li.profile-activity-item.new_friendship .profile-activity-text a img {
172 margin: 5px 5px 0 0; 174 margin: 5px 5px 0 0;
173 padding: 1px; 175 padding: 1px;
public/stylesheets/profile-editor.scss
@@ -263,3 +263,6 @@ @@ -263,3 +263,6 @@
263 -webkit-border-radius: 5px; 263 -webkit-border-radius: 5px;
264 } 264 }
265 265
  266 +#profile_allow_follows {
  267 + margin-top: 10px;
  268 +}
public/stylesheets/profile-list.scss
@@ -23,6 +23,7 @@ @@ -23,6 +23,7 @@
23 } 23 }
24 .controller-favorite_enterprises .profile-list a.profile-link, 24 .controller-favorite_enterprises .profile-list a.profile-link,
25 .controller-friends .profile-list a.profile-link, 25 .controller-friends .profile-list a.profile-link,
  26 +.controller-followers .profile-list a.profile-link,
26 .list-profile-connections .profile-list a.profile-link, 27 .list-profile-connections .profile-list a.profile-link,
27 .profiles-suggestions .profile-list a.profile-link { 28 .profiles-suggestions .profile-list a.profile-link {
28 text-decoration: none; 29 text-decoration: none;
@@ -32,11 +33,13 @@ @@ -32,11 +33,13 @@
32 } 33 }
33 .controller-favorite_enterprises .profile-list a.profile-link:hover, 34 .controller-favorite_enterprises .profile-list a.profile-link:hover,
34 .controller-friends .profile-list a.profile-link:hover, 35 .controller-friends .profile-list a.profile-link:hover,
  36 +.controller-followers .profile-list a.profile-link:hover,
35 .profiles-suggestions .profile-list a.profile-link:hover { 37 .profiles-suggestions .profile-list a.profile-link:hover {
36 color: #FFF; 38 color: #FFF;
37 } 39 }
38 .controller-favorite_enterprises .profile-list .profile_link span, 40 .controller-favorite_enterprises .profile-list .profile_link span,
39 .controller-friends .profile-list .profile_link span, 41 .controller-friends .profile-list .profile_link span,
  42 +.controller-followers .profile-list .profile_link span,
40 .box-1 .profiles-suggestions .profile-list .profile_link span { 43 .box-1 .profiles-suggestions .profile-list .profile_link span {
41 width: 80px; 44 width: 80px;
42 display: block; 45 display: block;
@@ -44,12 +47,14 @@ @@ -44,12 +47,14 @@
44 } 47 }
45 .controller-favorite_enterprises .profile-list, 48 .controller-favorite_enterprises .profile-list,
46 .controller-friends .profile-list, 49 .controller-friends .profile-list,
  50 +.controller-followers .profile-list,
47 .profiles-suggestions .profile-list { 51 .profiles-suggestions .profile-list {
48 position: relative; 52 position: relative;
49 } 53 }
50 54
51 .controller-favorite_enterprises .profile-list .controll, 55 .controller-favorite_enterprises .profile-list .controll,
52 .controller-friends .profile-list .controll, 56 .controller-friends .profile-list .controll,
  57 +.controller-followers .profile-list .controll,
53 .profiles-suggestions .profile-list .controll { 58 .profiles-suggestions .profile-list .controll {
54 position: absolute; 59 position: absolute;
55 top: 7px; 60 top: 7px;
@@ -57,17 +62,20 @@ @@ -57,17 +62,20 @@
57 } 62 }
58 .controller-favorite_enterprises .profile-list .controll a, 63 .controller-favorite_enterprises .profile-list .controll a,
59 .controller-friends .profile-list .controll a, 64 .controller-friends .profile-list .controll a,
  65 +.controller-followers .profile-list .controll a,
60 .profiles-suggestions .profile-list .controll a { 66 .profiles-suggestions .profile-list .controll a {
61 display: block; 67 display: block;
62 margin-bottom: 2px; 68 margin-bottom: 2px;
63 } 69 }
64 .controller-favorite_enterprises .msie6 .profile-list .controll a, 70 .controller-favorite_enterprises .msie6 .profile-list .controll a,
65 .controller-friends .msie6 .profile-list .controll a, 71 .controller-friends .msie6 .profile-list .controll a,
  72 +.controller-folloed_people .msie6 .profile-list .controll a,
66 .profiles-suggestions .msie6 .profile-list .controll a { 73 .profiles-suggestions .msie6 .profile-list .controll a {
67 width: 0px; 74 width: 0px;
68 } 75 }
69 .controller-favorite_enterprises .button-bar, 76 .controller-favorite_enterprises .button-bar,
70 .controller-friends .button-bar, 77 .controller-friends .button-bar,
  78 +.controller-followers .button-bar,
71 .profiles-suggestions .button-bar { 79 .profiles-suggestions .button-bar {
72 clear: both; 80 clear: both;
73 padding-top: 20px; 81 padding-top: 20px;
@@ -208,22 +216,35 @@ @@ -208,22 +216,35 @@
208 font-size: 12px; 216 font-size: 12px;
209 } 217 }
210 .action-profile-members .profile_link{ 218 .action-profile-members .profile_link{
211 - position: relative; 219 + position: relative;
212 } 220 }
213 .action-profile-members .profile_link span.new-profile:last-child{ 221 .action-profile-members .profile_link span.new-profile:last-child{
214 - position: absolute;  
215 - top: 3px;  
216 - right: 2px;  
217 - text-transform: uppercase;  
218 - color: #FFF;  
219 - font-size: 9px;  
220 - background: #66CC33;  
221 - padding: 2px;  
222 - display: block;  
223 - width: 35px;  
224 - font-weight: 700; 222 + position: absolute;
  223 + top: 3px;
  224 + right: 2px;
  225 + text-transform: uppercase;
  226 + color: #FFF;
  227 + font-size: 9px;
  228 + background: #66CC33;
  229 + padding: 2px;
  230 + display: block;
  231 + width: 35px;
  232 + font-weight: 700;
225 } 233 }
226 .action-profile-members .profile_link .fn{ 234 .action-profile-members .profile_link .fn{
227 - font-style: normal;  
228 - color: #000; 235 + font-style: normal;
  236 + color: #000;
  237 +}
  238 +.category-name {
  239 + margin-top: 0px;
  240 + margin-bottom: 0px;
  241 + font-style: italic;
  242 + color: #888a85;
  243 + text-align: center;
  244 +}
  245 +.set-category-modal {
  246 + width: 250px;
  247 +}
  248 +.set-category-modal #actions-container {
  249 + margin-top: 20px
229 } 250 }
public/stylesheets/profile.scss
@@ -39,3 +39,8 @@ @@ -39,3 +39,8 @@
39 width: 470px; 39 width: 470px;
40 overflow-x: hidden; 40 overflow-x: hidden;
41 } 41 }
  42 +
  43 +#circles-checkboxes {
  44 + text-align: left;
  45 + margin-left: 15%;
  46 +}
test/functional/circles_controller_test.rb 0 → 100644
@@ -0,0 +1,129 @@ @@ -0,0 +1,129 @@
  1 +require_relative "../test_helper"
  2 +require 'circles_controller'
  3 +
  4 +class CirclesControllerTest < ActionController::TestCase
  5 +
  6 + def setup
  7 + @controller = CirclesController.new
  8 + @person = create_user('person').person
  9 + login_as(@person.identifier)
  10 + end
  11 +
  12 + should 'return all circles of a profile' do
  13 + circle1 = Circle.create!(:name => "circle1", :person => @person, :profile_type => 'Person')
  14 + circle2 = Circle.create!(:name => "circle2", :person => @person, :profile_type => 'Person')
  15 + get :index, :profile => @person.identifier
  16 +
  17 + assert_equivalent [circle1, circle2], assigns[:circles]
  18 + end
  19 +
  20 + should 'initialize an empty circle for creation' do
  21 + get :new, :profile => @person.identifier
  22 + assert_nil assigns[:circle].id
  23 + assert_nil assigns[:circle].name
  24 + end
  25 +
  26 + should 'create a new circle' do
  27 + assert_difference '@person.circles.count' do
  28 + post :create, :profile => @person.identifier,
  29 + :circle => { :name => 'circle' , :profile_type => Person.name}
  30 + end
  31 + assert_redirected_to :action => :index
  32 + end
  33 +
  34 + should 'not create a circle without a name' do
  35 + assert_no_difference '@person.circles.count' do
  36 + post :create, :profile => @person.identifier, :circle => { :name => nil }
  37 + end
  38 + assert_template :new
  39 + end
  40 +
  41 + should 'retrieve an existing circle when editing' do
  42 + circle = Circle.create!(:name => "circle", :person => @person, :profile_type => 'Person')
  43 + get :edit, :profile => @person.identifier, :id => circle.id
  44 + assert_equal circle.name, assigns[:circle].name
  45 + end
  46 +
  47 + should 'return 404 when editing a circle that does not exist' do
  48 + get :edit, :profile => @person.identifier, :id => "nope"
  49 + assert_response 404
  50 + end
  51 +
  52 + should 'update an existing circle' do
  53 + circle = Circle.create!(:name => "circle", :person => @person, :profile_type => 'Person')
  54 + post :update, :profile => @person.identifier, :id => circle.id,
  55 + :circle => { :name => "new name" }
  56 +
  57 + circle.reload
  58 + assert_equal "new name", circle.name
  59 + assert_redirected_to :action => :index
  60 + end
  61 +
  62 + should 'not update an existing circle without a name' do
  63 + circle = Circle.create!(:name => "circle", :person => @person, :profile_type => 'Person')
  64 + post :update, :profile => @person.identifier, :id => circle.id,
  65 + :circle => { :name => nil }
  66 +
  67 + circle.reload
  68 + assert_equal "circle", circle.name
  69 + assert_template :edit
  70 + end
  71 +
  72 + should 'return 404 when updating a circle that does not exist' do
  73 + post :update, :profile => @person.identifier, :id => "nope", :name => "new name"
  74 + assert_response 404
  75 + end
  76 +
  77 + should 'destroy an existing circle and update related profiles' do
  78 + circle = Circle.create!(:name => "circle", :person => @person, :profile_type => 'Person')
  79 + follower = fast_create(ProfileFollower, :profile_id => fast_create(Person).id,
  80 + :circle_id => circle.id)
  81 +
  82 + assert_difference "@person.circles.count", -1 do
  83 + post :destroy, :profile => @person.identifier, :id => circle.id
  84 + end
  85 +
  86 + follower.reload
  87 + assert_nil follower.circle
  88 + end
  89 +
  90 + should 'not destroy an existing circle if action is not post' do
  91 + circle = Circle.create!(:name => "circle", :person => @person, :profile_type => 'Person')
  92 +
  93 + assert_no_difference "@person.circles.count" do
  94 + get :destroy, :profile => @person.identifier, :id => circle.id
  95 + end
  96 + assert_response 404
  97 + end
  98 +
  99 + should 'return 404 when deleting and circle that does not exist' do
  100 + get :destroy, :profile => @person.identifier, :id => "nope"
  101 + assert_response 404
  102 + end
  103 +
  104 + should 'return 404 for xhr_create if request is not xhr' do
  105 + post :xhr_create, :profile => @person.identifier
  106 + assert_response 404
  107 + end
  108 +
  109 + should 'return 400 if not possible to create circle via xhr' do
  110 + xhr :post, :xhr_create, :profile => @person.identifier,
  111 + :circle => { :name => 'Invalid Circle' }
  112 + assert_response 400
  113 + end
  114 +
  115 + should 'create a new circle via xhr' do
  116 + xhr :post, :xhr_create, :profile => @person.identifier,
  117 + :circle => { :name => 'A Brand New Circle',
  118 + :profile_type => Person.name }
  119 + assert_response 201
  120 + assert_match /A Brand New Circle/, response.body
  121 + end
  122 +
  123 + should 'not create a new circle via xhr with an invalid profile_type' do
  124 + xhr :post, :xhr_create, :profile => @person.identifier,
  125 + :circle => { :name => 'A Brand New Circle',
  126 + :profile_type => '__invalid__' }
  127 + assert_response 400
  128 + end
  129 +end
test/functional/followers_controller_test.rb 0 → 100644
@@ -0,0 +1,58 @@ @@ -0,0 +1,58 @@
  1 +require_relative "../test_helper"
  2 +require 'followers_controller'
  3 +
  4 +class FollowersControllerTest < ActionController::TestCase
  5 + def setup
  6 + @profile = create_user('testuser').person
  7 + end
  8 +
  9 + should 'return followed people list' do
  10 + login_as(@profile.identifier)
  11 + person = fast_create(Person)
  12 + circle = Circle.create!(:person=> @profile, :name => "Zombies", :profile_type => 'Person')
  13 + fast_create(ProfileFollower, :profile_id => person.id, :circle_id => circle.id)
  14 +
  15 + get :index, :profile => @profile.identifier
  16 + assert_includes assigns(:followed_people), person
  17 + end
  18 +
  19 + should 'return filtered followed people list' do
  20 + login_as(@profile.identifier)
  21 + person = fast_create(Person)
  22 + community = fast_create(Community)
  23 + circle = Circle.create!(:person=> @profile, :name => "Zombies", :profile_type => 'Person')
  24 + circle2 = Circle.create!(:person=> @profile, :name => "Teams", :profile_type => 'Community')
  25 + fast_create(ProfileFollower, :profile_id => person.id, :circle_id => circle.id)
  26 + fast_create(ProfileFollower, :profile_id => community.id, :circle_id => circle2.id)
  27 +
  28 + get :index, :profile => @profile.identifier, :filter => "Community"
  29 + assert_equal assigns(:followed_people), [community]
  30 +
  31 + get :index, :profile => @profile.identifier, :filter => "Person"
  32 + assert_equal assigns(:followed_people), [person]
  33 + end
  34 +
  35 + should 'redirect to login page if not logged in' do
  36 + get :index, :profile => @profile.identifier
  37 + assert_redirected_to :controller => 'account', :action => 'login'
  38 + end
  39 +
  40 + should 'render set category modal' do
  41 + login_as(@profile.identifier)
  42 + person = fast_create(Person)
  43 + get :set_category_modal, :profile => @profile.identifier, :followed_profile_id => person.id
  44 + assert_tag :tag => "input", :attributes => { :id => "followed_profile_id", :value => person.id }
  45 + end
  46 +
  47 + should 'update followed person category' do
  48 + login_as(@profile.identifier)
  49 + person = fast_create(Person)
  50 + circle = Circle.create!(:person=> @profile, :name => "Zombies", :profile_type => 'Person')
  51 + circle2 = Circle.create!(:person=> @profile, :name => "DotA", :profile_type => 'Person')
  52 + fast_create(ProfileFollower, :profile_id => person.id, :circle_id => circle.id)
  53 +
  54 + post :update_category, :profile => @profile.identifier, :circles => {"DotA"=> circle2.id}, :followed_profile_id => person.id
  55 + assert_equivalent ProfileFollower.with_profile(person).with_follower(@profile).map(&:circle), [circle2]
  56 + end
  57 +
  58 +end
test/functional/profile_controller_test.rb
@@ -771,12 +771,15 @@ class ProfileControllerTest &lt; ActionController::TestCase @@ -771,12 +771,15 @@ class ProfileControllerTest &lt; ActionController::TestCase
771 assert_equal 15, assigns(:activities).size 771 assert_equal 15, assigns(:activities).size
772 end 772 end
773 773
774 - should 'not see the friends activities in the current profile' do 774 + should 'not see the followers activities in the current profile' do
  775 + circle = Circle.create!(:person=> profile, :name => "Zombies", :profile_type => 'Person')
  776 +
775 p2 = create_user.person 777 p2 = create_user.person
776 - refute profile.is_a_friend?(p2) 778 + refute profile.follows?(p2)
777 p3 = create_user.person 779 p3 = create_user.person
778 - p3.add_friend(profile)  
779 - assert p3.is_a_friend?(profile) 780 + profile.follow(p3, circle)
  781 + assert profile.follows?(p3)
  782 +
780 ActionTracker::Record.destroy_all 783 ActionTracker::Record.destroy_all
781 784
782 scrap1 = create(Scrap, defaults_for_scrap(:sender => p2, :receiver => p3)) 785 scrap1 = create(Scrap, defaults_for_scrap(:sender => p2, :receiver => p3))
@@ -964,7 +967,11 @@ class ProfileControllerTest &lt; ActionController::TestCase @@ -964,7 +967,11 @@ class ProfileControllerTest &lt; ActionController::TestCase
964 should 'have activities defined if logged in and is following profile' do 967 should 'have activities defined if logged in and is following profile' do
965 login_as(profile.identifier) 968 login_as(profile.identifier)
966 p1= fast_create(Person) 969 p1= fast_create(Person)
967 - p1.add_friend(profile) 970 +
  971 + circle = Circle.create!(:person=> profile, :name => "Zombies", :profile_type => 'Person')
  972 +
  973 + profile.follow(p1, circle)
  974 +
968 ActionTracker::Record.destroy_all 975 ActionTracker::Record.destroy_all
969 get :index, :profile => p1.identifier 976 get :index, :profile => p1.identifier
970 assert_equal [], assigns(:activities) 977 assert_equal [], assigns(:activities)
@@ -1932,4 +1939,110 @@ class ProfileControllerTest &lt; ActionController::TestCase @@ -1932,4 +1939,110 @@ class ProfileControllerTest &lt; ActionController::TestCase
1932 assert_redirected_to :controller => 'account', :action => 'login' 1939 assert_redirected_to :controller => 'account', :action => 'login'
1933 end 1940 end
1934 1941
  1942 + should 'not follow a user without defining a circle' do
  1943 + login_as(@profile.identifier)
  1944 + person = fast_create(Person)
  1945 + assert_no_difference 'ProfileFollower.count' do
  1946 + post :follow, :profile => person.identifier, :circles => {}
  1947 + end
  1948 + end
  1949 +
  1950 + should "not follow user if not logged" do
  1951 + person = fast_create(Person)
  1952 + get :follow, :profile => person.identifier
  1953 +
  1954 + assert_redirected_to :controller => 'account', :action => 'login'
  1955 + end
  1956 +
  1957 + should 'follow a user with a circle' do
  1958 + login_as(@profile.identifier)
  1959 + person = fast_create(Person)
  1960 +
  1961 + circle = Circle.create!(:person=> @profile, :name => "Zombies", :profile_type => 'Person')
  1962 +
  1963 + assert_difference 'ProfileFollower.count' do
  1964 + post :follow, :profile => person.identifier, :circles => {"Zombies" => circle.id}
  1965 + end
  1966 + end
  1967 +
  1968 + should 'follow a user with more than one circle' do
  1969 + login_as(@profile.identifier)
  1970 + person = fast_create(Person)
  1971 +
  1972 + circle = Circle.create!(:person=> @profile, :name => "Zombies", :profile_type => 'Person')
  1973 + circle2 = Circle.create!(:person=> @profile, :name => "Brainsss", :profile_type => 'Person')
  1974 +
  1975 + assert_difference 'ProfileFollower.count', 2 do
  1976 + post :follow, :profile => person.identifier, :circles => {"Zombies" => circle.id, "Brainsss"=> circle2.id}
  1977 + end
  1978 + end
  1979 +
  1980 + should 'not follow a user with no circle selected' do
  1981 + login_as(@profile.identifier)
  1982 + person = fast_create(Person)
  1983 +
  1984 + circle = Circle.create!(:person=> @profile, :name => "Zombies", :profile_type => 'Person')
  1985 + circle2 = Circle.create!(:person=> @profile, :name => "Brainsss", :profile_type => 'Person')
  1986 +
  1987 + assert_no_difference 'ProfileFollower.count' do
  1988 + post :follow, :profile => person.identifier, :circles => {"Zombies" => "0", "Brainsss" => "0"}
  1989 + end
  1990 +
  1991 + assert_match /Select at least one circle to follow/, response.body
  1992 + end
  1993 +
  1994 + should 'not follow if current_person already follows the person' do
  1995 + login_as(@profile.identifier)
  1996 + person = fast_create(Person)
  1997 +
  1998 + circle = Circle.create!(:person=> @profile, :name => "Zombies", :profile_type => 'Person')
  1999 + fast_create(ProfileFollower, :profile_id => person.id, :circle_id => circle.id)
  2000 +
  2001 + assert_no_difference 'ProfileFollower.count' do
  2002 + post :follow, :profile => person.identifier, :follow => { :circles => {"Zombies" => circle.id} }
  2003 + end
  2004 + assert_response 400
  2005 + end
  2006 +
  2007 + should "not unfollow user if not logged" do
  2008 + person = fast_create(Person)
  2009 + get :unfollow, :profile => person.identifier
  2010 +
  2011 + assert_redirected_to :controller => 'account', :action => 'login'
  2012 + end
  2013 +
  2014 + should "unfollow a followed person" do
  2015 + login_as(@profile.identifier)
  2016 + person = fast_create(Person)
  2017 +
  2018 + circle = Circle.create!(:person=> @profile, :name => "Zombies", :profile_type => 'Person')
  2019 + follower = fast_create(ProfileFollower, :profile_id => person.id, :circle_id => circle.id)
  2020 +
  2021 + assert_not_nil follower
  2022 +
  2023 + get :unfollow, :profile => person.identifier
  2024 + follower = ProfileFollower.find_by(:profile_id => person.id, :circle_id => circle.id)
  2025 + assert_nil follower
  2026 + end
  2027 +
  2028 + should "not unfollow a not followed person" do
  2029 + login_as(@profile.identifier)
  2030 + person = fast_create(Person)
  2031 +
  2032 + assert_no_difference 'ProfileFollower.count' do
  2033 + get :unfollow, :profile => person.identifier
  2034 + end
  2035 + end
  2036 +
  2037 + should "redirect to page after unfollow" do
  2038 + login_as(@profile.identifier)
  2039 + person = fast_create(Person)
  2040 +
  2041 + circle = Circle.create!(:person=> @profile, :name => "Zombies", :profile_type => 'Person')
  2042 + fast_create(ProfileFollower, :profile_id => person.id, :circle_id => circle.id)
  2043 +
  2044 + get :unfollow, :profile => person.identifier, :redirect_to => "/some/url"
  2045 + assert_redirected_to "/some/url"
  2046 + end
  2047 +
1935 end 2048 end
test/unit/article_test.rb
@@ -1099,9 +1099,10 @@ class ArticleTest &lt; ActiveSupport::TestCase @@ -1099,9 +1099,10 @@ class ArticleTest &lt; ActiveSupport::TestCase
1099 assert_equal 3, ActionTrackerNotification.where(action_tracker_id: second_activity.id).count 1099 assert_equal 3, ActionTrackerNotification.where(action_tracker_id: second_activity.id).count
1100 end 1100 end
1101 1101
1102 - should 'create notifications to friends when creating an article' do 1102 + should 'create notifications to followers when creating an article' do
1103 friend = fast_create(Person) 1103 friend = fast_create(Person)
1104 - profile.add_friend(friend) 1104 + circle = Circle.create!(:person=> friend, :name => "Zombies", :profile_type => 'Person')
  1105 + friend.follow(profile, circle)
1105 Article.destroy_all 1106 Article.destroy_all
1106 ActionTracker::Record.destroy_all 1107 ActionTracker::Record.destroy_all
1107 ActionTrackerNotification.destroy_all 1108 ActionTrackerNotification.destroy_all
@@ -1112,9 +1113,10 @@ class ArticleTest &lt; ActiveSupport::TestCase @@ -1112,9 +1113,10 @@ class ArticleTest &lt; ActiveSupport::TestCase
1112 assert_equal friend, ActionTrackerNotification.last.profile 1113 assert_equal friend, ActionTrackerNotification.last.profile
1113 end 1114 end
1114 1115
1115 - should 'create the notification to the friend when one friend has the notification and the other no' do 1116 + should 'create the notification to the follower when one follower has the notification and the other no' do
1116 f1 = fast_create(Person) 1117 f1 = fast_create(Person)
1117 - profile.add_friend(f1) 1118 + circle = Circle.create!(:person=> f1, :name => "Zombies", :profile_type => 'Person')
  1119 + f1.follow(profile, circle)
1118 1120
1119 User.current = profile.user 1121 User.current = profile.user
1120 article = create TinyMceArticle, :name => 'Tracked Article 1', :profile_id => profile.id 1122 article = create TinyMceArticle, :name => 'Tracked Article 1', :profile_id => profile.id
@@ -1123,16 +1125,22 @@ class ArticleTest &lt; ActiveSupport::TestCase @@ -1123,16 +1125,22 @@ class ArticleTest &lt; ActiveSupport::TestCase
1123 assert_equal 2, ActionTrackerNotification.where(action_tracker_id: article.activity.id).count 1125 assert_equal 2, ActionTrackerNotification.where(action_tracker_id: article.activity.id).count
1124 1126
1125 f2 = fast_create(Person) 1127 f2 = fast_create(Person)
1126 - profile.add_friend(f2) 1128 + circle2 = Circle.create!(:person=> f2, :name => "Zombies", :profile_type => 'Person')
  1129 + f2.follow(profile, circle2)
  1130 +
1127 article2 = create TinyMceArticle, :name => 'Tracked Article 2', :profile_id => profile.id 1131 article2 = create TinyMceArticle, :name => 'Tracked Article 2', :profile_id => profile.id
1128 assert_equal 2, ActionTracker::Record.where(verb: 'create_article').count 1132 assert_equal 2, ActionTracker::Record.where(verb: 'create_article').count
1129 process_delayed_job_queue 1133 process_delayed_job_queue
1130 assert_equal 3, ActionTrackerNotification.where(action_tracker_id: article2.activity.id).count 1134 assert_equal 3, ActionTrackerNotification.where(action_tracker_id: article2.activity.id).count
1131 end 1135 end
1132 1136
1133 - should 'destroy activity and notifications of friends when destroying an article' do 1137 + should 'destroy activity and notifications of followers when destroying an article' do
1134 friend = fast_create(Person) 1138 friend = fast_create(Person)
1135 - profile.add_friend(friend) 1139 +
  1140 + circle = Circle.create!(:person=> friend, :name => "Zombies", :profile_type => 'Person')
  1141 +
  1142 + friend.follow(profile, circle)
  1143 +
1136 Article.destroy_all 1144 Article.destroy_all
1137 ActionTracker::Record.destroy_all 1145 ActionTracker::Record.destroy_all
1138 ActionTrackerNotification.destroy_all 1146 ActionTrackerNotification.destroy_all
test/unit/friendship_test.rb
@@ -28,14 +28,14 @@ class FriendshipTest &lt; ActiveSupport::TestCase @@ -28,14 +28,14 @@ class FriendshipTest &lt; ActiveSupport::TestCase
28 f.person = a 28 f.person = a
29 f.friend = b 29 f.friend = b
30 f.save! 30 f.save!
31 - ta = ActionTracker::Record.last 31 + ta = ActionTracker::Record.where(:target_type => "Friendship").last
32 assert_equal a, ta.user 32 assert_equal a, ta.user
33 assert_equal 'b', ta.get_friend_name[0] 33 assert_equal 'b', ta.get_friend_name[0]
34 f = Friendship.new 34 f = Friendship.new
35 f.person = a 35 f.person = a
36 f.friend = c 36 f.friend = c
37 f.save! 37 f.save!
38 - ta = ActionTracker::Record.last 38 + ta = ActionTracker::Record.where(:target_type => "Friendship").last
39 assert_equal a, ta.user 39 assert_equal a, ta.user
40 assert_equal 'c', ta.get_friend_name[1] 40 assert_equal 'c', ta.get_friend_name[1]
41 end 41 end
@@ -46,14 +46,14 @@ class FriendshipTest &lt; ActiveSupport::TestCase @@ -46,14 +46,14 @@ class FriendshipTest &lt; ActiveSupport::TestCase
46 f.person = a 46 f.person = a
47 f.friend = b 47 f.friend = b
48 f.save! 48 f.save!
49 - ta = ActionTracker::Record.last 49 + ta = ActionTracker::Record.where(:target_type => "Friendship").last
50 assert_equal a, ta.user 50 assert_equal a, ta.user
51 assert_equal ['b'], ta.get_friend_name 51 assert_equal ['b'], ta.get_friend_name
52 f = Friendship.new 52 f = Friendship.new
53 f.person = b 53 f.person = b
54 f.friend = a 54 f.friend = a
55 f.save! 55 f.save!
56 - ta = ActionTracker::Record.last 56 + ta = ActionTracker::Record.where(:target_type => "Friendship").last
57 assert_equal b, ta.user 57 assert_equal b, ta.user
58 assert_equal ['a'], ta.get_friend_name 58 assert_equal ['a'], ta.get_friend_name
59 end 59 end
@@ -72,4 +72,55 @@ class FriendshipTest &lt; ActiveSupport::TestCase @@ -72,4 +72,55 @@ class FriendshipTest &lt; ActiveSupport::TestCase
72 assert_not_includes p2.friends(true), p1 72 assert_not_includes p2.friends(true), p1
73 end 73 end
74 74
  75 + should 'add follower when adding friend' do
  76 + p1 = create_user('testuser1').person
  77 + p2 = create_user('testuser2').person
  78 +
  79 + assert_difference 'ProfileFollower.count', 2 do
  80 + p1.add_friend(p2, 'friends')
  81 + p2.add_friend(p1, 'friends')
  82 + end
  83 +
  84 + assert_includes p1.followers(true), p2
  85 + assert_includes p2.followers(true), p1
  86 + end
  87 +
  88 + should 'remove follower when a friend removal occurs' do
  89 + p1 = create_user('testuser1').person
  90 + p2 = create_user('testuser2').person
  91 +
  92 + p1.add_friend(p2, 'friends')
  93 + p2.add_friend(p1, 'friends')
  94 +
  95 + Friendship.remove_friendship(p1, p2)
  96 +
  97 + assert_not_includes p1.followers(true), p2
  98 + assert_not_includes p2.followers(true), p1
  99 + end
  100 +
  101 + should 'keep friendship intact when stop following' do
  102 + p1 = create_user('testuser1').person
  103 + p2 = create_user('testuser2').person
  104 +
  105 + p1.add_friend(p2, 'friends')
  106 + p2.add_friend(p1, 'friends')
  107 +
  108 + p1.unfollow(p2)
  109 +
  110 + assert_includes p1.friends(true), p2
  111 + assert_includes p2.friends(true), p1
  112 + end
  113 +
  114 + should 'do not add friendship when start following' do
  115 + p1 = create_user('testuser1').person
  116 + p2 = create_user('testuser2').person
  117 +
  118 + circle1 = Circle.create!(:person=> p1, :name => "Zombies", :profile_type => 'Person')
  119 + circle2 = Circle.create!(:person=> p2, :name => "Zombies", :profile_type => 'Person')
  120 + p1.follow(p2, circle1)
  121 + p2.follow(p1, circle2)
  122 +
  123 + assert_not_includes p1.friends(true), p2
  124 + assert_not_includes p2.friends(true), p1
  125 + end
75 end 126 end
test/unit/notify_activity_to_profiles_job_test.rb
@@ -24,15 +24,21 @@ class NotifyActivityToProfilesJobTest &lt; ActiveSupport::TestCase @@ -24,15 +24,21 @@ class NotifyActivityToProfilesJobTest &lt; ActiveSupport::TestCase
24 end 24 end
25 end 25 end
26 26
27 - should 'notify just the users and his friends tracking user actions' do 27 + should 'notify just the users and his followers tracking user actions' do
28 person = fast_create(Person) 28 person = fast_create(Person)
29 community = fast_create(Community) 29 community = fast_create(Community)
30 action_tracker = fast_create(ActionTracker::Record, :user_type => 'Profile', :user_id => person.id, :target_type => 'Profile', :verb => 'create_article') 30 action_tracker = fast_create(ActionTracker::Record, :user_type => 'Profile', :user_id => person.id, :target_type => 'Profile', :verb => 'create_article')
31 refute NotifyActivityToProfilesJob::NOTIFY_ONLY_COMMUNITY.include?(action_tracker.verb) 31 refute NotifyActivityToProfilesJob::NOTIFY_ONLY_COMMUNITY.include?(action_tracker.verb)
32 p1, p2, m1, m2 = fast_create(Person), fast_create(Person), fast_create(Person), fast_create(Person) 32 p1, p2, m1, m2 = fast_create(Person), fast_create(Person), fast_create(Person), fast_create(Person)
33 - fast_create(Friendship, :person_id => person.id, :friend_id => p1.id)  
34 - fast_create(Friendship, :person_id => person.id, :friend_id => p2.id)  
35 - fast_create(Friendship, :person_id => p1.id, :friend_id => m1.id) 33 +
  34 + circle1 = Circle.create!(:person=> p1, :name => "Zombies", :profile_type => 'Person')
  35 + circle2 = Circle.create!(:person=> p2, :name => "Zombies", :profile_type => 'Person')
  36 + circle = Circle.create!(:person=> person, :name => "Zombies", :profile_type => 'Person')
  37 +
  38 + fast_create(ProfileFollower, :profile_id => person.id, :circle_id => circle1.id)
  39 + fast_create(ProfileFollower, :profile_id => person.id, :circle_id => circle2.id)
  40 + fast_create(ProfileFollower, :profile_id => m1.id, :circle_id => circle.id)
  41 +
36 fast_create(RoleAssignment, :accessor_id => m2.id, :role_id => 3, :resource_id => community.id) 42 fast_create(RoleAssignment, :accessor_id => m2.id, :role_id => 3, :resource_id => community.id)
37 ActionTrackerNotification.delete_all 43 ActionTrackerNotification.delete_all
38 job = NotifyActivityToProfilesJob.new(action_tracker.id) 44 job = NotifyActivityToProfilesJob.new(action_tracker.id)
@@ -66,23 +72,24 @@ class NotifyActivityToProfilesJobTest &lt; ActiveSupport::TestCase @@ -66,23 +72,24 @@ class NotifyActivityToProfilesJobTest &lt; ActiveSupport::TestCase
66 end 72 end
67 end 73 end
68 74
69 - should 'notify users its friends, the community and its members' do 75 + should 'notify users its followers, the community and its members' do
70 person = fast_create(Person) 76 person = fast_create(Person)
71 community = fast_create(Community) 77 community = fast_create(Community)
72 action_tracker = fast_create(ActionTracker::Record, :user_type => 'Profile', :user_id => person.id, :target_type => 'Profile', :target_id => community.id, :verb => 'create_article') 78 action_tracker = fast_create(ActionTracker::Record, :user_type => 'Profile', :user_id => person.id, :target_type => 'Profile', :target_id => community.id, :verb => 'create_article')
73 refute NotifyActivityToProfilesJob::NOTIFY_ONLY_COMMUNITY.include?(action_tracker.verb) 79 refute NotifyActivityToProfilesJob::NOTIFY_ONLY_COMMUNITY.include?(action_tracker.verb)
74 p1, p2, m1, m2 = fast_create(Person), fast_create(Person), fast_create(Person), fast_create(Person) 80 p1, p2, m1, m2 = fast_create(Person), fast_create(Person), fast_create(Person), fast_create(Person)
75 - fast_create(Friendship, :person_id => person.id, :friend_id => p1.id)  
76 - fast_create(Friendship, :person_id => person.id, :friend_id => p2.id) 81 +
  82 + circle1 = Circle.create!(:person=> p1, :name => "Zombies", :profile_type => 'Person')
  83 + fast_create(ProfileFollower, :profile_id => person.id, :circle_id => circle1.id)
  84 +
77 fast_create(RoleAssignment, :accessor_id => m1.id, :role_id => 3, :resource_id => community.id) 85 fast_create(RoleAssignment, :accessor_id => m1.id, :role_id => 3, :resource_id => community.id)
78 fast_create(RoleAssignment, :accessor_id => m2.id, :role_id => 3, :resource_id => community.id) 86 fast_create(RoleAssignment, :accessor_id => m2.id, :role_id => 3, :resource_id => community.id)
79 ActionTrackerNotification.delete_all 87 ActionTrackerNotification.delete_all
80 job = NotifyActivityToProfilesJob.new(action_tracker.id) 88 job = NotifyActivityToProfilesJob.new(action_tracker.id)
81 job.perform 89 job.perform
82 process_delayed_job_queue 90 process_delayed_job_queue
83 -  
84 - assert_equal 6, ActionTrackerNotification.count  
85 - [person, community, p1, p2, m1, m2].each do |profile| 91 + assert_equal 5, ActionTrackerNotification.count
  92 + [person, community, p1, m1, m2].each do |profile|
86 notification = ActionTrackerNotification.find_by profile_id: profile.id 93 notification = ActionTrackerNotification.find_by profile_id: profile.id
87 assert_equal action_tracker, notification.action_tracker 94 assert_equal action_tracker, notification.action_tracker
88 end 95 end
@@ -119,8 +126,13 @@ class NotifyActivityToProfilesJobTest &lt; ActiveSupport::TestCase @@ -119,8 +126,13 @@ class NotifyActivityToProfilesJobTest &lt; ActiveSupport::TestCase
119 action_tracker = fast_create(ActionTracker::Record, :user_type => 'Profile', :user_id => person.id, :target_type => 'Profile', :target_id => community.id, :verb => 'join_community') 126 action_tracker = fast_create(ActionTracker::Record, :user_type => 'Profile', :user_id => person.id, :target_type => 'Profile', :target_id => community.id, :verb => 'join_community')
120 refute NotifyActivityToProfilesJob::NOTIFY_ONLY_COMMUNITY.include?(action_tracker.verb) 127 refute NotifyActivityToProfilesJob::NOTIFY_ONLY_COMMUNITY.include?(action_tracker.verb)
121 p1, p2, m1, m2 = fast_create(Person), fast_create(Person), fast_create(Person), fast_create(Person) 128 p1, p2, m1, m2 = fast_create(Person), fast_create(Person), fast_create(Person), fast_create(Person)
122 - fast_create(Friendship, :person_id => person.id, :friend_id => p1.id)  
123 - fast_create(Friendship, :person_id => person.id, :friend_id => p2.id) 129 +
  130 + circle1 = Circle.create!(:person=> p1, :name => "Zombies", :profile_type => 'Person')
  131 + circle2 = Circle.create!(:person=> p2, :name => "Zombies", :profile_type => 'Person')
  132 +
  133 + fast_create(ProfileFollower, :profile_id => person.id, :circle_id => circle1.id)
  134 + fast_create(ProfileFollower, :profile_id => person.id, :circle_id => circle2.id)
  135 +
124 fast_create(RoleAssignment, :accessor_id => m1.id, :role_id => 3, :resource_id => community.id) 136 fast_create(RoleAssignment, :accessor_id => m1.id, :role_id => 3, :resource_id => community.id)
125 fast_create(RoleAssignment, :accessor_id => m2.id, :role_id => 3, :resource_id => community.id) 137 fast_create(RoleAssignment, :accessor_id => m2.id, :role_id => 3, :resource_id => community.id)
126 ActionTrackerNotification.delete_all 138 ActionTrackerNotification.delete_all
test/unit/person_notifier_test.rb
@@ -178,6 +178,7 @@ class PersonNotifierTest &lt; ActiveSupport::TestCase @@ -178,6 +178,7 @@ class PersonNotifierTest &lt; ActiveSupport::TestCase
178 update_product: -> { create Product, profile: @profile, product_category: create(ProductCategory, environment: Environment.default) }, 178 update_product: -> { create Product, profile: @profile, product_category: create(ProductCategory, environment: Environment.default) },
179 remove_product: -> { create Product, profile: @profile, product_category: create(ProductCategory, environment: Environment.default) }, 179 remove_product: -> { create Product, profile: @profile, product_category: create(ProductCategory, environment: Environment.default) },
180 favorite_enterprise: -> { create FavoriteEnterprisePerson, enterprise: create(Enterprise), person: @member }, 180 favorite_enterprise: -> { create FavoriteEnterprisePerson, enterprise: create(Enterprise), person: @member },
  181 + new_follower: -> { @member }
181 } 182 }
182 183
183 ActionTrackerConfig.verb_names.each do |verb| 184 ActionTrackerConfig.verb_names.each do |verb|
@@ -197,6 +198,7 @@ class PersonNotifierTest &lt; ActiveSupport::TestCase @@ -197,6 +198,7 @@ class PersonNotifierTest &lt; ActiveSupport::TestCase
197 'friend_url' => '/', 'friend_profile_custom_icon' => [], 'friend_name' => ['joe'], 198 'friend_url' => '/', 'friend_profile_custom_icon' => [], 'friend_name' => ['joe'],
198 'resource_name' => ['resource'], 'resource_profile_custom_icon' => [], 'resource_url' => ['/'], 199 'resource_name' => ['resource'], 'resource_profile_custom_icon' => [], 'resource_url' => ['/'],
199 'enterprise_name' => 'coop', 'enterprise_url' => '/coop', 200 'enterprise_name' => 'coop', 'enterprise_url' => '/coop',
  201 + 'follower_url' => '/', 'follower_profile_custom_icon' => [], 'follower_name' => ['joe'],
200 'view_url'=> ['/'], 'thumbnail_path' => ['1'], 202 'view_url'=> ['/'], 'thumbnail_path' => ['1'],
201 } 203 }
202 a.get_url = '' 204 a.get_url = ''
test/unit/person_test.rb
@@ -728,7 +728,7 @@ class PersonTest &lt; ActiveSupport::TestCase @@ -728,7 +728,7 @@ class PersonTest &lt; ActiveSupport::TestCase
728 assert_equal [s4], p2.scraps_received.not_replies 728 assert_equal [s4], p2.scraps_received.not_replies
729 end 729 end
730 730
731 - should "the followed_by method be protected and true to the person friends and herself by default" do 731 + should "the followed_by method return true to the person friends and herself by default" do
732 p1 = fast_create(Person) 732 p1 = fast_create(Person)
733 p2 = fast_create(Person) 733 p2 = fast_create(Person)
734 p3 = fast_create(Person) 734 p3 = fast_create(Person)
@@ -740,9 +740,9 @@ class PersonTest &lt; ActiveSupport::TestCase @@ -740,9 +740,9 @@ class PersonTest &lt; ActiveSupport::TestCase
740 assert p1.is_a_friend?(p4) 740 assert p1.is_a_friend?(p4)
741 741
742 assert_equal true, p1.send(:followed_by?,p1) 742 assert_equal true, p1.send(:followed_by?,p1)
743 - assert_equal true, p1.send(:followed_by?,p2)  
744 - assert_equal true, p1.send(:followed_by?,p4)  
745 - assert_equal false, p1.send(:followed_by?,p3) 743 + assert_equal true, p2.send(:followed_by?,p1)
  744 + assert_equal true, p4.send(:followed_by?,p1)
  745 + assert_equal false, p3.send(:followed_by?,p1)
746 end 746 end
747 747
748 should "the person follows her friends and herself by default" do 748 should "the person follows her friends and herself by default" do
@@ -757,9 +757,9 @@ class PersonTest &lt; ActiveSupport::TestCase @@ -757,9 +757,9 @@ class PersonTest &lt; ActiveSupport::TestCase
757 assert p4.is_a_friend?(p1) 757 assert p4.is_a_friend?(p1)
758 758
759 assert_equal true, p1.follows?(p1) 759 assert_equal true, p1.follows?(p1)
760 - assert_equal true, p1.follows?(p2)  
761 - assert_equal true, p1.follows?(p4)  
762 - assert_equal false, p1.follows?(p3) 760 + assert_equal true, p2.follows?(p1)
  761 + assert_equal true, p4.follows?(p1)
  762 + assert_equal false, p3.follows?(p1)
763 end 763 end
764 764
765 should "a person member of a community follows the community" do 765 should "a person member of a community follows the community" do
@@ -836,18 +836,21 @@ class PersonTest &lt; ActiveSupport::TestCase @@ -836,18 +836,21 @@ class PersonTest &lt; ActiveSupport::TestCase
836 assert_nil Scrap.find_by(id: scrap.id) 836 assert_nil Scrap.find_by(id: scrap.id)
837 end 837 end
838 838
839 - should "the tracked action be notified to person friends and herself" do 839 + should "the tracked action be notified to person followers and herself" do
840 Person.destroy_all 840 Person.destroy_all
841 p1 = fast_create(Person) 841 p1 = fast_create(Person)
842 p2 = fast_create(Person) 842 p2 = fast_create(Person)
843 p3 = fast_create(Person) 843 p3 = fast_create(Person)
844 p4 = fast_create(Person) 844 p4 = fast_create(Person)
845 845
846 - p1.add_friend(p2)  
847 - assert p1.is_a_friend?(p2)  
848 - refute p1.is_a_friend?(p3)  
849 - p1.add_friend(p4)  
850 - assert p1.is_a_friend?(p4) 846 + circle2 = Circle.create!(:person=> p2, :name => "Zombies", :profile_type => 'Person')
  847 + circle4 = Circle.create!(:person=> p4, :name => "Zombies", :profile_type => 'Person')
  848 +
  849 + p2.follow(p1, circle2)
  850 + assert p2.follows?(p1)
  851 + refute p3.follows?(p1)
  852 + p4.follow(p1, circle4)
  853 + assert p4.follows?(p1)
851 854
852 action_tracker = fast_create(ActionTracker::Record, :user_id => p1.id) 855 action_tracker = fast_create(ActionTracker::Record, :user_id => p1.id)
853 ActionTrackerNotification.delete_all 856 ActionTrackerNotification.delete_all
@@ -880,17 +883,19 @@ class PersonTest &lt; ActiveSupport::TestCase @@ -880,17 +883,19 @@ class PersonTest &lt; ActiveSupport::TestCase
880 end 883 end
881 end 884 end
882 885
883 - should "the tracked action notify friends with one delayed job process" do 886 + should "the tracked action notify followers with one delayed job process" do
884 p1 = fast_create(Person) 887 p1 = fast_create(Person)
885 p2 = fast_create(Person) 888 p2 = fast_create(Person)
886 p3 = fast_create(Person) 889 p3 = fast_create(Person)
887 p4 = fast_create(Person) 890 p4 = fast_create(Person)
888 891
889 - p1.add_friend(p2)  
890 - assert p1.is_a_friend?(p2)  
891 - refute p1.is_a_friend?(p3)  
892 - p1.add_friend(p4)  
893 - assert p1.is_a_friend?(p4) 892 + circle2 = Circle.create!(:person=> p2, :name => "Zombies", :profile_type => 'Person')
  893 + circle4 = Circle.create!(:person=> p4, :name => "Zombies", :profile_type => 'Person')
  894 + p2.follow(p1, circle2)
  895 + assert p2.follows?(p1)
  896 + refute p3.follows?(p1)
  897 + p4.follow(p1, circle4)
  898 + assert p4.follows?(p1)
894 899
895 action_tracker = fast_create(ActionTracker::Record, :user_id => p1.id) 900 action_tracker = fast_create(ActionTracker::Record, :user_id => p1.id)
896 901
@@ -1035,11 +1040,13 @@ class PersonTest &lt; ActiveSupport::TestCase @@ -1035,11 +1040,13 @@ class PersonTest &lt; ActiveSupport::TestCase
1035 p2 = create_user('p2').person 1040 p2 = create_user('p2').person
1036 p3 = create_user('p3').person 1041 p3 = create_user('p3').person
1037 c = fast_create(Community, :name => "Foo") 1042 c = fast_create(Community, :name => "Foo")
  1043 +
1038 c.add_member(p1) 1044 c.add_member(p1)
1039 process_delayed_job_queue 1045 process_delayed_job_queue
1040 c.add_member(p3) 1046 c.add_member(p3)
1041 process_delayed_job_queue 1047 process_delayed_job_queue
1042 - assert_equal 4, ActionTracker::Record.count 1048 +
  1049 + assert_equal 5, ActionTracker::Record.count
1043 assert_equal 5, ActionTrackerNotification.count 1050 assert_equal 5, ActionTrackerNotification.count
1044 has_add_member_notification = false 1051 has_add_member_notification = false
1045 ActionTrackerNotification.all.map do |notification| 1052 ActionTrackerNotification.all.map do |notification|
@@ -1951,4 +1958,51 @@ class PersonTest &lt; ActiveSupport::TestCase @@ -1951,4 +1958,51 @@ class PersonTest &lt; ActiveSupport::TestCase
1951 person.save! 1958 person.save!
1952 end 1959 end
1953 1960
  1961 + should 'update profile circles for a person' do
  1962 + person = create_user('testuser').person
  1963 + community = fast_create(Community)
  1964 + circle = Circle.create!(:person=> person, :name => "Zombies", :profile_type => 'Community')
  1965 + circle2 = Circle.create!(:person=> person, :name => "Dota", :profile_type => 'Community')
  1966 + circle3 = Circle.create!(:person=> person, :name => "Quadrado", :profile_type => 'Community')
  1967 + person.follow(community, [circle, circle2])
  1968 + person.update_profile_circles(community, [circle2, circle3])
  1969 + assert_equivalent [circle2, circle3], ProfileFollower.with_profile(community).with_follower(person).map(&:circle)
  1970 + end
  1971 +
  1972 + should 'a person follow a profile' do
  1973 + person = create_user('testuser').person
  1974 + community = fast_create(Community)
  1975 + circle = Circle.create!(:person=> person, :name => "Zombies", :profile_type => 'Community')
  1976 + person.follow(community, circle)
  1977 + assert_includes person.followed_profiles, community
  1978 + end
  1979 +
  1980 + should 'a person follow a profile with more than one circle' do
  1981 + person = create_user('testuser').person
  1982 + community = fast_create(Community)
  1983 + circle = Circle.create!(:person=> person, :name => "Zombies", :profile_type => 'Community')
  1984 + circle2 = Circle.create!(:person=> person, :name => "Dota", :profile_type => 'Community')
  1985 + person.follow(community, [circle, circle2])
  1986 + assert_includes person.followed_profiles, community
  1987 + assert_equivalent [circle, circle2], ProfileFollower.with_profile(community).with_follower(person).map(&:circle)
  1988 + end
  1989 +
  1990 + should 'a person unfollow a profile' do
  1991 + person = create_user('testuser').person
  1992 + community = fast_create(Community)
  1993 + circle = Circle.create!(:person=> person, :name => "Zombies", :profile_type => 'Community')
  1994 + person.follow(community, circle)
  1995 + person.unfollow(community)
  1996 + assert_not_includes person.followed_profiles, community
  1997 + end
  1998 +
  1999 + should 'a person remove a profile from a circle' do
  2000 + person = create_user('testuser').person
  2001 + community = fast_create(Community)
  2002 + circle = Circle.create!(:person=> person, :name => "Zombies", :profile_type => 'Community')
  2003 + circle2 = Circle.create!(:person=> person, :name => "Dota", :profile_type => 'Community')
  2004 + person.follow(community, [circle, circle2])
  2005 + person.remove_profile_from_circle(community, circle)
  2006 + assert_equivalent [circle2], ProfileFollower.with_profile(community).with_follower(person).map(&:circle)
  2007 + end
1954 end 2008 end
test/unit/profile_followers_test.rb 0 → 100644
@@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
  1 +require_relative "../test_helper"
  2 +
  3 +class ProfileFollowersTest < ActiveSupport::TestCase
  4 +
  5 + should 'a person follow another' do
  6 + p1 = create_user('person_test').person
  7 + p2 = create_user('person_test_2').person
  8 + circle = Circle.create!(:person=> p1, :name => "Zombies", :profile_type => 'Person')
  9 +
  10 + assert_difference 'ProfileFollower.count' do
  11 + p1.follow(p2, circle)
  12 + end
  13 +
  14 + assert_includes p2.followers(true), p1
  15 + assert_not_includes p1.followers(true), p2
  16 + end
  17 +
  18 + should 'a person unfollow another person' do
  19 + p1 = create_user('person_test').person
  20 + p2 = create_user('person_test_2').person
  21 + circle = Circle.create!(:person=> p1, :name => "Zombies", :profile_type => 'Person')
  22 +
  23 + p1.follow(p2,circle)
  24 +
  25 + assert_difference 'ProfileFollower.count', -1 do
  26 + p1.unfollow(p2)
  27 + end
  28 +
  29 + assert_not_includes p2.followers(true), p1
  30 + end
  31 +
  32 + should 'get the followed persons for a profile' do
  33 + p1 = create_user('person_test').person
  34 + p2 = create_user('person_test_2').person
  35 + p3 = create_user('person_test_3').person
  36 + circle = Circle.create!(:person=> p1, :name => "Zombies", :profile_type => 'Person')
  37 +
  38 + p1.follow(p2, circle)
  39 + p1.follow(p3, circle)
  40 +
  41 + assert_equivalent p1.followed_profiles, [p2,p3]
  42 + assert_equivalent Profile.followed_by(p1), [p2,p3]
  43 + end
  44 +
  45 + should 'not follow same person twice' do
  46 + p1 = create_user('person_test').person
  47 + p2 = create_user('person_test_2').person
  48 + circle = Circle.create!(:person=> p1, :name => "Zombies", :profile_type => 'Person')
  49 +
  50 + assert_difference 'ProfileFollower.count' do
  51 + p1.follow(p2, circle)
  52 + p1.follow(p2, circle)
  53 + end
  54 +
  55 + assert_equivalent p1.followed_profiles, [p2]
  56 + assert_equivalent p2.followers, [p1]
  57 + end
  58 +
  59 + should 'show the correct message when a profile is followed by the same person' do
  60 + p1 = create_user('person_test').person
  61 + p2 = create_user('person_test_2').person
  62 + circle = Circle.create!(:person=> p1, :name => "Zombies", :profile_type => 'Person')
  63 +
  64 + p1.follow(p2, circle)
  65 + profile_follower = ProfileFollower.new
  66 + profile_follower.circle = circle
  67 + profile_follower.profile = p2
  68 + profile_follower.valid?
  69 +
  70 + assert_includes profile_follower.errors.messages[:profile_id],
  71 + "can't put a profile in the same circle twice"
  72 + end
  73 +end
test/unit/scrap_test.rb
@@ -125,11 +125,11 @@ class ScrapTest &lt; ActiveSupport::TestCase @@ -125,11 +125,11 @@ class ScrapTest &lt; ActiveSupport::TestCase
125 assert_equal c, ta.target 125 assert_equal c, ta.target
126 end 126 end
127 127
128 - should "notify leave_scrap action tracker verb to friends and itself" do 128 + should "notify leave_scrap action tracker verb to followers and itself" do
129 User.current = create_user 129 User.current = create_user
130 p1 = User.current.person 130 p1 = User.current.person
131 p2 = create_user.person 131 p2 = create_user.person
132 - p1.add_friend(p2) 132 + p2.add_friend(p1)
133 process_delayed_job_queue 133 process_delayed_job_queue
134 s = Scrap.new 134 s = Scrap.new
135 s.sender= p1 135 s.sender= p1
@@ -180,11 +180,11 @@ class ScrapTest &lt; ActiveSupport::TestCase @@ -180,11 +180,11 @@ class ScrapTest &lt; ActiveSupport::TestCase
180 assert_equal p, ta.user 180 assert_equal p, ta.user
181 end 181 end
182 182
183 - should "notify leave_scrap_to_self action tracker verb to friends and itself" do 183 + should "notify leave_scrap_to_self action tracker verb to followers and itself" do
184 User.current = create_user 184 User.current = create_user
185 p1 = User.current.person 185 p1 = User.current.person
186 p2 = create_user.person 186 p2 = create_user.person
187 - p1.add_friend(p2) 187 + p2.add_friend(p1)
188 ActionTrackerNotification.delete_all 188 ActionTrackerNotification.delete_all
189 Delayed::Job.delete_all 189 Delayed::Job.delete_all
190 s = Scrap.new 190 s = Scrap.new