OpenProject is the leading open source project management software.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
openproject/app/models/watcher.rb

153 lines
5.3 KiB

#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
class Watcher < ApplicationRecord
belongs_to :watchable, polymorphic: true
belongs_to :user
validates_presence_of :watchable, :user
validates_uniqueness_of :user_id, scope: [:watchable_type, :watchable_id]
validate :validate_active_user
validate :validate_user_allowed_to_watch
def self.prune(user: [], project_id: nil)
user_ids = Array(user).compact.map { |u| u.is_a?(User) ? u.id : nil }.compact
projects = project_id ? Project.where(id: project_id) : Project.all
prune_project_related(user_ids, projects)
end
protected
def validate_active_user
# TODO add informative error message
return if user.blank?
errors.add :user_id, :invalid unless user.active_or_registered?
end
def validate_user_allowed_to_watch
# TODO add informative error message
return if user.blank? || watchable.blank?
errors.add :user_id, :invalid unless watchable.possible_watcher?(user)
end
class << self
def prune_project_related(user_ids, projects)
watchers = watchers_in_projects(projects, user_ids)
watchers_by_watchable_class = watchers
.includes({ watchable: :project }, :user)
.group_by(&:watchable_type)
watchers_by_watchable_class.each do |watchable_class_string, class_candidates|
watchable_class = watchable_class_string.constantize
destroy_watchers_if_permission_missing(watchable_class, class_candidates)
end
end
def watchers_in_projects(projects, user_ids)
watchable_classes = active_watchable_classes(user_ids)
neutral_scope = where(Arel::Nodes::Equality.new(1, 0))
watchable_classes.inject(neutral_scope) do |aggregate_scope, watched_class|
klass = watched_class.constantize
individual_scope = watchers_of_watchable(klass, projects, user_ids)
aggregate_scope.or(individual_scope)
end
end
def destroy_watchers_if_permission_missing(watchable_class, class_candidates)
watchers_by_users = class_candidates.group_by(&:user)
watchers_by_projects = class_candidates.group_by { |c| c.watchable.project }
if watchers_by_users.keys.length < watchers_by_projects.keys.length
prune_by_users(watchers_by_users, watchable_class)
else
prune_by_projects(watchers_by_projects, watchable_class)
end
end
def prune_by_users(watchers_by_users, watchable_class)
watchers_by_users.each do |user, watchers|
allowed_project_ids = Project
.allowed_to(user,
watchable_class.acts_as_watchable_permission)
.pluck(:id)
watchers
.select { |w| !allowed_project_ids.include?(w.watchable.project.id) }
.each(&:destroy)
end
end
def prune_by_projects(watchers_by_projects, watchable_class)
watchers_by_projects.each do |project, watchers|
allowed_user_ids = User
.allowed(watchable_class.acts_as_watchable_permission,
project)
.pluck(:id)
watchers
.select { |c| !allowed_user_ids.include?(c.user_id) }
.each(&:destroy)
end
end
def watchers_of_watchable(watchable, projects, user_ids)
# By using
# where(projects: { id: project.id }
# instead of
# where(projects_id: project.id)
# we don't have to distinguish between project associations with
# project_id on the watchable class and those on a class associated to
# the watchable class (using :through).
id_subquery = watchable
.joins(:watchers)
.joins(:project)
.where(projects: { id: projects.map(&:id) })
.select('watchers.id')
id_subquery = id_subquery.where(watchers: { user_id: user_ids }) unless user_ids.empty?
where(id: id_subquery)
end
def active_watchable_classes(user_ids)
classes = distinct(:watchable_type)
classes.where(user_id: user_ids) unless user_ids.blank?
classes.pluck(:watchable_type)
end
end
end