From 8b945809d1d522c39798b10d96ecca9eccd13328 Mon Sep 17 00:00:00 2001 From: Marcos Ronaldo Date: Thu, 9 Jun 2016 11:32:19 -0300 Subject: [PATCH] Profile followers feature --- app/controllers/application_controller.rb | 4 ++++ app/controllers/my_profile/circles_controller.rb | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ app/controllers/my_profile/followers_controller.rb | 43 +++++++++++++++++++++++++++++++++++++++++++ app/controllers/public/content_viewer_controller.rb | 4 ++-- app/controllers/public/profile_controller.rb | 46 +++++++++++++++++++++++++++++++++++++++++++++- app/helpers/action_tracker_helper.rb | 18 ++++++++++++++---- app/helpers/profile_helper.rb | 13 +++++++++++-- app/jobs/notify_activity_to_profiles_job.rb | 4 ++-- app/models/add_member.rb | 1 + app/models/article.rb | 4 ++-- app/models/block.rb | 2 +- app/models/circle.rb | 37 +++++++++++++++++++++++++++++++++++++ app/models/enterprise.rb | 4 ---- app/models/favorite_enterprise_person.rb | 4 ++++ app/models/friendship.rb | 4 ++++ app/models/person.rb | 38 ++++++++++++++++++++++++++++++++++---- app/models/profile.rb | 33 +++++++++++++++++++++++++++++++-- app/models/profile_follower.rb | 28 ++++++++++++++++++++++++++++ app/views/blocks/profile_info_actions/_circles.html.erb | 11 +++++++++++ app/views/blocks/profile_info_actions/_common.html.erb | 9 +++++++++ app/views/blocks/profile_info_actions/_select_circles.html.erb | 24 ++++++++++++++++++++++++ app/views/circles/_circle_checkbox.html.erb | 3 +++ app/views/circles/_form.html.erb | 14 ++++++++++++++ app/views/circles/edit.html.erb | 3 +++ app/views/circles/index.html.erb | 30 ++++++++++++++++++++++++++++++ app/views/circles/new.html.erb | 3 +++ app/views/followers/_edit_circles_modal.html.erb | 14 ++++++++++++++ app/views/followers/_profile_list.html.erb | 16 ++++++++++++++++ app/views/followers/index.html.erb | 27 +++++++++++++++++++++++++++ app/views/person_notifier/mailer/_new_follower.html.erb | 1 + app/views/profile/_follow.html.erb | 18 ++++++++++++++++++ app/views/profile/_new_follower.html.erb | 1 + app/views/profile/followed.html.erb | 7 +++++++ app/views/profile/following.html.erb | 7 +++++++ app/views/profile_editor/edit.html.erb | 6 ++++-- app/views/profile_editor/index.html.erb | 5 +++++ config/initializers/action_tracker.rb | 4 ++++ db/migrate/20160608123748_create_profile_followers_table.rb | 42 ++++++++++++++++++++++++++++++++++++++++++ features/follow_profile.feature | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ features/step_definitions/followers_steps.rb | 35 +++++++++++++++++++++++++++++++++++ public/javascripts/application.js | 7 +++++++ public/javascripts/followers.js | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ public/javascripts/manage-followers.js | 9 +++++++++ public/stylesheets/blocks/profile-info.scss | 41 +++++++++++++++++++++++++++++++++++++++++ public/stylesheets/profile-activity.scss | 2 ++ public/stylesheets/profile-editor.scss | 3 +++ public/stylesheets/profile-list.scss | 49 +++++++++++++++++++++++++++++++++++-------------- public/stylesheets/profile.scss | 5 +++++ test/functional/circles_controller_test.rb | 129 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/functional/followers_controller_test.rb | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/functional/profile_controller_test.rb | 123 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----- test/unit/article_test.rb | 22 +++++++++++++++------- test/unit/friendship_test.rb | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++---- test/unit/notify_activity_to_profiles_job_test.rb | 36 ++++++++++++++++++++++++------------ test/unit/person_notifier_test.rb | 2 ++ test/unit/person_test.rb | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------- test/unit/profile_followers_test.rb | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/unit/scrap_test.rb | 8 ++++---- 58 files changed, 1448 insertions(+), 92 deletions(-) create mode 100644 app/controllers/my_profile/circles_controller.rb create mode 100644 app/controllers/my_profile/followers_controller.rb create mode 100644 app/models/circle.rb create mode 100644 app/models/profile_follower.rb create mode 100644 app/views/blocks/profile_info_actions/_circles.html.erb create mode 100644 app/views/blocks/profile_info_actions/_select_circles.html.erb create mode 100644 app/views/circles/_circle_checkbox.html.erb create mode 100644 app/views/circles/_form.html.erb create mode 100644 app/views/circles/edit.html.erb create mode 100644 app/views/circles/index.html.erb create mode 100644 app/views/circles/new.html.erb create mode 100644 app/views/followers/_edit_circles_modal.html.erb create mode 100644 app/views/followers/_profile_list.html.erb create mode 100644 app/views/followers/index.html.erb create mode 100644 app/views/person_notifier/mailer/_new_follower.html.erb create mode 100644 app/views/profile/_follow.html.erb create mode 100644 app/views/profile/_new_follower.html.erb create mode 100644 app/views/profile/followed.html.erb create mode 100644 app/views/profile/following.html.erb create mode 100644 db/migrate/20160608123748_create_profile_followers_table.rb create mode 100644 features/follow_profile.feature create mode 100644 features/step_definitions/followers_steps.rb create mode 100644 public/javascripts/followers.js create mode 100644 public/javascripts/manage-followers.js create mode 100644 test/functional/circles_controller_test.rb create mode 100644 test/functional/followers_controller_test.rb create mode 100644 test/unit/profile_followers_test.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1e2fda5..b9a51c5 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -115,6 +115,10 @@ class ApplicationController < ActionController::Base protected + def accept_only_post + return render_not_found if !request.post? + end + def verified_request? super || form_authenticity_token == request.headers['X-XSRF-TOKEN'] end diff --git a/app/controllers/my_profile/circles_controller.rb b/app/controllers/my_profile/circles_controller.rb new file mode 100644 index 0000000..ff03259 --- /dev/null +++ b/app/controllers/my_profile/circles_controller.rb @@ -0,0 +1,58 @@ +class CirclesController < MyProfileController + + before_action :accept_only_post, :only => [:create, :update, :destroy] + + def index + @circles = current_person.circles + end + + def new + @circle = Circle.new + end + + def create + @circle = Circle.new(params[:circle].merge({ :person => current_person })) + if @circle.save + redirect_to :action => 'index' + else + render :action => 'new' + end + end + + def xhr_create + if request.xhr? + circle = Circle.new(params[:circle].merge({:person => current_person })) + if circle.save + render :partial => "circle_checkbox", :locals => { :circle => circle }, + :status => 201 + else + render :text => _('The circle could not be saved'), :status => 400 + end + else + render_not_found + end + end + + def edit + @circle = Circle.find_by_id(params[:id]) + render_not_found if @circle.nil? + end + + def update + @circle = Circle.find_by_id(params[:id]) + return render_not_found if @circle.nil? + + if @circle.update(params[:circle]) + redirect_to :action => 'index' + else + render :action => 'edit' + end + end + + def destroy + @circle = Circle.find_by_id(params[:id]) + return render_not_found if @circle.nil? + @circle.destroy + redirect_to :action => 'index' + end +end diff --git a/app/controllers/my_profile/followers_controller.rb b/app/controllers/my_profile/followers_controller.rb new file mode 100644 index 0000000..86b6113 --- /dev/null +++ b/app/controllers/my_profile/followers_controller.rb @@ -0,0 +1,43 @@ +class FollowersController < MyProfileController + + before_action :only_for_person, :only => :index + before_action :accept_only_post, :only => [:update_category] + + def index + @followed_people = current_person.followed_profiles.order(:type) + @profile_types = {_('All profiles') => nil}.merge(Circle.profile_types).to_a + + if params['filter'].present? + @followed_people = @followed_people.where(:type => params['filter']) + @active_filter = params['filter'] + end + + @followed_people = @followed_people.paginate(:per_page => 15, :page => params[:npage]) + end + + def set_category_modal + profile = Profile.find(params[:followed_profile_id]) + circles = Circle.where(:person => current_person, :profile_type => profile.class.name) + render :partial => 'followers/edit_circles_modal', :locals => { :circles => circles, :profile => profile } + end + + def update_category + followed_profile = Profile.find_by(:id => params["followed_profile_id"]) + + selected_circles = params[:circles].map{|circle_name, circle_id| Circle.find_by(:id => circle_id)}.select{|c|not c.nil?} + + if followed_profile + current_person.update_profile_circles(followed_profile, selected_circles) + render :text => _("Circles of %s updated successfully") % followed_profile.name, :status => 200 + else + render :text => _("Error: No profile to follow."), :status => 400 + end + end + + protected + + def only_for_person + render_not_found unless profile.person? + end + +end diff --git a/app/controllers/public/content_viewer_controller.rb b/app/controllers/public/content_viewer_controller.rb index 24b633f..0a4ccfe 100644 --- a/app/controllers/public/content_viewer_controller.rb +++ b/app/controllers/public/content_viewer_controller.rb @@ -128,9 +128,9 @@ class ContentViewerController < ApplicationController end unless @page.display_to?(user) - if !profile.visible? || profile.secret? || (user && user.follows?(profile)) || user.blank? + if !profile.visible? || profile.secret? || (user && profile.in_social_circle?(user)) || user.blank? render_access_denied - else #!profile.public? + else private_profile_partial_parameters render :template => 'profile/_private_profile', :status => 403, :formats => [:html] end diff --git a/app/controllers/public/profile_controller.rb b/app/controllers/public/profile_controller.rb index b28ec69..d1fc0c4 100644 --- a/app/controllers/public/profile_controller.rb +++ b/app/controllers/public/profile_controller.rb @@ -3,7 +3,8 @@ class ProfileController < PublicController needs_profile before_filter :check_access_to_profile, :except => [:join, :join_not_logged, :index, :add] before_filter :store_location, :only => [:join, :join_not_logged, :report_abuse, :send_mail] - 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] + 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] + before_filter :allow_followers?, :only => [:follow, :unfollow] helper TagsHelper helper ActionTrackerHelper @@ -65,6 +66,14 @@ class ProfileController < PublicController end end + def following + @followed_people = profile.followed_profiles.paginate(:per_page => per_page, :page => params[:npage], :total_entries => profile.followed_profiles.count) + end + + def followed + @followed_by = profile.followers.paginate(:per_page => per_page, :page => params[:npage], :total_entries => profile.followers.count) + end + def members if is_cache_expired?(profile.members_cache_key(params)) sort = (params[:sort] == 'desc') ? params[:sort] : 'asc' @@ -151,6 +160,37 @@ class ProfileController < PublicController end end + def follow + if request.post? + if profile.followed_by?(current_person) + render :text => _("You are already following %s.") % profile.name, :status => 400 + else + selected_circles = params[:circles].map{|circle_name, circle_id| Circle.find_by(:id => circle_id)}.select{|c|not c.nil?} + if selected_circles.present? + current_person.follow(profile, selected_circles) + render :text => _("You are now following %s") % profile.name, :status => 200 + else + render :text => _("Select at least one circle to follow %s.") % profile.name, :status => 400 + end + end + else + render_not_found + end + end + + def find_profile_circles + circles = Circle.where(:person => current_person, :profile_type => profile.class.name) + render :partial => 'blocks/profile_info_actions/circles', :locals => { :circles => circles, :profile_types => Circle.profile_types.to_a } + end + + def unfollow + if current_person.follows?(profile) + current_person.unfollow(profile) + end + redirect_url = params["redirect_to"] ? params["redirect_to"] : profile.url + redirect_to redirect_url + end + def check_friendship unless logged_in? render :text => '' @@ -437,4 +477,8 @@ class ProfileController < PublicController [:image, :domains, :preferred_domain, :environment] end + def allow_followers? + render_not_found unless profile.allow_followers? + end + end diff --git a/app/helpers/action_tracker_helper.rb b/app/helpers/action_tracker_helper.rb index 7cfe46f..8e66349 100644 --- a/app/helpers/action_tracker_helper.rb +++ b/app/helpers/action_tracker_helper.rb @@ -14,13 +14,23 @@ module ActionTrackerHelper } end + def new_follower_description ta + n_('has 1 new follower:
%{name}', 'has %{num} new followers:
%{name}', ta.get_follower_name.size).html_safe % { + num: ta.get_follower_name.size, + name: safe_join(ta.collect_group_with_index(:follower_name) do |n,i| + link_to image_tag(ta.get_follower_profile_custom_icon[i] || default_or_themed_icon("/images/icons-app/person-icon.png")), + ta.get_follower_url[i], title: n + end) + } + end + def join_community_description ta n_('has joined 1 community:
%{name}', 'has joined %{num} communities:
%{name}', ta.get_resource_name.size).html_safe % { num: ta.get_resource_name.size, - name: ta.collect_group_with_index(:resource_name) do |n,i| + name: safe_join(ta.collect_group_with_index(:resource_name) do |n,i| link = link_to image_tag(ta.get_resource_profile_custom_icon[i] || default_or_themed_icon("/images/icons-app/community-icon.png")), ta.get_resource_url[i], title: n - end.join.html_safe + end) } end @@ -68,9 +78,9 @@ module ActionTrackerHelper end def favorite_enterprise_description ta - _('favorited enterprise %{title}') % { + (_('favorited enterprise %{title}') % { title: link_to(truncate(ta.get_enterprise_name), ta.get_enterprise_url), - } + }).html_safe end end diff --git a/app/helpers/profile_helper.rb b/app/helpers/profile_helper.rb index fe4ee6f..203abe4 100644 --- a/app/helpers/profile_helper.rb +++ b/app/helpers/profile_helper.rb @@ -11,7 +11,7 @@ module ProfileHelper PERSON_CATEGORIES[:location] = [:address, :address_reference, :zip_code, :city, :state, :district, :country, :nationality] PERSON_CATEGORIES[:work] = [:organization, :organization_website, :professional_activity] PERSON_CATEGORIES[:study] = [:schooling, :formation, :area_of_study] - PERSON_CATEGORIES[:network] = [:friends, :communities, :enterprises] + PERSON_CATEGORIES[:network] = [:friends, :followers, :followed_profiles, :communities, :enterprises] PERSON_CATEGORIES.merge!(COMMON_CATEGORIES) ORGANIZATION_CATEGORIES = {} @@ -42,7 +42,8 @@ module ProfileHelper :created_at => _('Profile created at'), :members_count => _('Members'), :privacy_setting => _('Privacy setting'), - :article_tags => _('Tags') + :article_tags => _('Tags'), + :followed_profiles => _('Following') } EXCEPTION = { @@ -144,6 +145,14 @@ module ProfileHelper link_to(n_('One picture', '%{num} pictures', gallery.images.published.count) % { :num => gallery.images.published.count }, gallery.url) end + def treat_followers(followers) + link_to(profile.followers.count, {:action=>"followed", :controller=>"profile", :profile=>"#{profile.identifier}"}) + end + + def treat_followed_profiles(followed_profiles) + link_to(profile.followed_profiles.count, {:action=>"following", :controller=>"profile", :profile=>"#{profile.identifier}"}) + end + def treat_events(events) link_to events.published.count, :controller => 'events', :action => 'events' end diff --git a/app/jobs/notify_activity_to_profiles_job.rb b/app/jobs/notify_activity_to_profiles_job.rb index b9a5a80..42d6f88 100644 --- a/app/jobs/notify_activity_to_profiles_job.rb +++ b/app/jobs/notify_activity_to_profiles_job.rb @@ -19,8 +19,8 @@ class NotifyActivityToProfilesJob < Struct.new(:tracked_action_id) # Notify the user ActionTrackerNotification.create(:profile_id => tracked_action.user.id, :action_tracker_id => tracked_action.id) - # Notify all friends - 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})") + # Notify all followers + 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}))") if tracked_action.user.is_a? Organization ActionTrackerNotification.connection.execute "insert into action_tracker_notifications(profile_id, action_tracker_id) " + diff --git a/app/models/add_member.rb b/app/models/add_member.rb index 65ccef7..3731c15 100644 --- a/app/models/add_member.rb +++ b/app/models/add_member.rb @@ -22,6 +22,7 @@ class AddMember < Task self.roles = [Profile::Roles.member(organization.environment.id).id] end target.affiliate(requestor, self.roles.select{|r| !r.to_i.zero? }.map{|i| Role.find(i)}) + person.follow(organization, Circle.find_or_create_by(:person => person, :name =>_('memberships'), :profile_type => 'Community')) end def title diff --git a/app/models/article.rb b/app/models/article.rb index a7bbcd8..044ebe1 100644 --- a/app/models/article.rb +++ b/app/models/article.rb @@ -538,13 +538,13 @@ class Article < ApplicationRecord scope :display_filter, lambda {|user, profile| return published if (user.nil? && profile && profile.public?) - return [] if user.nil? || (profile && !profile.public? && !user.follows?(profile)) + return [] if user.nil? || (profile && !profile.public? && !profile.in_social_circle?(user)) where( [ "published = ? OR last_changed_by_id = ? OR profile_id = ? OR ? OR (show_to_followers = ? AND ? AND profile_id IN (?))", true, user.id, user.id, profile.nil? ? false : user.has_permission?(:view_private_content, profile), - true, (profile.nil? ? true : user.follows?(profile)), ( profile.nil? ? (user.friends.select('profiles.id')) : [profile.id]) + true, (profile.nil? ? true : profile.in_social_circle?(user)), ( profile.nil? ? (user.friends.select('profiles.id')) : [profile.id]) ] ) } diff --git a/app/models/block.rb b/app/models/block.rb index f3379c7..47d6618 100644 --- a/app/models/block.rb +++ b/app/models/block.rb @@ -89,7 +89,7 @@ class Block < ApplicationRecord end def display_to_user?(user) - display_user == 'all' || (user.nil? && display_user == 'not_logged') || (user && display_user == 'logged') || (user && display_user == 'followers' && user.follows?(owner)) + display_user == 'all' || (user.nil? && display_user == 'not_logged') || (user && display_user == 'logged') || (user && display_user == 'followers' && owner.in_social_circle?(user)) end def display_always(context) diff --git a/app/models/circle.rb b/app/models/circle.rb new file mode 100644 index 0000000..d5e5478 --- /dev/null +++ b/app/models/circle.rb @@ -0,0 +1,37 @@ +class Circle < ApplicationRecord + has_many :profile_followers + belongs_to :person + + attr_accessible :name, :person, :profile_type + + validates :name, presence: true + validates :person_id, presence: true + validates :profile_type, presence: true + validates :person_id, :uniqueness => {:scope => :name, :message => "can't add two circles with the same name"} + + validate :profile_type_must_be_in_list + + scope :by_owner, -> person{ + where(:person => person) + } + + scope :with_name, -> name{ + where(:name => name) + } + + def self.profile_types + { + _("Person") => Person.name, + _("Community") => Community.name, + _("Enterprise") => Enterprise.name + } + end + + def profile_type_must_be_in_list + valid_profile_types = Circle.profile_types.values + unless self.profile_type.in? valid_profile_types + self.errors.add(:profile_type, "invalid profile type") + end + end + +end diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index de68ab9..76d9500 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -174,8 +174,4 @@ class Enterprise < Organization '' end - def followed_by? person - super or self.fans.where(id: person.id).count > 0 - end - end diff --git a/app/models/favorite_enterprise_person.rb b/app/models/favorite_enterprise_person.rb index 6820f34..7312ff0 100644 --- a/app/models/favorite_enterprise_person.rb +++ b/app/models/favorite_enterprise_person.rb @@ -7,6 +7,10 @@ class FavoriteEnterprisePerson < ApplicationRecord belongs_to :enterprise belongs_to :person + after_create do |favorite| + favorite.person.follow(favorite.enterprise, Circle.find_or_create_by(:person => favorite.person, :name =>_('favorites'), :profile_type => 'Enterprise')) + end + protected def is_trackable? diff --git a/app/models/friendship.rb b/app/models/friendship.rb index c497b1b..cebd883 100644 --- a/app/models/friendship.rb +++ b/app/models/friendship.rb @@ -9,11 +9,15 @@ class Friendship < ApplicationRecord after_create do |friendship| Friendship.update_cache_counter(:friends_count, friendship.person, 1) Friendship.update_cache_counter(:friends_count, friendship.friend, 1) + friendship.person.follow(friendship.friend, Circle.find_or_create_by(:person => friendship.person, :name => (friendship.group.blank? ? 'friendships': friendship.group), :profile_type => 'Person')) end after_destroy do |friendship| Friendship.update_cache_counter(:friends_count, friendship.person, -1) Friendship.update_cache_counter(:friends_count, friendship.friend, -1) + + circle = Circle.find_by(:person => friendship.person, :name => (friendship.group.blank? ? 'friendships': friendship.group) ) + friendship.person.remove_profile_from_circle(friendship.friend, circle) if circle end def self.remove_friendship(person1, person2) diff --git a/app/models/person.rb b/app/models/person.rb index 323e5b6..8f67ac7 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -8,7 +8,6 @@ class Person < Profile :display => %w[compact] } - def self.type_name _('Person') end @@ -93,6 +92,7 @@ class Person < Profile has_many :following_articles, :class_name => 'Article', :through => :article_followers, :source => :article has_many :friendships, :dependent => :destroy has_many :friends, :class_name => 'Person', :through => :friendships + has_many :circles scope :online, -> { 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 < Profile end end + def follow (profile, circles) + circles = [circles] unless circles.is_a?(Array) + circles.each do |new_circle| + ProfileFollower.create(profile: profile, circle: new_circle) + end + end + + def update_profile_circles (profile, new_circles) + profile_circles = ProfileFollower.with_profile(profile).with_follower(self).map(&:circle) + circles_to_add = new_circles - profile_circles + circles_to_remove = profile_circles - new_circles + circles_to_add.each do |new_circle| + ProfileFollower.create(profile: profile, circle: new_circle) + end + + ProfileFollower.where('circle_id IN (?) AND profile_id = ?', + circles_to_remove.map(&:id), profile.id).destroy_all + end + + def unfollow(profile) + ProfileFollower.with_follower(self).with_profile(profile).destroy_all + end + + def remove_profile_from_circle(profile, circle) + ProfileFollower.with_profile(profile).with_circle(circle).destroy_all + end + def already_request_friendship?(person) person.tasks.where(requestor_id: self.id, type: 'AddFriend', status: Task::Status::ACTIVE).first end @@ -580,9 +607,12 @@ class Person < Profile person.has_permission?(:manage_friends, self) end - protected + def followed_profiles + Profile.followed_by self + end - def followed_by?(profile) - self == profile || self.is_a_friend?(profile) + def in_social_circle?(person) + self.is_a_friend?(person) || super end + end diff --git a/app/models/profile.rb b/app/models/profile.rb index 8c59a9f..582bbdf 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -5,7 +5,7 @@ class Profile < ApplicationRecord 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, :redirection_after_login, :custom_url_redirection, - :email_suggestions, :allow_members_to_invite, :invite_friends_only, :secret, :profile_admin_mail_notification + :email_suggestions, :allow_members_to_invite, :invite_friends_only, :secret, :profile_admin_mail_notification, :allow_followers # use for internationalizable human type names in search facets # reimplement on subclasses @@ -206,6 +206,23 @@ class Profile < ApplicationRecord scope :more_active, -> { order 'activities_count DESC' } scope :more_recent, -> { order "created_at DESC" } + scope :followed_by, -> person{ + distinct.select('profiles.*'). + joins('left join profiles_circles ON profiles_circles.profile_id = profiles.id'). + joins('left join circles ON circles.id = profiles_circles.circle_id'). + where('circles.person_id = ?', person.id) + } + + scope :in_circle, -> circle{ + distinct.select('profiles.*'). + joins('left join profiles_circles ON profiles_circles.profile_id = profiles.id'). + joins('left join circles ON circles.id = profiles_circles.circle_id'). + where('circles.id = ?', circle.id) + } + + settings_items :allow_followers, :type => :boolean, :default => true + alias_method :allow_followers?, :allow_followers + acts_as_trackable :dependent => :destroy has_many :profile_activities @@ -218,6 +235,9 @@ class Profile < ApplicationRecord has_many :email_templates, :foreign_key => :owner_id + has_many :profile_followers + has_many :followers, :class_name => 'Person', :through => :profile_followers, :source => :person + # Although this should be a has_one relation, there are no non-silly names for # a foreign key on article to reference the template to which it is # welcome_page... =P @@ -769,6 +789,7 @@ private :generate_url, :url_options else self.affiliate(person, Profile::Roles.admin(environment.id), attributes) if members.count == 0 self.affiliate(person, Profile::Roles.member(environment.id), attributes) + person.follow(self, Circle.find_or_create_by(:person => person, :name =>_('memberships'), :profile_type => 'Community')) end person.tasks.pending.of("InviteMember").select { |t| t.data[:community_id] == self.id }.each { |invite| invite.cancel } remove_from_suggestion_list person @@ -1112,7 +1133,11 @@ private :generate_url, :url_options end def followed_by?(person) - person.is_member_of?(self) + (person == self) || (person.in? self.followers) + end + + def in_social_circle?(person) + (person == self) || (person.is_member_of?(self)) end def display_private_info_to?(user) @@ -1153,4 +1178,8 @@ private :generate_url, :url_options def allow_destroy?(person = nil) person.kind_of?(Profile) && person.has_permission?('destroy_profile', self) end + + def in_circle?(circle, follower) + ProfileFollower.with_follower(follower).with_circle(circle).with_profile(self).present? + end end diff --git a/app/models/profile_follower.rb b/app/models/profile_follower.rb new file mode 100644 index 0000000..4bf1698 --- /dev/null +++ b/app/models/profile_follower.rb @@ -0,0 +1,28 @@ +class ProfileFollower < ApplicationRecord + self.table_name = :profiles_circles + track_actions :new_follower, :after_create, :keep_params => ["follower.name", "follower.url", "follower.profile_custom_icon"], :custom_user => :profile + + attr_accessible :profile, :circle + + belongs_to :profile + belongs_to :circle + + has_one :person, through: :circle + alias follower person + + validates_presence_of :profile_id, :circle_id + validates :profile_id, :uniqueness => {:scope => :circle_id, :message => "can't put a profile in the same circle twice"} + + scope :with_follower, -> person{ + joins(:circle).where('circles.person_id = ?', person.id) + } + + scope :with_profile, -> profile{ + where(:profile => profile) + } + + scope :with_circle, -> circle{ + where(:circle => circle) + } + +end diff --git a/app/views/blocks/profile_info_actions/_circles.html.erb b/app/views/blocks/profile_info_actions/_circles.html.erb new file mode 100644 index 0000000..6f66c7a --- /dev/null +++ b/app/views/blocks/profile_info_actions/_circles.html.erb @@ -0,0 +1,11 @@ +
+ + <%= form_for :circles, :url => {:controller => 'profile', :action => 'follow'}, :html => {:id => "follow-circles-form"} do |f|%> + <%= render partial: "blocks/profile_info_actions/select_circles", :locals => {:circles => circles} %> + +
+ <%= submit_button :ok, _("Follow") %> + " id="cancel-set-circle" class="button with-text icon-cancel"/> +
+ <% end %> +
diff --git a/app/views/blocks/profile_info_actions/_common.html.erb b/app/views/blocks/profile_info_actions/_common.html.erb index 8596139..691a3dd 100644 --- a/app/views/blocks/profile_info_actions/_common.html.erb +++ b/app/views/blocks/profile_info_actions/_common.html.erb @@ -1,2 +1,11 @@
  • <%= report_abuse(profile, :button) %>
  • +<%if logged_in? && (user != profile) && profile.allow_followers?%> +
  • + <% follow = user.follows?(profile) %> + <%= button(:unfollow, content_tag('span', _('Unfollow')), {:profile => profile.identifier, :controller => 'profile', :action => 'unfollow'}, :id => 'action-unfollow', :title => _("Unfollow"), :style => follow ? "" : "display: none;") %> + <%= button(:ok, content_tag('span', _('Follow')), {:profile => profile.identifier, :controller => 'profile', :action => 'find_profile_circles'}, :id => 'action-follow', :title => _("Follow"), :style => follow ? "display: none;" : "") %> + +
  • +<%end%> <%= render_environment_features(:profile_actions) %> diff --git a/app/views/blocks/profile_info_actions/_select_circles.html.erb b/app/views/blocks/profile_info_actions/_select_circles.html.erb new file mode 100644 index 0000000..d17ff72 --- /dev/null +++ b/app/views/blocks/profile_info_actions/_select_circles.html.erb @@ -0,0 +1,24 @@ +
    +

    <%= _("Select the circles for %s") % profile.name %>

    +
    + <% circles.each do |circle| %> +
    + <%= labelled_check_box circle.name, "circles[#{circle.name}]", circle.id, profile.in_circle?(circle, current_person) %> +
    + <% end %> +
    + + + <%= _("New Circle") %> + + + +
    diff --git a/app/views/circles/_circle_checkbox.html.erb b/app/views/circles/_circle_checkbox.html.erb new file mode 100644 index 0000000..1204a11 --- /dev/null +++ b/app/views/circles/_circle_checkbox.html.erb @@ -0,0 +1,3 @@ +
    + <%= labelled_check_box circle.name, "circles[#{circle.name}]", circle.id %> +
    diff --git a/app/views/circles/_form.html.erb b/app/views/circles/_form.html.erb new file mode 100644 index 0000000..d812ce5 --- /dev/null +++ b/app/views/circles/_form.html.erb @@ -0,0 +1,14 @@ +<%= error_messages_for :circle %> + +<%= labelled_form_for :circle, :url => (mode == :edit) ? {:action => 'update', :id => circle} : {:action => 'create'} do |f| %> + + <%= required_fields_message %> + + <%= required f.text_field(:name) %> + + <%= required labelled_form_field _("Profile type"), f.select(:profile_type, Circle.profile_types.to_a) %> + + <%= button_bar do %> + <%= submit_button('save', (mode == :edit) ? _('Save changes') : _('Create circle'), :cancel => {:action => 'index'} ) %> + <% end %> +<% end %> diff --git a/app/views/circles/edit.html.erb b/app/views/circles/edit.html.erb new file mode 100644 index 0000000..d05c587 --- /dev/null +++ b/app/views/circles/edit.html.erb @@ -0,0 +1,3 @@ +

    <%= _("Edit circle") %>

    + +<%= render :partial => 'form', :locals => { :mode => :edit, :circle => @circle, :profile_types => @profile_types } %> diff --git a/app/views/circles/index.html.erb b/app/views/circles/index.html.erb new file mode 100644 index 0000000..aeb6da4 --- /dev/null +++ b/app/views/circles/index.html.erb @@ -0,0 +1,30 @@ +

    <%= _('Manage circles') %>

    + + + + + + + + <% @circles.each do |circle| %> + + + + + + <% end %> +
    <%= _('Circle name') %><%= _('Profile type') %><%= _('Actions') %>
    + <%= circle.name %> + + <%= _(circle.profile_type) %> + +
    + <%= button_without_text :edit, _('Edit'), :action => 'edit', :id => circle %> + <%= button_without_text :delete, _('Delete'), { :action => 'destroy', :id => circle }, { "data-method" => "POST" } %> +
    +
    + +<%= button_bar do %> + <%= button :add, _('Create a new circle'), :action => 'new' %> + <%= button :back, _('Back to control panel'), :controller => 'profile_editor' %> +<% end %> diff --git a/app/views/circles/new.html.erb b/app/views/circles/new.html.erb new file mode 100644 index 0000000..5c6b4ba --- /dev/null +++ b/app/views/circles/new.html.erb @@ -0,0 +1,3 @@ +

    <%= _("New circle") %>

    + +<%= render :partial => 'form', :locals => { :mode => :new, :circle => @circle, :profile_types => @profile_types } %> diff --git a/app/views/followers/_edit_circles_modal.html.erb b/app/views/followers/_edit_circles_modal.html.erb new file mode 100644 index 0000000..0b73eb1 --- /dev/null +++ b/app/views/followers/_edit_circles_modal.html.erb @@ -0,0 +1,14 @@ +
    + <%= form_for :circles, :url => {:controller => 'followers', :action => 'update_category'}, :html => {:id => "follow-circles-form"} do |f|%> + <%= render partial: "blocks/profile_info_actions/select_circles", :locals => {:circles => circles, :profile => profile} %> + + <%= hidden_field_tag('followed_profile_id', profile.id) %> + +
    +
    + <%= submit_button('save', _('Save')) %> + <%= modal_close_button _("Cancel") %> +
    +
    + <% end %> +
    diff --git a/app/views/followers/_profile_list.html.erb b/app/views/followers/_profile_list.html.erb new file mode 100644 index 0000000..9fb2794 --- /dev/null +++ b/app/views/followers/_profile_list.html.erb @@ -0,0 +1,16 @@ + diff --git a/app/views/followers/index.html.erb b/app/views/followers/index.html.erb new file mode 100644 index 0000000..d2a5561 --- /dev/null +++ b/app/views/followers/index.html.erb @@ -0,0 +1,27 @@ +
    + +

    <%= _("%s following") % profile.name %>

    + +<% cache_timeout(profile.manage_friends_cache_key(params), 4.hours) do %> + <% if @followed_people.empty? %> +

    + + <%= _("You don't follow anybody yet.") %> + +

    + <% end %> + + <%= button_bar do %> + <%= button(:back, _('Back to control panel'), :controller => 'profile_editor') %> + <%= button(:search, _('Find people'), :controller => 'search', :action => 'assets', :asset => 'people') %> + <% end %> + + <%= labelled_select(_('Profile type')+': ', :filter_profile_type, :last, :first, @active_filter, @profile_types, :id => "profile-type-filter") %> + + <%= render :partial => 'profile_list', :locals => { :profiles => @followed_people } %> + +
    + <%= pagination_links @followed_people, :param_name => 'npage' %> +<% end %> + +
    diff --git a/app/views/person_notifier/mailer/_new_follower.html.erb b/app/views/person_notifier/mailer/_new_follower.html.erb new file mode 100644 index 0000000..ce1d787 --- /dev/null +++ b/app/views/person_notifier/mailer/_new_follower.html.erb @@ -0,0 +1 @@ +<%= render :partial => 'default_activity', :locals => { :activity => activity } %> diff --git a/app/views/profile/_follow.html.erb b/app/views/profile/_follow.html.erb new file mode 100644 index 0000000..c0c5fe3 --- /dev/null +++ b/app/views/profile/_follow.html.erb @@ -0,0 +1,18 @@ +<% cache_timeout(profile.friends_cache_key(params), 4.hours) do %> + + +
    + <%= pagination_links follow, :param_name => 'npage' %> +
    +<% end %> + +<%= button_bar do %> + <%= button :back, _('Go back'), { :controller => 'profile' } %> + <% if user == profile %> + <%= button :edit, _('Manage followed people'), :controller => 'friends', :action => 'index', :profile => profile.identifier %> + <% end %> +<% end %> diff --git a/app/views/profile/_new_follower.html.erb b/app/views/profile/_new_follower.html.erb new file mode 100644 index 0000000..3cc3227 --- /dev/null +++ b/app/views/profile/_new_follower.html.erb @@ -0,0 +1 @@ +<%= render :partial => 'default_activity', :locals => { :activity => activity, :tab_action => tab_action } %> diff --git a/app/views/profile/followed.html.erb b/app/views/profile/followed.html.erb new file mode 100644 index 0000000..90a6fa2 --- /dev/null +++ b/app/views/profile/followed.html.erb @@ -0,0 +1,7 @@ +
    + +

    <%= _("%s is followed by") % profile.name %>

    + +<%= render :partial => 'follow', :locals => {:follow => @followed_by} %> + +
    diff --git a/app/views/profile/following.html.erb b/app/views/profile/following.html.erb new file mode 100644 index 0000000..c167339 --- /dev/null +++ b/app/views/profile/following.html.erb @@ -0,0 +1,7 @@ +
    + +

    <%= _("%s is following") % profile.name %>

    + +<%= render :partial => 'follow', :locals => {:follow => @followed_people} %> + +
    diff --git a/app/views/profile_editor/edit.html.erb b/app/views/profile_editor/edit.html.erb index 8d26b96..7fce287 100644 --- a/app/views/profile_editor/edit.html.erb +++ b/app/views/profile_editor/edit.html.erb @@ -23,9 +23,11 @@

    <%= _('Privacy options') %>

    - +
    + <%= labelled_check_box _("Allow other users to follow me"), 'profile_data[allow_followers]', true, @profile.allow_followers?, :class => "person-can-be-followed" %> +
    <% if profile.person? %> -
    +
    <%= labelled_radio_button _('Public — show my contents to all internet users').html_safe, 'profile_data[public_profile]', true, @profile.public_profile? %>
    diff --git a/app/views/profile_editor/index.html.erb b/app/views/profile_editor/index.html.erb index 9c99687..17378f9 100644 --- a/app/views/profile_editor/index.html.erb +++ b/app/views/profile_editor/index.html.erb @@ -72,6 +72,11 @@ <%= control_panel_button(_('Email Templates'), 'email-templates', :controller => :profile_email_templates) if profile.organization? %> + <% if profile.person? %> + <%= control_panel_button(_('Manage followed profiles'), 'edit-profile', :controller => :followers) %> + <%= control_panel_button(_('Manage circles'), 'edit-profile-group', :controller => :circles) %> + <% end %> + <% @plugins.dispatch(:control_panel_buttons).each do |button| %> <%= control_panel_button(button[:title], button[:icon], button[:url], button[:html_options]) %> <% end %> diff --git a/config/initializers/action_tracker.rb b/config/initializers/action_tracker.rb index 7080775..9bfcfa3 100644 --- a/config/initializers/action_tracker.rb +++ b/config/initializers/action_tracker.rb @@ -12,6 +12,10 @@ ActionTrackerConfig.verbs = { type: :groupable }, + new_follower: { + type: :groupable + }, + join_community: { type: :groupable }, diff --git a/db/migrate/20160608123748_create_profile_followers_table.rb b/db/migrate/20160608123748_create_profile_followers_table.rb new file mode 100644 index 0000000..94d5332 --- /dev/null +++ b/db/migrate/20160608123748_create_profile_followers_table.rb @@ -0,0 +1,42 @@ +class CreateProfileFollowersTable < ActiveRecord::Migration + def up + create_table :profiles_circles do |t| + t.column :profile_id, :integer + t.column :circle_id, :integer + t.timestamps + end + + create_table :circles do |t| + t.column :name, :string + t.belongs_to :person + t.column :profile_type, :string, :null => false + end + + add_foreign_key :profiles_circles, :circles, :on_delete => :nullify + + add_index :profiles_circles, [:profile_id, :circle_id], :name => "profiles_circles_composite_key_index", :unique => true + add_index :circles, [:person_id, :name], :name => "circles_composite_key_index", :unique => true + + #insert one category for each friend group a person has + 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") + #insert 'memberships' category if a person is in a community as a member, moderator or profile admin + 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')") + #insert 'favorites' category if a person has any favorited enterprise + execute("INSERT INTO circles(name, person_id, profile_type) SELECT DISTINCT 'favorites', person_id, 'Enterprise' FROM favorite_enterprise_people") + + #insert a follower entry for each friend, with the category the same as the friendship group or equals 'friendships' + 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'") + #insert a follower entry for each favorited enterprise, with the category 'favorites' + 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' ") + #insert a follower entry for each community a person participates as a member, moderator or admininstrator + 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'") + end + + def down + remove_foreign_key :profiles_circles, :circles + remove_index :profiles_circles, :name => "profiles_circles_composite_key_index" + remove_index :circles, :name => "circles_composite_key_index" + drop_table :circles + drop_table :profiles_circles + end +end diff --git a/features/follow_profile.feature b/features/follow_profile.feature new file mode 100644 index 0000000..b9f420e --- /dev/null +++ b/features/follow_profile.feature @@ -0,0 +1,114 @@ +Feature: follow profile + As a noosfero user + I want to follow a profile + So I can receive notifications from it + + Background: + Given the following community + | identifier | name | + | nightswatch | Nights Watch | + And the following users + | login | + | johnsnow | + And the user "johnsnow" has the following circles + | name | profile_type | + | Family | Person | + | Work | Community | + | Favorites | Community | + + @selenium + Scenario: Common noofero user follow a community + Given I am logged in as "johnsnow" + When I go to nightswatch's homepage + When I follow "Follow" + When I check "Work" + When I press "Follow" + And I wait 1 second + Then "johnsnow" should be a follower of "nightswatch" in circle "Work" + + @selenium + Scenario: Common noofero user follow a community in more than one circle + Given I am logged in as "johnsnow" + When I go to nightswatch's homepage + When I follow "Follow" + When I check "Work" + When I check "Favorites" + When I press "Follow" + And I wait 1 second + Then "johnsnow" should be a follower of "nightswatch" in circle "Work" + And "johnsnow" should be a follower of "nightswatch" in circle "Favorites" + + @selenium + Scenario: No see another profile type circle when following a community + Given I am logged in as "johnsnow" + When I go to nightswatch's homepage + When I follow "Follow" + Then I should not see "Family" + And I should see "Favorites" + And I should see "Work" + + @selenium + Scenario: Common noofero user follow a community then cancel the action + Given I am logged in as "johnsnow" + When I go to nightswatch's homepage + When I follow "Follow" + When I press "Cancel" + And I wait 1 second + Then I should not see "Family" + And I should not see "Favorites" + And I should not see "Work" + And I should not see "New Circle" + Then "johnsnow" should not be a follower of "nightswatch" + + @selenium + Scenario: Common noofero user cancel the circle creation action + Given I am logged in as "johnsnow" + When I go to nightswatch's homepage + When I follow "Follow" + When I follow "New Circle" + When I press "Cancel" + And I wait 1 second + Then I should not see "Circle name" + And I should not see "Create" + + @selenium + Scenario: Noosfero user see new circle option when following a community + Given I am logged in as "johnsnow" + When I go to nightswatch's homepage + When I follow "Follow" + Then I should see "New Circle" + + @selenium + Scenario: Common noofero user follow a community with a new circle + Given I am logged in as "johnsnow" + When I go to nightswatch's homepage + When I follow "Follow" + When I follow "New Circle" + And I fill in "text-field-name-new-circle" with "Winterfell" + When I follow "Create" + When I check "Winterfell" + When I press "Follow" + And I wait 1 second + Then "johnsnow" should be a follower of "nightswatch" in circle "Winterfell" + + @selenium + Scenario: Common noofero user create a new circle when following a community + Given I am logged in as "johnsnow" + When I go to nightswatch's homepage + When I follow "Follow" + When I follow "New Circle" + And I fill in "text-field-name-new-circle" with "Winterfell" + When I follow "Create" + And I wait 1 second + Then "johnsnow" should have the circle "Winterfell" with profile type "Community" + Then I should not see "Circle name" + Then I should not see "Create" + + @selenium + Scenario: Common noofero user unfollow a community + Given "johnsnow" is a follower of "nightswatch" in circle "Work" + And I am logged in as "johnsnow" + When I go to nightswatch's homepage + When I follow "Unfollow" + Then "johnsnow" should not be a follower of "nightswatch" + diff --git a/features/step_definitions/followers_steps.rb b/features/step_definitions/followers_steps.rb new file mode 100644 index 0000000..86aef1f --- /dev/null +++ b/features/step_definitions/followers_steps.rb @@ -0,0 +1,35 @@ +Given /^the user "(.+)" has the following circles$/ do |user_name,table| + person = User.find_by(:login => user_name).person + table.hashes.each do |circle| + Circle.create!(:person => person, :name => circle[:name], :profile_type => circle[:profile_type]) + end +end + +Then /^"(.+)" should be a follower of "(.+)" in circle "(.+)"$/ do |person, profile, circle| + profile = Profile.find_by(identifier: profile) + followers = profile.followers + person = Person.find_by(identifier: person) + followers.should include(person) + + circle = Circle.find_by(:name => circle, :person => person) + ProfileFollower.find_by(:circle => circle, :profile => profile).should_not == nil +end + +Then /^"(.+)" should not be a follower of "(.+)"$/ do |person, profile| + profile = Profile.find_by(identifier: profile) + followers = profile.followers + person = Person.find_by(identifier: person) + followers.should_not include(person) +end + +Given /^"(.+)" is a follower of "(.+)" in circle "(.+)"$/ do |person, profile, circle| + profile = Profile.find_by(identifier: profile) + person = Person.find_by(identifier: person) + circle = Circle.find_by(:name => circle, :person => person) + ProfileFollower.create!(:circle => circle, :profile => profile) +end + +Then /^"(.+)" should have the circle "(.+)" with profile type "(.+)"$/ do |user_name, circle, profile_type| + person = User.find_by(:login => user_name).person + Circle.find_by(:name => circle, :person => person, :profile_type => profile_type).should_not == nil +end diff --git a/public/javascripts/application.js b/public/javascripts/application.js index fff7e9b..d9c7db4 100644 --- a/public/javascripts/application.js +++ b/public/javascripts/application.js @@ -26,6 +26,8 @@ *= require pagination.js * views speficics *= require add-and-join.js +*= require followers.js +*= require manage-followers.js *= require report-abuse.js *= require autogrow.js *= require require_login.js @@ -550,6 +552,11 @@ function loading_for_button(selector) { jQuery(selector).css('cursor', 'progress'); } +function hide_loading_for_button(selector) { + selector.css("cursor",""); + $(".small-loading").remove(); +} + function new_qualifier_row(selector, select_qualifiers, delete_button) { index = jQuery(selector + ' tr').size() - 1; jQuery(selector).append("" + select_qualifiers + "" + delete_button + ""); diff --git a/public/javascripts/followers.js b/public/javascripts/followers.js new file mode 100644 index 0000000..fb69c35 --- /dev/null +++ b/public/javascripts/followers.js @@ -0,0 +1,81 @@ +$("#action-follow").live("click", function() { + var button = $(this); + var url = button.attr("href"); + loading_for_button(button); + + $.post(url, function(data) { + button.fadeOut("fast", function() { + $("#circles-container").html(data); + $("#circles-container").fadeIn(); + }); + }).always(function() { + hide_loading_for_button(button); + }); + return false; +}); + +$("#cancel-set-circle").live("click", function() { + $("#circles-container").fadeOut("fast", function() { + $("#action-follow").fadeIn(); + }); + return false; +}); + +$("#new-circle").live("click", function() { + $(this).fadeOut(); + $("#circle-actions").fadeOut("fast", function() { + $("#new-circle-form").fadeIn(); + }); + return false; +}); + +$("#new-circle-cancel").live("click", function() { + $("#new-circle-form").fadeOut("fast", function() { + $("#circle-actions").fadeIn(); + $("#new-circle").fadeIn(); + $("#text-field-name-new-circle").val('') + }); + return false; +}); + +$('#follow-circles-form').live("submit", function() { + var valuesToSubmit = $(this).serialize(); + $.ajax({ + type: "POST", + url: $(this).attr('action'), + data: valuesToSubmit, + dataType: "JSON", + statusCode: { + 200: function(response){ + $("#circles-container").fadeOut(); + $("#action-unfollow").fadeIn(); + $.colorbox.close(); + display_notice(response.responseText); + }, + 400: function(response) { + display_notice(response.responseText); + } + } + }) + return false; +}); + +$("#new-circle-submit").live("click", function() { + $.ajax({ + method: 'POST', + url: $(this).attr("href"), + data: {'circle[name]': $("#text-field-name-new-circle").val(), + 'circle[profile_type]': $("#circle_profile_type").val()}, + success: function(response) { + $('#circles-checkboxes').append(response); + }, + error: function(response) { + display_notice(response.responseText); + }, + complete: function(response) { + $("#text-field-name-new-circle").val('') + $("#new-circle-cancel").trigger("click"); + } + }) + return false; +}); diff --git a/public/javascripts/manage-followers.js b/public/javascripts/manage-followers.js new file mode 100644 index 0000000..f01e72f --- /dev/null +++ b/public/javascripts/manage-followers.js @@ -0,0 +1,9 @@ +$('#profile-type-filter').live('change', function() { + var filter_type = $(this).val(); + $(".profile-list").addClass("fetching"); + $.get(window.location.pathname, {filter: filter_type}, function(data) { + $(".main-content").html(data); + }).fail(function(data) { + $(".profile-list").removeClass("fetching"); + }); +}); diff --git a/public/stylesheets/blocks/profile-info.scss b/public/stylesheets/blocks/profile-info.scss index 80fd7f0..1ff89ca 100644 --- a/public/stylesheets/blocks/profile-info.scss +++ b/public/stylesheets/blocks/profile-info.scss @@ -99,3 +99,44 @@ margin: 0px 0px 5px 0px; padding: 2px; } +#circles-container { + background-color: #eee; + padding: 5px; + display: flex; +} +#circles-container p { + font-size: 12px; + margin-bottom: 5px; +} +#circle-actions { + margin-top: 15px; +} +#new-category-field-actions-block { + float: left; + width: 80%; + margin-bottom: 10px; +} +#new-circle-form { + margin-top: 10px; +} +#new-circle-form input { + width: 90px; +} +#new-circle-form select { + margin-top: 2px; + width: 95px; +} +#new-circle-form label { + font-size: 10px; + margin-right: 5px; +} +#new-circle-form .button-bar { + padding-top: 0px; +} +#new-circle-form .button { + width: 60px; +} +#new-circle-form .button-bar .button { + width: 40px; + font-size: 10px; +} diff --git a/public/stylesheets/profile-activity.scss b/public/stylesheets/profile-activity.scss index 91848a1..6163fe1 100644 --- a/public/stylesheets/profile-activity.scss +++ b/public/stylesheets/profile-activity.scss @@ -167,7 +167,9 @@ li.profile-activity-item.upload_image .activity-gallery-images-count-1 img { #profile-wall li.profile-activity-item.join_community .profile-activity-text a img, #profile-wall li.profile-activity-item.new_friendship .profile-activity-text a img, +#profile-wall li.profile-activity-item.new_follower .profile-activity-text a img, #profile-network li.profile-activity-item.join_community .profile-activity-text a img, +#profile-network li.profile-activity-item.new_follower .profile-activity-text a img, #profile-network li.profile-activity-item.new_friendship .profile-activity-text a img { margin: 5px 5px 0 0; padding: 1px; diff --git a/public/stylesheets/profile-editor.scss b/public/stylesheets/profile-editor.scss index d0ac0b9..ea49c89 100644 --- a/public/stylesheets/profile-editor.scss +++ b/public/stylesheets/profile-editor.scss @@ -263,3 +263,6 @@ -webkit-border-radius: 5px; } +#profile_allow_follows { + margin-top: 10px; +} diff --git a/public/stylesheets/profile-list.scss b/public/stylesheets/profile-list.scss index cbd9bc1..3458bb9 100644 --- a/public/stylesheets/profile-list.scss +++ b/public/stylesheets/profile-list.scss @@ -23,6 +23,7 @@ } .controller-favorite_enterprises .profile-list a.profile-link, .controller-friends .profile-list a.profile-link, +.controller-followers .profile-list a.profile-link, .list-profile-connections .profile-list a.profile-link, .profiles-suggestions .profile-list a.profile-link { text-decoration: none; @@ -32,11 +33,13 @@ } .controller-favorite_enterprises .profile-list a.profile-link:hover, .controller-friends .profile-list a.profile-link:hover, +.controller-followers .profile-list a.profile-link:hover, .profiles-suggestions .profile-list a.profile-link:hover { color: #FFF; } .controller-favorite_enterprises .profile-list .profile_link span, .controller-friends .profile-list .profile_link span, +.controller-followers .profile-list .profile_link span, .box-1 .profiles-suggestions .profile-list .profile_link span { width: 80px; display: block; @@ -44,12 +47,14 @@ } .controller-favorite_enterprises .profile-list, .controller-friends .profile-list, +.controller-followers .profile-list, .profiles-suggestions .profile-list { position: relative; } .controller-favorite_enterprises .profile-list .controll, .controller-friends .profile-list .controll, +.controller-followers .profile-list .controll, .profiles-suggestions .profile-list .controll { position: absolute; top: 7px; @@ -57,17 +62,20 @@ } .controller-favorite_enterprises .profile-list .controll a, .controller-friends .profile-list .controll a, +.controller-followers .profile-list .controll a, .profiles-suggestions .profile-list .controll a { display: block; margin-bottom: 2px; } .controller-favorite_enterprises .msie6 .profile-list .controll a, .controller-friends .msie6 .profile-list .controll a, +.controller-folloed_people .msie6 .profile-list .controll a, .profiles-suggestions .msie6 .profile-list .controll a { width: 0px; } .controller-favorite_enterprises .button-bar, .controller-friends .button-bar, +.controller-followers .button-bar, .profiles-suggestions .button-bar { clear: both; padding-top: 20px; @@ -208,22 +216,35 @@ font-size: 12px; } .action-profile-members .profile_link{ - position: relative; + position: relative; } .action-profile-members .profile_link span.new-profile:last-child{ - position: absolute; - top: 3px; - right: 2px; - text-transform: uppercase; - color: #FFF; - font-size: 9px; - background: #66CC33; - padding: 2px; - display: block; - width: 35px; - font-weight: 700; + position: absolute; + top: 3px; + right: 2px; + text-transform: uppercase; + color: #FFF; + font-size: 9px; + background: #66CC33; + padding: 2px; + display: block; + width: 35px; + font-weight: 700; } .action-profile-members .profile_link .fn{ - font-style: normal; - color: #000; + font-style: normal; + color: #000; +} +.category-name { + margin-top: 0px; + margin-bottom: 0px; + font-style: italic; + color: #888a85; + text-align: center; +} +.set-category-modal { + width: 250px; +} +.set-category-modal #actions-container { + margin-top: 20px } diff --git a/public/stylesheets/profile.scss b/public/stylesheets/profile.scss index 8152e2d..3bdcb10 100644 --- a/public/stylesheets/profile.scss +++ b/public/stylesheets/profile.scss @@ -39,3 +39,8 @@ width: 470px; overflow-x: hidden; } + +#circles-checkboxes { + text-align: left; + margin-left: 15%; +} diff --git a/test/functional/circles_controller_test.rb b/test/functional/circles_controller_test.rb new file mode 100644 index 0000000..3bd8f40 --- /dev/null +++ b/test/functional/circles_controller_test.rb @@ -0,0 +1,129 @@ +require_relative "../test_helper" +require 'circles_controller' + +class CirclesControllerTest < ActionController::TestCase + + def setup + @controller = CirclesController.new + @person = create_user('person').person + login_as(@person.identifier) + end + + should 'return all circles of a profile' do + circle1 = Circle.create!(:name => "circle1", :person => @person, :profile_type => 'Person') + circle2 = Circle.create!(:name => "circle2", :person => @person, :profile_type => 'Person') + get :index, :profile => @person.identifier + + assert_equivalent [circle1, circle2], assigns[:circles] + end + + should 'initialize an empty circle for creation' do + get :new, :profile => @person.identifier + assert_nil assigns[:circle].id + assert_nil assigns[:circle].name + end + + should 'create a new circle' do + assert_difference '@person.circles.count' do + post :create, :profile => @person.identifier, + :circle => { :name => 'circle' , :profile_type => Person.name} + end + assert_redirected_to :action => :index + end + + should 'not create a circle without a name' do + assert_no_difference '@person.circles.count' do + post :create, :profile => @person.identifier, :circle => { :name => nil } + end + assert_template :new + end + + should 'retrieve an existing circle when editing' do + circle = Circle.create!(:name => "circle", :person => @person, :profile_type => 'Person') + get :edit, :profile => @person.identifier, :id => circle.id + assert_equal circle.name, assigns[:circle].name + end + + should 'return 404 when editing a circle that does not exist' do + get :edit, :profile => @person.identifier, :id => "nope" + assert_response 404 + end + + should 'update an existing circle' do + circle = Circle.create!(:name => "circle", :person => @person, :profile_type => 'Person') + post :update, :profile => @person.identifier, :id => circle.id, + :circle => { :name => "new name" } + + circle.reload + assert_equal "new name", circle.name + assert_redirected_to :action => :index + end + + should 'not update an existing circle without a name' do + circle = Circle.create!(:name => "circle", :person => @person, :profile_type => 'Person') + post :update, :profile => @person.identifier, :id => circle.id, + :circle => { :name => nil } + + circle.reload + assert_equal "circle", circle.name + assert_template :edit + end + + should 'return 404 when updating a circle that does not exist' do + post :update, :profile => @person.identifier, :id => "nope", :name => "new name" + assert_response 404 + end + + should 'destroy an existing circle and update related profiles' do + circle = Circle.create!(:name => "circle", :person => @person, :profile_type => 'Person') + follower = fast_create(ProfileFollower, :profile_id => fast_create(Person).id, + :circle_id => circle.id) + + assert_difference "@person.circles.count", -1 do + post :destroy, :profile => @person.identifier, :id => circle.id + end + + follower.reload + assert_nil follower.circle + end + + should 'not destroy an existing circle if action is not post' do + circle = Circle.create!(:name => "circle", :person => @person, :profile_type => 'Person') + + assert_no_difference "@person.circles.count" do + get :destroy, :profile => @person.identifier, :id => circle.id + end + assert_response 404 + end + + should 'return 404 when deleting and circle that does not exist' do + get :destroy, :profile => @person.identifier, :id => "nope" + assert_response 404 + end + + should 'return 404 for xhr_create if request is not xhr' do + post :xhr_create, :profile => @person.identifier + assert_response 404 + end + + should 'return 400 if not possible to create circle via xhr' do + xhr :post, :xhr_create, :profile => @person.identifier, + :circle => { :name => 'Invalid Circle' } + assert_response 400 + end + + should 'create a new circle via xhr' do + xhr :post, :xhr_create, :profile => @person.identifier, + :circle => { :name => 'A Brand New Circle', + :profile_type => Person.name } + assert_response 201 + assert_match /A Brand New Circle/, response.body + end + + should 'not create a new circle via xhr with an invalid profile_type' do + xhr :post, :xhr_create, :profile => @person.identifier, + :circle => { :name => 'A Brand New Circle', + :profile_type => '__invalid__' } + assert_response 400 + end +end diff --git a/test/functional/followers_controller_test.rb b/test/functional/followers_controller_test.rb new file mode 100644 index 0000000..fbe0278 --- /dev/null +++ b/test/functional/followers_controller_test.rb @@ -0,0 +1,58 @@ +require_relative "../test_helper" +require 'followers_controller' + +class FollowersControllerTest < ActionController::TestCase + def setup + @profile = create_user('testuser').person + end + + should 'return followed people list' do + login_as(@profile.identifier) + person = fast_create(Person) + circle = Circle.create!(:person=> @profile, :name => "Zombies", :profile_type => 'Person') + fast_create(ProfileFollower, :profile_id => person.id, :circle_id => circle.id) + + get :index, :profile => @profile.identifier + assert_includes assigns(:followed_people), person + end + + should 'return filtered followed people list' do + login_as(@profile.identifier) + person = fast_create(Person) + community = fast_create(Community) + circle = Circle.create!(:person=> @profile, :name => "Zombies", :profile_type => 'Person') + circle2 = Circle.create!(:person=> @profile, :name => "Teams", :profile_type => 'Community') + fast_create(ProfileFollower, :profile_id => person.id, :circle_id => circle.id) + fast_create(ProfileFollower, :profile_id => community.id, :circle_id => circle2.id) + + get :index, :profile => @profile.identifier, :filter => "Community" + assert_equal assigns(:followed_people), [community] + + get :index, :profile => @profile.identifier, :filter => "Person" + assert_equal assigns(:followed_people), [person] + end + + should 'redirect to login page if not logged in' do + get :index, :profile => @profile.identifier + assert_redirected_to :controller => 'account', :action => 'login' + end + + should 'render set category modal' do + login_as(@profile.identifier) + person = fast_create(Person) + get :set_category_modal, :profile => @profile.identifier, :followed_profile_id => person.id + assert_tag :tag => "input", :attributes => { :id => "followed_profile_id", :value => person.id } + end + + should 'update followed person category' do + login_as(@profile.identifier) + person = fast_create(Person) + circle = Circle.create!(:person=> @profile, :name => "Zombies", :profile_type => 'Person') + circle2 = Circle.create!(:person=> @profile, :name => "DotA", :profile_type => 'Person') + fast_create(ProfileFollower, :profile_id => person.id, :circle_id => circle.id) + + post :update_category, :profile => @profile.identifier, :circles => {"DotA"=> circle2.id}, :followed_profile_id => person.id + assert_equivalent ProfileFollower.with_profile(person).with_follower(@profile).map(&:circle), [circle2] + end + +end diff --git a/test/functional/profile_controller_test.rb b/test/functional/profile_controller_test.rb index 57da6e0..808b063 100644 --- a/test/functional/profile_controller_test.rb +++ b/test/functional/profile_controller_test.rb @@ -771,12 +771,15 @@ class ProfileControllerTest < ActionController::TestCase assert_equal 15, assigns(:activities).size end - should 'not see the friends activities in the current profile' do + should 'not see the followers activities in the current profile' do + circle = Circle.create!(:person=> profile, :name => "Zombies", :profile_type => 'Person') + p2 = create_user.person - refute profile.is_a_friend?(p2) + refute profile.follows?(p2) p3 = create_user.person - p3.add_friend(profile) - assert p3.is_a_friend?(profile) + profile.follow(p3, circle) + assert profile.follows?(p3) + ActionTracker::Record.destroy_all scrap1 = create(Scrap, defaults_for_scrap(:sender => p2, :receiver => p3)) @@ -964,7 +967,11 @@ class ProfileControllerTest < ActionController::TestCase should 'have activities defined if logged in and is following profile' do login_as(profile.identifier) p1= fast_create(Person) - p1.add_friend(profile) + + circle = Circle.create!(:person=> profile, :name => "Zombies", :profile_type => 'Person') + + profile.follow(p1, circle) + ActionTracker::Record.destroy_all get :index, :profile => p1.identifier assert_equal [], assigns(:activities) @@ -1932,4 +1939,110 @@ class ProfileControllerTest < ActionController::TestCase assert_redirected_to :controller => 'account', :action => 'login' end + should 'not follow a user without defining a circle' do + login_as(@profile.identifier) + person = fast_create(Person) + assert_no_difference 'ProfileFollower.count' do + post :follow, :profile => person.identifier, :circles => {} + end + end + + should "not follow user if not logged" do + person = fast_create(Person) + get :follow, :profile => person.identifier + + assert_redirected_to :controller => 'account', :action => 'login' + end + + should 'follow a user with a circle' do + login_as(@profile.identifier) + person = fast_create(Person) + + circle = Circle.create!(:person=> @profile, :name => "Zombies", :profile_type => 'Person') + + assert_difference 'ProfileFollower.count' do + post :follow, :profile => person.identifier, :circles => {"Zombies" => circle.id} + end + end + + should 'follow a user with more than one circle' do + login_as(@profile.identifier) + person = fast_create(Person) + + circle = Circle.create!(:person=> @profile, :name => "Zombies", :profile_type => 'Person') + circle2 = Circle.create!(:person=> @profile, :name => "Brainsss", :profile_type => 'Person') + + assert_difference 'ProfileFollower.count', 2 do + post :follow, :profile => person.identifier, :circles => {"Zombies" => circle.id, "Brainsss"=> circle2.id} + end + end + + should 'not follow a user with no circle selected' do + login_as(@profile.identifier) + person = fast_create(Person) + + circle = Circle.create!(:person=> @profile, :name => "Zombies", :profile_type => 'Person') + circle2 = Circle.create!(:person=> @profile, :name => "Brainsss", :profile_type => 'Person') + + assert_no_difference 'ProfileFollower.count' do + post :follow, :profile => person.identifier, :circles => {"Zombies" => "0", "Brainsss" => "0"} + end + + assert_match /Select at least one circle to follow/, response.body + end + + should 'not follow if current_person already follows the person' do + login_as(@profile.identifier) + person = fast_create(Person) + + circle = Circle.create!(:person=> @profile, :name => "Zombies", :profile_type => 'Person') + fast_create(ProfileFollower, :profile_id => person.id, :circle_id => circle.id) + + assert_no_difference 'ProfileFollower.count' do + post :follow, :profile => person.identifier, :follow => { :circles => {"Zombies" => circle.id} } + end + assert_response 400 + end + + should "not unfollow user if not logged" do + person = fast_create(Person) + get :unfollow, :profile => person.identifier + + assert_redirected_to :controller => 'account', :action => 'login' + end + + should "unfollow a followed person" do + login_as(@profile.identifier) + person = fast_create(Person) + + circle = Circle.create!(:person=> @profile, :name => "Zombies", :profile_type => 'Person') + follower = fast_create(ProfileFollower, :profile_id => person.id, :circle_id => circle.id) + + assert_not_nil follower + + get :unfollow, :profile => person.identifier + follower = ProfileFollower.find_by(:profile_id => person.id, :circle_id => circle.id) + assert_nil follower + end + + should "not unfollow a not followed person" do + login_as(@profile.identifier) + person = fast_create(Person) + + assert_no_difference 'ProfileFollower.count' do + get :unfollow, :profile => person.identifier + end + end + + should "redirect to page after unfollow" do + login_as(@profile.identifier) + person = fast_create(Person) + + circle = Circle.create!(:person=> @profile, :name => "Zombies", :profile_type => 'Person') + fast_create(ProfileFollower, :profile_id => person.id, :circle_id => circle.id) + + get :unfollow, :profile => person.identifier, :redirect_to => "/some/url" + assert_redirected_to "/some/url" + end + end diff --git a/test/unit/article_test.rb b/test/unit/article_test.rb index 68cb57f..82a4fdf 100644 --- a/test/unit/article_test.rb +++ b/test/unit/article_test.rb @@ -1099,9 +1099,10 @@ class ArticleTest < ActiveSupport::TestCase assert_equal 3, ActionTrackerNotification.where(action_tracker_id: second_activity.id).count end - should 'create notifications to friends when creating an article' do + should 'create notifications to followers when creating an article' do friend = fast_create(Person) - profile.add_friend(friend) + circle = Circle.create!(:person=> friend, :name => "Zombies", :profile_type => 'Person') + friend.follow(profile, circle) Article.destroy_all ActionTracker::Record.destroy_all ActionTrackerNotification.destroy_all @@ -1112,9 +1113,10 @@ class ArticleTest < ActiveSupport::TestCase assert_equal friend, ActionTrackerNotification.last.profile end - should 'create the notification to the friend when one friend has the notification and the other no' do + should 'create the notification to the follower when one follower has the notification and the other no' do f1 = fast_create(Person) - profile.add_friend(f1) + circle = Circle.create!(:person=> f1, :name => "Zombies", :profile_type => 'Person') + f1.follow(profile, circle) User.current = profile.user article = create TinyMceArticle, :name => 'Tracked Article 1', :profile_id => profile.id @@ -1123,16 +1125,22 @@ class ArticleTest < ActiveSupport::TestCase assert_equal 2, ActionTrackerNotification.where(action_tracker_id: article.activity.id).count f2 = fast_create(Person) - profile.add_friend(f2) + circle2 = Circle.create!(:person=> f2, :name => "Zombies", :profile_type => 'Person') + f2.follow(profile, circle2) + article2 = create TinyMceArticle, :name => 'Tracked Article 2', :profile_id => profile.id assert_equal 2, ActionTracker::Record.where(verb: 'create_article').count process_delayed_job_queue assert_equal 3, ActionTrackerNotification.where(action_tracker_id: article2.activity.id).count end - should 'destroy activity and notifications of friends when destroying an article' do + should 'destroy activity and notifications of followers when destroying an article' do friend = fast_create(Person) - profile.add_friend(friend) + + circle = Circle.create!(:person=> friend, :name => "Zombies", :profile_type => 'Person') + + friend.follow(profile, circle) + Article.destroy_all ActionTracker::Record.destroy_all ActionTrackerNotification.destroy_all diff --git a/test/unit/friendship_test.rb b/test/unit/friendship_test.rb index 8ab5729..05778e8 100644 --- a/test/unit/friendship_test.rb +++ b/test/unit/friendship_test.rb @@ -28,14 +28,14 @@ class FriendshipTest < ActiveSupport::TestCase f.person = a f.friend = b f.save! - ta = ActionTracker::Record.last + ta = ActionTracker::Record.where(:target_type => "Friendship").last assert_equal a, ta.user assert_equal 'b', ta.get_friend_name[0] f = Friendship.new f.person = a f.friend = c f.save! - ta = ActionTracker::Record.last + ta = ActionTracker::Record.where(:target_type => "Friendship").last assert_equal a, ta.user assert_equal 'c', ta.get_friend_name[1] end @@ -46,14 +46,14 @@ class FriendshipTest < ActiveSupport::TestCase f.person = a f.friend = b f.save! - ta = ActionTracker::Record.last + ta = ActionTracker::Record.where(:target_type => "Friendship").last assert_equal a, ta.user assert_equal ['b'], ta.get_friend_name f = Friendship.new f.person = b f.friend = a f.save! - ta = ActionTracker::Record.last + ta = ActionTracker::Record.where(:target_type => "Friendship").last assert_equal b, ta.user assert_equal ['a'], ta.get_friend_name end @@ -72,4 +72,55 @@ class FriendshipTest < ActiveSupport::TestCase assert_not_includes p2.friends(true), p1 end + should 'add follower when adding friend' do + p1 = create_user('testuser1').person + p2 = create_user('testuser2').person + + assert_difference 'ProfileFollower.count', 2 do + p1.add_friend(p2, 'friends') + p2.add_friend(p1, 'friends') + end + + assert_includes p1.followers(true), p2 + assert_includes p2.followers(true), p1 + end + + should 'remove follower when a friend removal occurs' do + p1 = create_user('testuser1').person + p2 = create_user('testuser2').person + + p1.add_friend(p2, 'friends') + p2.add_friend(p1, 'friends') + + Friendship.remove_friendship(p1, p2) + + assert_not_includes p1.followers(true), p2 + assert_not_includes p2.followers(true), p1 + end + + should 'keep friendship intact when stop following' do + p1 = create_user('testuser1').person + p2 = create_user('testuser2').person + + p1.add_friend(p2, 'friends') + p2.add_friend(p1, 'friends') + + p1.unfollow(p2) + + assert_includes p1.friends(true), p2 + assert_includes p2.friends(true), p1 + end + + should 'do not add friendship when start following' do + p1 = create_user('testuser1').person + p2 = create_user('testuser2').person + + circle1 = Circle.create!(:person=> p1, :name => "Zombies", :profile_type => 'Person') + circle2 = Circle.create!(:person=> p2, :name => "Zombies", :profile_type => 'Person') + p1.follow(p2, circle1) + p2.follow(p1, circle2) + + assert_not_includes p1.friends(true), p2 + assert_not_includes p2.friends(true), p1 + end end diff --git a/test/unit/notify_activity_to_profiles_job_test.rb b/test/unit/notify_activity_to_profiles_job_test.rb index 311113d..c0e6ed5 100644 --- a/test/unit/notify_activity_to_profiles_job_test.rb +++ b/test/unit/notify_activity_to_profiles_job_test.rb @@ -24,15 +24,21 @@ class NotifyActivityToProfilesJobTest < ActiveSupport::TestCase end end - should 'notify just the users and his friends tracking user actions' do + should 'notify just the users and his followers tracking user actions' do person = fast_create(Person) community = fast_create(Community) action_tracker = fast_create(ActionTracker::Record, :user_type => 'Profile', :user_id => person.id, :target_type => 'Profile', :verb => 'create_article') refute NotifyActivityToProfilesJob::NOTIFY_ONLY_COMMUNITY.include?(action_tracker.verb) p1, p2, m1, m2 = fast_create(Person), fast_create(Person), fast_create(Person), fast_create(Person) - fast_create(Friendship, :person_id => person.id, :friend_id => p1.id) - fast_create(Friendship, :person_id => person.id, :friend_id => p2.id) - fast_create(Friendship, :person_id => p1.id, :friend_id => m1.id) + + circle1 = Circle.create!(:person=> p1, :name => "Zombies", :profile_type => 'Person') + circle2 = Circle.create!(:person=> p2, :name => "Zombies", :profile_type => 'Person') + circle = Circle.create!(:person=> person, :name => "Zombies", :profile_type => 'Person') + + fast_create(ProfileFollower, :profile_id => person.id, :circle_id => circle1.id) + fast_create(ProfileFollower, :profile_id => person.id, :circle_id => circle2.id) + fast_create(ProfileFollower, :profile_id => m1.id, :circle_id => circle.id) + fast_create(RoleAssignment, :accessor_id => m2.id, :role_id => 3, :resource_id => community.id) ActionTrackerNotification.delete_all job = NotifyActivityToProfilesJob.new(action_tracker.id) @@ -66,23 +72,24 @@ class NotifyActivityToProfilesJobTest < ActiveSupport::TestCase end end - should 'notify users its friends, the community and its members' do + should 'notify users its followers, the community and its members' do person = fast_create(Person) community = fast_create(Community) action_tracker = fast_create(ActionTracker::Record, :user_type => 'Profile', :user_id => person.id, :target_type => 'Profile', :target_id => community.id, :verb => 'create_article') refute NotifyActivityToProfilesJob::NOTIFY_ONLY_COMMUNITY.include?(action_tracker.verb) p1, p2, m1, m2 = fast_create(Person), fast_create(Person), fast_create(Person), fast_create(Person) - fast_create(Friendship, :person_id => person.id, :friend_id => p1.id) - fast_create(Friendship, :person_id => person.id, :friend_id => p2.id) + + circle1 = Circle.create!(:person=> p1, :name => "Zombies", :profile_type => 'Person') + fast_create(ProfileFollower, :profile_id => person.id, :circle_id => circle1.id) + fast_create(RoleAssignment, :accessor_id => m1.id, :role_id => 3, :resource_id => community.id) fast_create(RoleAssignment, :accessor_id => m2.id, :role_id => 3, :resource_id => community.id) ActionTrackerNotification.delete_all job = NotifyActivityToProfilesJob.new(action_tracker.id) job.perform process_delayed_job_queue - - assert_equal 6, ActionTrackerNotification.count - [person, community, p1, p2, m1, m2].each do |profile| + assert_equal 5, ActionTrackerNotification.count + [person, community, p1, m1, m2].each do |profile| notification = ActionTrackerNotification.find_by profile_id: profile.id assert_equal action_tracker, notification.action_tracker end @@ -119,8 +126,13 @@ class NotifyActivityToProfilesJobTest < ActiveSupport::TestCase action_tracker = fast_create(ActionTracker::Record, :user_type => 'Profile', :user_id => person.id, :target_type => 'Profile', :target_id => community.id, :verb => 'join_community') refute NotifyActivityToProfilesJob::NOTIFY_ONLY_COMMUNITY.include?(action_tracker.verb) p1, p2, m1, m2 = fast_create(Person), fast_create(Person), fast_create(Person), fast_create(Person) - fast_create(Friendship, :person_id => person.id, :friend_id => p1.id) - fast_create(Friendship, :person_id => person.id, :friend_id => p2.id) + + circle1 = Circle.create!(:person=> p1, :name => "Zombies", :profile_type => 'Person') + circle2 = Circle.create!(:person=> p2, :name => "Zombies", :profile_type => 'Person') + + fast_create(ProfileFollower, :profile_id => person.id, :circle_id => circle1.id) + fast_create(ProfileFollower, :profile_id => person.id, :circle_id => circle2.id) + fast_create(RoleAssignment, :accessor_id => m1.id, :role_id => 3, :resource_id => community.id) fast_create(RoleAssignment, :accessor_id => m2.id, :role_id => 3, :resource_id => community.id) ActionTrackerNotification.delete_all diff --git a/test/unit/person_notifier_test.rb b/test/unit/person_notifier_test.rb index 0a37d1e..6b1c4a1 100644 --- a/test/unit/person_notifier_test.rb +++ b/test/unit/person_notifier_test.rb @@ -178,6 +178,7 @@ class PersonNotifierTest < ActiveSupport::TestCase update_product: -> { create Product, profile: @profile, product_category: create(ProductCategory, environment: Environment.default) }, remove_product: -> { create Product, profile: @profile, product_category: create(ProductCategory, environment: Environment.default) }, favorite_enterprise: -> { create FavoriteEnterprisePerson, enterprise: create(Enterprise), person: @member }, + new_follower: -> { @member } } ActionTrackerConfig.verb_names.each do |verb| @@ -197,6 +198,7 @@ class PersonNotifierTest < ActiveSupport::TestCase 'friend_url' => '/', 'friend_profile_custom_icon' => [], 'friend_name' => ['joe'], 'resource_name' => ['resource'], 'resource_profile_custom_icon' => [], 'resource_url' => ['/'], 'enterprise_name' => 'coop', 'enterprise_url' => '/coop', + 'follower_url' => '/', 'follower_profile_custom_icon' => [], 'follower_name' => ['joe'], 'view_url'=> ['/'], 'thumbnail_path' => ['1'], } a.get_url = '' diff --git a/test/unit/person_test.rb b/test/unit/person_test.rb index 440a8f6..6fbf2a1 100644 --- a/test/unit/person_test.rb +++ b/test/unit/person_test.rb @@ -728,7 +728,7 @@ class PersonTest < ActiveSupport::TestCase assert_equal [s4], p2.scraps_received.not_replies end - should "the followed_by method be protected and true to the person friends and herself by default" do + should "the followed_by method return true to the person friends and herself by default" do p1 = fast_create(Person) p2 = fast_create(Person) p3 = fast_create(Person) @@ -740,9 +740,9 @@ class PersonTest < ActiveSupport::TestCase assert p1.is_a_friend?(p4) assert_equal true, p1.send(:followed_by?,p1) - assert_equal true, p1.send(:followed_by?,p2) - assert_equal true, p1.send(:followed_by?,p4) - assert_equal false, p1.send(:followed_by?,p3) + assert_equal true, p2.send(:followed_by?,p1) + assert_equal true, p4.send(:followed_by?,p1) + assert_equal false, p3.send(:followed_by?,p1) end should "the person follows her friends and herself by default" do @@ -757,9 +757,9 @@ class PersonTest < ActiveSupport::TestCase assert p4.is_a_friend?(p1) assert_equal true, p1.follows?(p1) - assert_equal true, p1.follows?(p2) - assert_equal true, p1.follows?(p4) - assert_equal false, p1.follows?(p3) + assert_equal true, p2.follows?(p1) + assert_equal true, p4.follows?(p1) + assert_equal false, p3.follows?(p1) end should "a person member of a community follows the community" do @@ -836,18 +836,21 @@ class PersonTest < ActiveSupport::TestCase assert_nil Scrap.find_by(id: scrap.id) end - should "the tracked action be notified to person friends and herself" do + should "the tracked action be notified to person followers and herself" do Person.destroy_all p1 = fast_create(Person) p2 = fast_create(Person) p3 = fast_create(Person) p4 = fast_create(Person) - p1.add_friend(p2) - assert p1.is_a_friend?(p2) - refute p1.is_a_friend?(p3) - p1.add_friend(p4) - assert p1.is_a_friend?(p4) + circle2 = Circle.create!(:person=> p2, :name => "Zombies", :profile_type => 'Person') + circle4 = Circle.create!(:person=> p4, :name => "Zombies", :profile_type => 'Person') + + p2.follow(p1, circle2) + assert p2.follows?(p1) + refute p3.follows?(p1) + p4.follow(p1, circle4) + assert p4.follows?(p1) action_tracker = fast_create(ActionTracker::Record, :user_id => p1.id) ActionTrackerNotification.delete_all @@ -880,17 +883,19 @@ class PersonTest < ActiveSupport::TestCase end end - should "the tracked action notify friends with one delayed job process" do + should "the tracked action notify followers with one delayed job process" do p1 = fast_create(Person) p2 = fast_create(Person) p3 = fast_create(Person) p4 = fast_create(Person) - p1.add_friend(p2) - assert p1.is_a_friend?(p2) - refute p1.is_a_friend?(p3) - p1.add_friend(p4) - assert p1.is_a_friend?(p4) + circle2 = Circle.create!(:person=> p2, :name => "Zombies", :profile_type => 'Person') + circle4 = Circle.create!(:person=> p4, :name => "Zombies", :profile_type => 'Person') + p2.follow(p1, circle2) + assert p2.follows?(p1) + refute p3.follows?(p1) + p4.follow(p1, circle4) + assert p4.follows?(p1) action_tracker = fast_create(ActionTracker::Record, :user_id => p1.id) @@ -1035,11 +1040,13 @@ class PersonTest < ActiveSupport::TestCase p2 = create_user('p2').person p3 = create_user('p3').person c = fast_create(Community, :name => "Foo") + c.add_member(p1) process_delayed_job_queue c.add_member(p3) process_delayed_job_queue - assert_equal 4, ActionTracker::Record.count + + assert_equal 5, ActionTracker::Record.count assert_equal 5, ActionTrackerNotification.count has_add_member_notification = false ActionTrackerNotification.all.map do |notification| @@ -1951,4 +1958,51 @@ class PersonTest < ActiveSupport::TestCase person.save! end + should 'update profile circles for a person' do + person = create_user('testuser').person + community = fast_create(Community) + circle = Circle.create!(:person=> person, :name => "Zombies", :profile_type => 'Community') + circle2 = Circle.create!(:person=> person, :name => "Dota", :profile_type => 'Community') + circle3 = Circle.create!(:person=> person, :name => "Quadrado", :profile_type => 'Community') + person.follow(community, [circle, circle2]) + person.update_profile_circles(community, [circle2, circle3]) + assert_equivalent [circle2, circle3], ProfileFollower.with_profile(community).with_follower(person).map(&:circle) + end + + should 'a person follow a profile' do + person = create_user('testuser').person + community = fast_create(Community) + circle = Circle.create!(:person=> person, :name => "Zombies", :profile_type => 'Community') + person.follow(community, circle) + assert_includes person.followed_profiles, community + end + + should 'a person follow a profile with more than one circle' do + person = create_user('testuser').person + community = fast_create(Community) + circle = Circle.create!(:person=> person, :name => "Zombies", :profile_type => 'Community') + circle2 = Circle.create!(:person=> person, :name => "Dota", :profile_type => 'Community') + person.follow(community, [circle, circle2]) + assert_includes person.followed_profiles, community + assert_equivalent [circle, circle2], ProfileFollower.with_profile(community).with_follower(person).map(&:circle) + end + + should 'a person unfollow a profile' do + person = create_user('testuser').person + community = fast_create(Community) + circle = Circle.create!(:person=> person, :name => "Zombies", :profile_type => 'Community') + person.follow(community, circle) + person.unfollow(community) + assert_not_includes person.followed_profiles, community + end + + should 'a person remove a profile from a circle' do + person = create_user('testuser').person + community = fast_create(Community) + circle = Circle.create!(:person=> person, :name => "Zombies", :profile_type => 'Community') + circle2 = Circle.create!(:person=> person, :name => "Dota", :profile_type => 'Community') + person.follow(community, [circle, circle2]) + person.remove_profile_from_circle(community, circle) + assert_equivalent [circle2], ProfileFollower.with_profile(community).with_follower(person).map(&:circle) + end end diff --git a/test/unit/profile_followers_test.rb b/test/unit/profile_followers_test.rb new file mode 100644 index 0000000..89684e3 --- /dev/null +++ b/test/unit/profile_followers_test.rb @@ -0,0 +1,73 @@ +require_relative "../test_helper" + +class ProfileFollowersTest < ActiveSupport::TestCase + + should 'a person follow another' do + p1 = create_user('person_test').person + p2 = create_user('person_test_2').person + circle = Circle.create!(:person=> p1, :name => "Zombies", :profile_type => 'Person') + + assert_difference 'ProfileFollower.count' do + p1.follow(p2, circle) + end + + assert_includes p2.followers(true), p1 + assert_not_includes p1.followers(true), p2 + end + + should 'a person unfollow another person' do + p1 = create_user('person_test').person + p2 = create_user('person_test_2').person + circle = Circle.create!(:person=> p1, :name => "Zombies", :profile_type => 'Person') + + p1.follow(p2,circle) + + assert_difference 'ProfileFollower.count', -1 do + p1.unfollow(p2) + end + + assert_not_includes p2.followers(true), p1 + end + + should 'get the followed persons for a profile' do + p1 = create_user('person_test').person + p2 = create_user('person_test_2').person + p3 = create_user('person_test_3').person + circle = Circle.create!(:person=> p1, :name => "Zombies", :profile_type => 'Person') + + p1.follow(p2, circle) + p1.follow(p3, circle) + + assert_equivalent p1.followed_profiles, [p2,p3] + assert_equivalent Profile.followed_by(p1), [p2,p3] + end + + should 'not follow same person twice' do + p1 = create_user('person_test').person + p2 = create_user('person_test_2').person + circle = Circle.create!(:person=> p1, :name => "Zombies", :profile_type => 'Person') + + assert_difference 'ProfileFollower.count' do + p1.follow(p2, circle) + p1.follow(p2, circle) + end + + assert_equivalent p1.followed_profiles, [p2] + assert_equivalent p2.followers, [p1] + end + + should 'show the correct message when a profile is followed by the same person' do + p1 = create_user('person_test').person + p2 = create_user('person_test_2').person + circle = Circle.create!(:person=> p1, :name => "Zombies", :profile_type => 'Person') + + p1.follow(p2, circle) + profile_follower = ProfileFollower.new + profile_follower.circle = circle + profile_follower.profile = p2 + profile_follower.valid? + + assert_includes profile_follower.errors.messages[:profile_id], + "can't put a profile in the same circle twice" + end +end diff --git a/test/unit/scrap_test.rb b/test/unit/scrap_test.rb index fb68094..49e5c20 100644 --- a/test/unit/scrap_test.rb +++ b/test/unit/scrap_test.rb @@ -125,11 +125,11 @@ class ScrapTest < ActiveSupport::TestCase assert_equal c, ta.target end - should "notify leave_scrap action tracker verb to friends and itself" do + should "notify leave_scrap action tracker verb to followers and itself" do User.current = create_user p1 = User.current.person p2 = create_user.person - p1.add_friend(p2) + p2.add_friend(p1) process_delayed_job_queue s = Scrap.new s.sender= p1 @@ -180,11 +180,11 @@ class ScrapTest < ActiveSupport::TestCase assert_equal p, ta.user end - should "notify leave_scrap_to_self action tracker verb to friends and itself" do + should "notify leave_scrap_to_self action tracker verb to followers and itself" do User.current = create_user p1 = User.current.person p2 = create_user.person - p1.add_friend(p2) + p2.add_friend(p1) ActionTrackerNotification.delete_all Delayed::Job.delete_all s = Scrap.new -- libgit2 0.21.2