205 lines
7.2 KiB
Ruby
205 lines
7.2 KiB
Ruby
#
|
|
# Copyright (C) 2012 Instructure, Inc.
|
|
#
|
|
# This file is part of Canvas.
|
|
#
|
|
# Canvas is free software: you can redistribute it and/or modify it under
|
|
# the terms of the GNU Affero General Public License as published by the Free
|
|
# Software Foundation, version 3 of the License.
|
|
#
|
|
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License along
|
|
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
class UserFollow < ActiveRecord::Base
|
|
VALID_FOLLOWED_ITEM_TYPES = [:user, :collection, :group]
|
|
|
|
attr_accessible :following_user, :followed_item
|
|
|
|
belongs_to :following_user, :class_name => "User"
|
|
belongs_to :followed_item, :polymorphic => true, :types => VALID_FOLLOWED_ITEM_TYPES
|
|
|
|
validates_presence_of :following_user, :followed_item
|
|
|
|
# Using this method to create a new UserFollow is preferable to creating it
|
|
# directly, since it handles if the unique constraint fails because the
|
|
# record is being created twice in parallel.
|
|
#
|
|
# normally just leave complementary_record as false, that's used internally
|
|
# while creating the complementary record on the other shard.
|
|
def self.create_follow(following_user, followed_item, complementary_record = false)
|
|
search_shard = (complementary_record ? followed_item : following_user).shard
|
|
search_shard.activate do
|
|
UserFollow.unique_constraint_retry do
|
|
user_follow = UserFollow.first(:conditions => { :following_user_id => following_user.id, :followed_item_id => followed_item.id, :followed_item_type => followed_item.class.name })
|
|
user_follow ||= UserFollow.create(:following_user => following_user, :followed_item => followed_item)
|
|
end
|
|
end
|
|
end
|
|
|
|
validate_on_create :validate_following_logic
|
|
|
|
def validate_following_logic
|
|
case followed_item
|
|
when User
|
|
if followed_item == following_user
|
|
errors.add(:followed_item, t("errors.follow_self", "You cannot follow yourself"))
|
|
return false
|
|
end
|
|
when Collection
|
|
if followed_item.context == following_user
|
|
errors.add(:followed_item, t("errors.follow_own_collection", "You cannot follow your own collection"))
|
|
return false
|
|
end
|
|
when Group
|
|
# always ok
|
|
else
|
|
raise("unknown followed_item type: #{followed_item.inspect}")
|
|
end
|
|
return true
|
|
end
|
|
|
|
after_create :create_complementary_record
|
|
attr_writer :complementary_record
|
|
|
|
# returns true if the following user isn't on the same shard as the followed
|
|
# item, and this UserFollow is the secondary copy that's on the followed
|
|
# item's shard
|
|
def complementary_record?
|
|
self.shard != following_user.shard
|
|
end
|
|
|
|
def create_complementary_record
|
|
if !complementary_record? && followed_item.shard != following_user.shard
|
|
UserFollow.create_follow(following_user, followed_item, true)
|
|
end
|
|
true
|
|
end
|
|
|
|
after_destroy :destroy_complementary_record
|
|
def destroy_complementary_record
|
|
find_complementary_record.try(:destroy)
|
|
end
|
|
|
|
def find_complementary_record
|
|
return nil if followed_item.shard == following_user.shard
|
|
if self.shard == followed_item.shard
|
|
finding_shard = following_user.shard
|
|
elsif self.shard == following_user.shard
|
|
finding_shard = followed_item.shard
|
|
else
|
|
return nil
|
|
end
|
|
|
|
finding_shard.try(:activate) do
|
|
UserFollow.first(:conditions => { :following_user_id => following_user.id,
|
|
:followed_item_id => followed_item.id,
|
|
:followed_item_type => followed_item.class.name })
|
|
end
|
|
end
|
|
|
|
after_create :check_auto_follow_collections
|
|
|
|
# when a user follows a group or other user, they auto-follow all existing
|
|
# collections in that context as well
|
|
def check_auto_follow_collections
|
|
return true if self.complementary_record?
|
|
case followed_item
|
|
when User, Group
|
|
if !followed_item.collections.empty?
|
|
send_later_enqueue_args :auto_follow_collections, :priority => Delayed::LOW_PRIORITY
|
|
end
|
|
end
|
|
true
|
|
end
|
|
|
|
def auto_follow_collections
|
|
followed_item.collections.active.each do |coll|
|
|
if coll.grants_right?(following_user, :follow)
|
|
UserFollow.create_follow(following_user, coll)
|
|
end
|
|
end
|
|
end
|
|
|
|
after_destroy :check_auto_unfollow_collections
|
|
|
|
# when a user leaves a group, they auto-unfollow all private collections in
|
|
# that group
|
|
def check_auto_unfollow_collections
|
|
return true if self.complementary_record?
|
|
case followed_item
|
|
when Group
|
|
if !followed_item.collections.empty?
|
|
UserFollow.send_later_enqueue_args(:auto_unfollow_collections_for,
|
|
{ :priority => Delayed::LOW_PRIORITY },
|
|
self.following_user_id,
|
|
self.followed_item_type,
|
|
self.followed_item_id)
|
|
end
|
|
end
|
|
true
|
|
end
|
|
|
|
# this is called after the UserFollow object is destroyed, so it needs to
|
|
# re-lookup the user and context
|
|
def self.auto_unfollow_collections_for(following_user_id, followed_item_type, followed_item_id)
|
|
if context = Object.const_get(followed_item_type).find_by_id(followed_item_id)
|
|
following_user = User.find(following_user_id)
|
|
context.collections.active.each do |coll|
|
|
if !coll.grants_right?(following_user, :follow)
|
|
user_follow = following_user.user_follows.scoped(:conditions => { :followed_item_type => 'Collection',
|
|
:followed_item_id => coll.id }).first
|
|
user_follow.try(:destroy)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
trigger.after(:insert) do |t|
|
|
t.where("NEW.followed_item_type = 'Collection'") do
|
|
<<-SQL
|
|
UPDATE collections
|
|
SET followers_count = followers_count + 1
|
|
WHERE id = NEW.followed_item_id;
|
|
SQL
|
|
end
|
|
end
|
|
|
|
trigger.after(:delete) do |t|
|
|
t.where("OLD.followed_item_type = 'Collection'") do
|
|
<<-SQL
|
|
UPDATE collections
|
|
SET followers_count = followers_count - 1
|
|
WHERE id = OLD.followed_item_id;
|
|
SQL
|
|
end
|
|
end
|
|
|
|
# returns the subset of items that are currently being followed by the given user
|
|
#
|
|
# currently this method assumes that all the items are of the same type, this
|
|
# could be expanded later to partition the query by item type.
|
|
def self.followed_by_user(items, user)
|
|
user.shard.activate do
|
|
item_subset = items
|
|
item_ids = item_subset.map(&:id)
|
|
followed_ids = Set.new(connection.select_values(sanitize_sql_for_conditions(["SELECT followed_item_id FROM #{table_name} WHERE following_user_id = ? AND followed_item_type = ? AND followed_item_id IN (?)", user.id, item_subset.first.class.name, item_ids])))
|
|
item_subset.find_all { |c| followed_ids.include?(c.id.to_s) }
|
|
end
|
|
end
|
|
|
|
module FollowedItem
|
|
# returns the users who are following this item
|
|
def followers
|
|
follows = self.following_user_follows.to_a
|
|
UserFollow.send(:preload_associations, follows, :following_user)
|
|
follows.map(&:following_user)
|
|
end
|
|
end
|
|
end
|