Feature/member notifications (#8958)
* spec with correctly scoped links * move db check into own file - fix deprecation * basic spec for member creation service * use constants for all notifications * send an OP notification after member has been created * send an OP notification after member has been updated * mails on group member added Depending on whether the membership existed before or not, an updated or a created notification is send. This is done asynchronously. * move all mail sender background jobs into namespace * wip * wip * correct handling group member notifications * add setting enable/disable mail sending on member alterations * use services in members controller * move Notifiable to OpenProject * remove member after save hooks * cleanup/testing/linting * render member mails in receiver locale * remove add_member! method * use mailer layout for all mailers * Update app/services/groups/cleanup_inherited_roles_service.rb Co-authored-by: Oliver Günther <mail@oliverguenther.de> * use around callback to avoid prepending * handle nil params Co-authored-by: Oliver Günther <mail@oliverguenther.de>pull/9172/head
parent
3f3e831f6a
commit
9fa5599392
@ -0,0 +1,94 @@ |
||||
#-- 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. |
||||
#++ |
||||
|
||||
# Sends mails for updates to memberships. There can be three cases we have to cover: |
||||
# * user is added to a project |
||||
# * existing project membership is altered |
||||
# * global roles are altered |
||||
# |
||||
# There is no creation of a global membership as far as the user is concerned. Hence, all |
||||
# global cases can be covered by one method. |
||||
# |
||||
# The mailer does not fan out in case a group is provided. The individual members of a group |
||||
# need to be mailed to individually. |
||||
|
||||
class MemberMailer < BaseMailer |
||||
def added_project(current_user, member) |
||||
alter_project(current_user, |
||||
member, |
||||
in_member_locale(member) { I18n.t(:'mail_member_added_project.subject', project: member.project.name) }) |
||||
end |
||||
|
||||
def updated_project(current_user, member) |
||||
alter_project(current_user, |
||||
member, |
||||
in_member_locale(member) { I18n.t(:'mail_member_updated_project.subject', project: member.project.name) }) |
||||
end |
||||
|
||||
def updated_global(current_user, member) |
||||
send_mail(current_user, |
||||
member, |
||||
in_member_locale(member) { I18n.t(:'mail_member_updated_global.subject') }) |
||||
end |
||||
|
||||
private |
||||
|
||||
def alter_project(current_user, member, subject) |
||||
send_mail(current_user, |
||||
member, |
||||
subject) do |
||||
open_project_headers Project: member.project.identifier |
||||
|
||||
@project = member.project |
||||
end |
||||
end |
||||
|
||||
def send_mail(current_user, member, subject) |
||||
in_member_locale(member) do |
||||
User.execute_as(current_user) do |
||||
message_id member, current_user |
||||
|
||||
@roles = member.roles |
||||
@principal = member.principal |
||||
|
||||
yield if block_given? |
||||
|
||||
mail to: member.principal.mail, |
||||
subject: subject |
||||
end |
||||
end |
||||
end |
||||
|
||||
def in_member_locale(member, &block) |
||||
raise ArgumentError unless member.principal.is_a?(User) |
||||
|
||||
with_locale_for(member.principal, &block) |
||||
end |
||||
end |
@ -1,109 +0,0 @@ |
||||
module Group::Destroy |
||||
extend ActiveSupport::Concern |
||||
|
||||
included do |
||||
before_destroy :destroy_members |
||||
end |
||||
|
||||
## |
||||
# Instead of firing of separate queries for each and every Member and MemberRole |
||||
# instance upon group deletion this implementation does most of the deletion |
||||
# in a hand full of aggregate queries. |
||||
# |
||||
# Instead of doing |
||||
# |
||||
# Member: |
||||
# before_destroy :remove_from_category_assignments |
||||
# after_destroy :unwatch_from_permission_change |
||||
# after_destroy :destroy_notification |
||||
# |
||||
# MemberRole: |
||||
# after_destroy :remove_role_from_group_users |
||||
# |
||||
# for every row all relevant roles are deleted within 5 mass delete queries |
||||
# + 1 query for each member instance for each group itself + number of watchers |
||||
# among the users in the deleted group. |
||||
# |
||||
# Example: |
||||
# |
||||
# Given: 150 projects and 1 group with 20 users which is member in every project |
||||
# |
||||
# That makes 150 * 20 3000 Member rows and also 3000 MemberRole rows. |
||||
# |
||||
# Without this patch this would result in at least 4 queries for each member |
||||
# (the callbacks mentioned above + the deletion of the member) and 2 queries |
||||
# for each MemberRole (callback mentioned above + deletion of the actual MemberRole). |
||||
# Altogether that makes: |
||||
# |
||||
# num_queries_pre_patch = 3000 * 4 + 3000 * 2 + W = 18000 + W |
||||
# |
||||
# Where W is the number of watchers among the users in the destroyed group. |
||||
# The actual number is actually even higher as for the callbacks a bunch of read queries |
||||
# (loading the project, the user, etc.) are triggered, too. |
||||
# |
||||
# With this patch the number of queries is reduced to the 5 + 1 for each group member |
||||
# as explained above, making it: |
||||
# |
||||
# num_queries_post_patch = 5 + 150 + W = 155 + W |
||||
# |
||||
def destroy_members |
||||
MemberRole.transaction do |
||||
members = Member.table_name |
||||
member_roles = MemberRole.table_name |
||||
|
||||
# Store all project/user combinations for later watcher pruning |
||||
# See: Member#unwatch_from_permission_change |
||||
user_id_and_project_id = Member |
||||
.joins( |
||||
"INNER JOIN #{member_roles} umr |
||||
ON #{members}.id = umr.member_id |
||||
INNER JOIN #{member_roles} gmr |
||||
ON umr.inherited_from = gmr.id |
||||
INNER JOIN #{members} gm |
||||
ON gm.id = gmr.member_id AND gm.user_id = #{id}" |
||||
) |
||||
.distinct |
||||
.pluck(:user_id, :project_id) |
||||
|
||||
user_ids, project_ids = user_id_and_project_id.each_with_object([[], []]) do |element, array| |
||||
array.first << element.first |
||||
array.last << element.last |
||||
end |
||||
|
||||
users = User.find(user_ids) |
||||
|
||||
# Delete all MemberRoles created through this group for each user within it. |
||||
MemberRole |
||||
.joins("INNER JOIN #{member_roles} b on #{member_roles}.inherited_from = b.id") |
||||
.joins("INNER JOIN #{members} on #{members}.id = b.member_id") |
||||
.where("#{members}.user_id" => id) # group ID |
||||
.delete_all |
||||
|
||||
# Delete all MemberRoles associating this group itself with a project. |
||||
MemberRole |
||||
.joins("INNER JOIN #{members} on #{members}.id = #{member_roles}.member_id") |
||||
.where("#{members}.user_id" => id) |
||||
.delete_all |
||||
|
||||
Watcher.prune(user: users, project_id: project_ids) |
||||
|
||||
# Destroy member instances for this group itself to trigger |
||||
# member destroyed notifications. |
||||
Member |
||||
.where(user_id: id) |
||||
.destroy_all |
||||
|
||||
# Remove category based auto assignments for this member. |
||||
# See: Member#remove_from_category_assignments |
||||
Category |
||||
.joins("INNER JOIN #{members} |
||||
ON #{members}.project_id = categories.project_id |
||||
AND #{members}.user_id = categories.assigned_to_id") |
||||
.where("#{members}.user_id" => id) |
||||
.update_all "assigned_to_id = NULL" |
||||
|
||||
self.users.delete_all # remove all users from this group |
||||
reload # so associated member instances are not destroyed again |
||||
end |
||||
end |
||||
end |
@ -1,78 +0,0 @@ |
||||
#-- 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 WatcherNotificationMailer |
||||
class << self |
||||
def handle_watcher(watcher, watcher_changer) |
||||
# We only handle this watcher setting if associated user wants to be notified |
||||
# about it. |
||||
return unless notify_about_watcher_changed?(watcher, watcher_changer) |
||||
|
||||
unless other_jobs_queued?(watcher.watchable) |
||||
perform_notification_job(watcher, watcher_changer) |
||||
end |
||||
end |
||||
|
||||
private |
||||
|
||||
def perform_notification_job(_watcher, _watcher_changer) |
||||
raise NotImplementedError, 'Subclass has to implement #notification_job' |
||||
end |
||||
|
||||
# HACK: TODO this needs generalization as well as performance improvements |
||||
# We need to make sure no work package created or updated job is queued to avoid sending two |
||||
# mails in short succession. |
||||
def other_jobs_queued?(work_package) |
||||
Delayed::Job.where('handler LIKE ?', |
||||
"%NotificationJob%journal_id: #{work_package.journals.last.id}%").exists? |
||||
end |
||||
|
||||
def notify_about_watcher_changed?(watcher, watcher_changer) |
||||
return false if notify_about_self_watching?(watcher, watcher_changer) |
||||
|
||||
case watcher.user.mail_notification |
||||
when 'only_my_events' |
||||
true |
||||
when 'selected' |
||||
watching_selected_includes_project?(watcher) |
||||
else |
||||
watcher.user.notify_about?(watcher.watchable) |
||||
end |
||||
end |
||||
|
||||
def notify_about_self_watching?(watcher, watcher_changer) |
||||
watcher.user == watcher_changer && !watcher.user.pref.self_notified? |
||||
end |
||||
|
||||
def watching_selected_includes_project?(watcher) |
||||
watcher.user.notified_projects_ids.include?(watcher.watchable.project_id) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,97 @@ |
||||
#-- 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. |
||||
#++ |
||||
|
||||
# Deletes the roles granted to users by being part of a group. |
||||
# Will only delete the roles that are no longer granted so the group's membership needs to be deleted first. |
||||
# In case the user has roles independent of the group (not inherited) they are kept. |
||||
# |
||||
# This service is not specific to a certain group membership being removed. Rather, it will remove |
||||
# all MemberRole associations and in turn their Member associations if no matching inherited_from is found. |
||||
|
||||
module Groups |
||||
class CleanupInheritedRolesService < ::BaseServices::BaseContracted |
||||
include Groups::Concerns::MembershipManipulation |
||||
|
||||
def initialize(group, current_user:, contract_class: AdminOnlyContract) |
||||
self.model = group |
||||
|
||||
super user: current_user, |
||||
contract_class: contract_class |
||||
end |
||||
|
||||
private |
||||
|
||||
def modify_members_and_roles(params) |
||||
affected_member_ids = execute_query(remove_member_roles_sql(params[:member_role_ids])) |
||||
members_to_remove = members_to_remove(affected_member_ids) |
||||
|
||||
remove_members(members_to_remove) |
||||
|
||||
affected_member_ids - members_to_remove.map(&:id) |
||||
end |
||||
|
||||
def remove_member_roles_sql(member_role_ids) |
||||
if member_role_ids.present? |
||||
sql_query = <<~SQL |
||||
DELETE FROM #{MemberRole.table_name} |
||||
WHERE id IN (:member_role_ids) |
||||
RETURNING member_roles.member_id |
||||
SQL |
||||
|
||||
::OpenProject::SqlSanitization |
||||
.sanitize sql_query, |
||||
member_role_ids: member_role_ids |
||||
else |
||||
<<~SQL |
||||
DELETE FROM #{MemberRole.table_name} |
||||
USING #{MemberRole.table_name} user_member_roles |
||||
WHERE |
||||
user_member_roles.inherited_from IS NOT NULL |
||||
AND NOT EXISTS (SELECT 1 FROM #{MemberRole.table_name} group_member_roles WHERE group_member_roles.id = user_member_roles.inherited_from) |
||||
AND user_member_roles.id = member_roles.id |
||||
RETURNING member_roles.member_id |
||||
SQL |
||||
end |
||||
end |
||||
|
||||
def members_to_remove(member_ids) |
||||
Member |
||||
.where(id: member_ids) |
||||
.where.not(id: MemberRole.select(:member_id).distinct) |
||||
.to_a |
||||
end |
||||
|
||||
def remove_members(members) |
||||
members.each do |member| |
||||
Members::DeleteService |
||||
.new(model: member, user: user, contract_class: EmptyContract) |
||||
.call |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,84 @@ |
||||
#-- 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. |
||||
#++ |
||||
|
||||
module Groups::Concerns |
||||
module MembershipManipulation |
||||
extend ActiveSupport::Concern |
||||
|
||||
def after_validate(params, _call) |
||||
params ||= {} |
||||
|
||||
with_error_handled do |
||||
::Group.transaction do |
||||
exec_query!(params, params.delete(:send_notifications) { true }) |
||||
end |
||||
end |
||||
end |
||||
|
||||
private |
||||
|
||||
def with_error_handled |
||||
yield |
||||
ServiceResult.new success: true, result: model |
||||
rescue StandardError => e |
||||
Rails.logger.error { "Failed to modify members and associated roles of group #{model.id}: #{e} #{e.message}" } |
||||
ServiceResult.new(success: false, |
||||
message: I18n.t(:notice_internal_server_error, app_title: Setting.app_title)) |
||||
end |
||||
|
||||
def exec_query!(params, send_notifications) |
||||
affected_member_ids = modify_members_and_roles(params) |
||||
|
||||
touch_updated(affected_member_ids) |
||||
|
||||
send_notifications(affected_member_ids) if affected_member_ids.any? && send_notifications |
||||
end |
||||
|
||||
def modify_members_and_roles(_params) |
||||
raise NotImplementedError |
||||
end |
||||
|
||||
def execute_query(query) |
||||
::Group |
||||
.connection |
||||
.exec_query(query) |
||||
.rows |
||||
.flatten |
||||
end |
||||
|
||||
def touch_updated(member_ids) |
||||
Member |
||||
.where(id: member_ids) |
||||
.touch_all |
||||
end |
||||
|
||||
def send_notifications(member_ids) |
||||
Notifications::GroupMemberAlteredJob.perform_later(member_ids) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,128 @@ |
||||
#-- 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. |
||||
#++ |
||||
|
||||
# Updates the roles of a membership assigned to the group. |
||||
|
||||
module Groups |
||||
class UpdateRolesService < ::BaseServices::BaseContracted |
||||
include Groups::Concerns::MembershipManipulation |
||||
|
||||
def initialize(group, current_user:, contract_class: AdminOnlyContract) |
||||
self.model = group |
||||
|
||||
super user: current_user, |
||||
contract_class: contract_class |
||||
end |
||||
|
||||
private |
||||
|
||||
def modify_members_and_roles(params) |
||||
member = params.fetch(:member) |
||||
|
||||
sql_query = ::OpenProject::SqlSanitization |
||||
.sanitize update_roles_cte, |
||||
group_id: model.id, |
||||
member_id: member.id, |
||||
project_id: member.project_id, |
||||
role_ids: member.role_ids |
||||
|
||||
execute_query(sql_query) |
||||
end |
||||
|
||||
# rubocop:disable Metrics/AbcSize |
||||
def update_roles_cte |
||||
<<~SQL |
||||
WITH |
||||
-- select all users of the group |
||||
group_users AS ( |
||||
SELECT user_id |
||||
FROM #{GroupUser.table_name} |
||||
WHERE group_id = :group_id |
||||
), |
||||
-- select all members of the users of the group |
||||
user_members AS ( |
||||
SELECT id |
||||
FROM #{Member.table_name} |
||||
WHERE user_id IN (SELECT user_id FROM group_users) |
||||
AND project_id = :project_id |
||||
), |
||||
-- select all member roles the group has for the member |
||||
group_member_roles AS ( |
||||
SELECT member_roles.role_id AS role_id, |
||||
member_roles.id |
||||
FROM #{MemberRole.table_name} member_roles |
||||
WHERE member_roles.member_id = :member_id |
||||
), |
||||
-- delete all roles assigned to users that group no longer has but keep those that the user |
||||
-- has independently of the group (not inherited) |
||||
remove_roles AS ( |
||||
DELETE FROM #{MemberRole.table_name} |
||||
USING #{MemberRole.table_name} user_member_roles |
||||
JOIN user_members ON user_members.id = user_member_roles.member_id |
||||
LEFT JOIN group_member_roles ON user_member_roles.role_id = group_member_roles.role_id |
||||
WHERE user_member_roles.inherited_from IS NOT NULL AND group_member_roles.role_id IS NULL |
||||
AND member_roles.id = user_member_roles.id |
||||
RETURNING #{MemberRole.table_name}.id, #{MemberRole.table_name}.member_id, #{MemberRole.table_name}.role_id |
||||
), |
||||
-- add all roles to the user memberships |
||||
add_roles AS ( |
||||
INSERT INTO #{MemberRole.table_name} (member_id, role_id, inherited_from) |
||||
SELECT user_members.id, group_member_roles.role_id, group_member_roles.id |
||||
FROM group_member_roles, user_members |
||||
-- Ignore if role was already assigned |
||||
ON CONFLICT DO NOTHING |
||||
RETURNING member_id, role_id, id |
||||
), |
||||
-- get all the member_roles that are duplicates of removed ones |
||||
members_with_removed_roles AS ( |
||||
SELECT DISTINCT remove_roles.member_id |
||||
FROM remove_roles |
||||
WHERE NOT EXISTS |
||||
(SELECT 1 FROM #{MemberRole.table_name} |
||||
WHERE #{MemberRole.table_name}.member_id = remove_roles.member_id |
||||
AND #{MemberRole.table_name}.role_id = remove_roles.role_id |
||||
AND #{MemberRole.table_name}.id != remove_roles.id) |
||||
), |
||||
-- get only the ids of members where roles have been added the member did not have before |
||||
members_with_added_roles AS ( |
||||
SELECT DISTINCT add_roles.member_id |
||||
FROM add_roles |
||||
WHERE NOT EXISTS |
||||
(SELECT 1 FROM #{MemberRole.table_name} |
||||
WHERE #{MemberRole.table_name}.member_id = add_roles.member_id |
||||
AND #{MemberRole.table_name}.role_id = add_roles.role_id |
||||
AND #{MemberRole.table_name}.id != add_roles.id) |
||||
) |
||||
|
||||
SELECT member_id from members_with_removed_roles |
||||
UNION SELECT member_id from members_with_added_roles |
||||
SQL |
||||
end |
||||
# rubocop:enable Metrics/AbcSize |
||||
end |
||||
end |
@ -0,0 +1,70 @@ |
||||
#-- 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. |
||||
#++ |
||||
|
||||
module Members |
||||
class CleanupService < ::BaseServices::BaseCallable |
||||
def initialize(users, projects) |
||||
self.users = users |
||||
self.projects = Array(projects) |
||||
|
||||
super() |
||||
end |
||||
|
||||
protected |
||||
|
||||
def perform(_params = {}) |
||||
prune_watchers |
||||
unassign_categories |
||||
|
||||
ServiceResult.new(success: true) |
||||
end |
||||
|
||||
attr_accessor :users, |
||||
:projects |
||||
|
||||
def prune_watchers |
||||
Watcher.prune(user: users, project_id: project_ids) |
||||
end |
||||
|
||||
def unassign_categories |
||||
Category |
||||
.where(assigned_to_id: users) |
||||
.where(project_id: project_ids) |
||||
.where.not(assigned_to_id: Member.assignable.of(projects).select(:user_id)) |
||||
.update_all(assigned_to_id: nil) |
||||
end |
||||
|
||||
def project_ids |
||||
projects.first.is_a?(Project) ? projects.map(&:id) : projects |
||||
end |
||||
|
||||
def members_table |
||||
Member.table_name |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,49 @@ |
||||
#-- 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. |
||||
#++ |
||||
|
||||
module Members::Concerns::CleanedUp |
||||
extend ActiveSupport::Concern |
||||
|
||||
included do |
||||
around_call :cleanup_members |
||||
|
||||
protected |
||||
|
||||
def cleanup_members |
||||
service_call = yield |
||||
|
||||
return unless service_call.success? |
||||
|
||||
member = service_call.result |
||||
|
||||
Members::CleanupService |
||||
.new(member.principal, member.project_id) |
||||
.call |
||||
end |
||||
end |
||||
end |
@ -1,77 +0,0 @@ |
||||
<%#-- 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. |
||||
|
||||
++#%> |
||||
<html> |
||||
<head> |
||||
<style> |
||||
:root { |
||||
color-scheme: light dark; |
||||
supported-color-schemes: light dark; |
||||
} |
||||
|
||||
body { |
||||
font-family: Verdana, sans-serif; |
||||
font-size: 0.8em; |
||||
color: #484848; |
||||
background: #FFFFFF; |
||||
} |
||||
|
||||
@media (prefers-color-scheme: dark) { |
||||
body { |
||||
color: #CCC !important; |
||||
background: #222222 !important; |
||||
} |
||||
} |
||||
|
||||
h1, h2, h3 { font-family: "Trebuchet MS", Verdana, sans-serif; margin: 0px; } |
||||
h1 { font-size: 1.2em; } |
||||
h2, h3 { font-size: 1.1em; } |
||||
a, a:link, a:visited { color: #2A5685;} |
||||
a:hover, a:active { color: #c61a1a; } |
||||
a.op-uc-link_permalink { display: none; } |
||||
hr { |
||||
width: 100%; |
||||
height: 1px; |
||||
background: #ccc; |
||||
border: 0; |
||||
} |
||||
.footer { |
||||
font-size: 0.8em; |
||||
font-style: italic; |
||||
} |
||||
</style> |
||||
</head> |
||||
<body> |
||||
<span class="header"><%= OpenProject::TextFormatting::Renderer.format_text(Setting.localized_emails_header) %></span> |
||||
<%= call_hook(:view_layouts_mailer_html_before_content, self.assigns) %> |
||||
<%= yield %> |
||||
<%= call_hook(:view_layouts_mailer_html_after_content, self.assigns) %> |
||||
<hr /> |
||||
<span class="footer"><%= OpenProject::TextFormatting::Renderer.format_text(Setting.localized_emails_footer) %></span> |
||||
</body> |
||||
</html> |
@ -1,35 +0,0 @@ |
||||
<%#-- 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. |
||||
|
||||
++#%> |
||||
|
||||
<%= Setting.localized_emails_header %> |
||||
<%= call_hook(:view_layouts_mailer_plain_before_content, self.assigns) %> |
||||
<%= yield %> |
||||
<%= call_hook(:view_layouts_mailer_plain_after_content, self.assigns) %> |
||||
-- |
||||
<%= Setting.localized_emails_footer %> |
@ -0,0 +1,10 @@ |
||||
<%= I18n.t(:'mail_member_added_project.body.added_by', project: @project.name, user: User.current.name) %> |
||||
|
||||
<%= I18n.t(:'mail_member_added_project.body.roles') %> |
||||
<ul> |
||||
<% @roles.each do |role| %> |
||||
<li> |
||||
<%= role.name %> |
||||
</li> |
||||
<% end %> |
||||
</ul> |
@ -0,0 +1,6 @@ |
||||
<%= I18n.t(:'mail_member_added_project.body.added_by', project: h(@project.name), user: h(User.current.name)) %> |
||||
|
||||
<%= I18n.t(:'mail_member_added_project.body.roles') %> |
||||
<% @roles.each do |role| %> |
||||
* <%= h(role.name) %> |
||||
<% end %> |
@ -0,0 +1,10 @@ |
||||
<%= I18n.t(:'mail_member_updated_global.body.added_by', user: User.current.name) %> |
||||
|
||||
<%= I18n.t(:'mail_member_updated_global.body.roles') %> |
||||
<ul> |
||||
<% @roles.each do |role| %> |
||||
<li> |
||||
<%= role.name %> |
||||
</li> |
||||
<% end %> |
||||
</ul> |
@ -0,0 +1,6 @@ |
||||
<%= I18n.t(:'mail_member_updated_global.body.added_by', user: User.current.name) %> |
||||
|
||||
<%= I18n.t(:'mail_member_updated_global.body.roles') %> |
||||
<% @roles.each do |role| %> |
||||
* <%= h(role.name) %> |
||||
<% end %> |
@ -0,0 +1,10 @@ |
||||
<%= I18n.t(:'mail_member_updated_project.body.added_by', project: @project.name, user: User.current.name) %> |
||||
|
||||
<%= I18n.t(:'mail_member_updated_project.body.roles') %> |
||||
<ul> |
||||
<% @roles.each do |role| %> |
||||
<li> |
||||
<%= role.name %> |
||||
</li> |
||||
<% end %> |
||||
</ul> |
@ -0,0 +1,6 @@ |
||||
<%= I18n.t(:'mail_member_updated_project.body.added_by', project: @project.name, user: User.current.name) %> |
||||
|
||||
<%= I18n.t(:'mail_member_updated_project.body.roles') %> |
||||
<% @roles.each do |role| %> |
||||
* <%= h(role.name) %> |
||||
<% end %> |
@ -0,0 +1,50 @@ |
||||
#-- 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 Mails::MemberCreatedJob < Mails::MemberJob |
||||
private |
||||
|
||||
alias_method :send_for_project_user, :send_added_project |
||||
|
||||
def send_for_group_user(current_user, user_member, group_member) |
||||
if new_roles_added?(user_member, group_member) |
||||
send_updated_project(current_user, user_member) |
||||
elsif all_roles_added?(user_member, group_member) |
||||
send_added_project(current_user, user_member) |
||||
end |
||||
end |
||||
|
||||
def new_roles_added?(user_member, group_member) |
||||
(group_member.member_roles.map(&:id) - user_member.member_roles.map(&:inherited_from)).length < |
||||
group_member.member_roles.length && user_member.member_roles.any? { |mr| mr.inherited_from.nil? } |
||||
end |
||||
|
||||
def all_roles_added?(user_member, group_member) |
||||
(user_member.member_roles.map(&:inherited_from) - group_member.member_roles.map(&:id)).empty? |
||||
end |
||||
end |
@ -0,0 +1,90 @@ |
||||
#-- 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 Mails::MemberJob < ApplicationJob |
||||
queue_with_priority :notification |
||||
|
||||
def perform(current_user:, |
||||
member:) |
||||
if member.project.nil? |
||||
send_updated_global(current_user, member) |
||||
elsif member.principal.is_a?(Group) |
||||
every_group_user_member(member) do |user_member| |
||||
send_for_group_user(current_user, user_member, member) |
||||
end |
||||
elsif member.principal.is_a?(User) |
||||
send_for_project_user(current_user, member) |
||||
end |
||||
end |
||||
|
||||
private |
||||
|
||||
def send_for_group_user(_current_user, _member, _group) |
||||
raise NotImplementedError, "subclass responsibility" |
||||
end |
||||
|
||||
def send_for_project_user(_current_user, _member) |
||||
raise NotImplementedError, "subclass responsibility" |
||||
end |
||||
|
||||
def send_updated_global(current_user, member) |
||||
return if sending_disabled?(:updated) |
||||
|
||||
MemberMailer |
||||
.updated_global(current_user, member) |
||||
.deliver_now |
||||
end |
||||
|
||||
def send_added_project(current_user, member) |
||||
return if sending_disabled?(:added) |
||||
|
||||
MemberMailer |
||||
.added_project(current_user, member) |
||||
.deliver_now |
||||
end |
||||
|
||||
def send_updated_project(current_user, member) |
||||
return if sending_disabled?(:updated) |
||||
|
||||
MemberMailer |
||||
.updated_project(current_user, member) |
||||
.deliver_now |
||||
end |
||||
|
||||
def every_group_user_member(member, &block) |
||||
Member |
||||
.of(member.project) |
||||
.where(principal: member.principal.users) |
||||
.includes(:project, :principal, :roles, :member_roles) |
||||
.each(&block) |
||||
end |
||||
|
||||
def sending_disabled?(setting) |
||||
!Setting.notified_events.include?("membership_#{setting}") |
||||
end |
||||
end |
@ -0,0 +1,37 @@ |
||||
#-- 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 Mails::MemberUpdatedJob < Mails::MemberJob |
||||
private |
||||
|
||||
alias_method :send_for_project_user, :send_updated_project |
||||
|
||||
def send_for_group_user(current_user, member, _group) |
||||
send_updated_project(current_user, member) |
||||
end |
||||
end |
@ -0,0 +1,78 @@ |
||||
#-- 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 Mails::WatcherJob < Mails::DeliverJob |
||||
def perform(watcher, watcher_changer) |
||||
self.watcher = watcher |
||||
|
||||
super(watcher.user, watcher_changer) |
||||
end |
||||
|
||||
def render_mail(recipient:, sender:) |
||||
UserMailer |
||||
.work_package_watcher_changed(watcher.watchable, |
||||
recipient, |
||||
sender, |
||||
action) |
||||
end |
||||
|
||||
private |
||||
|
||||
attr_accessor :watcher |
||||
|
||||
def abort? |
||||
super || !notify_about_watcher_changed? |
||||
end |
||||
|
||||
def notify_about_watcher_changed? |
||||
return false if notify_about_self_watching? |
||||
|
||||
case watcher.user.mail_notification |
||||
when 'only_my_events' |
||||
true |
||||
when 'selected' |
||||
watching_selected_includes_project? |
||||
else |
||||
watcher.user.notify_about?(watcher.watchable) |
||||
end |
||||
end |
||||
|
||||
def notify_about_self_watching? |
||||
watcher.user == sender && !sender.pref.self_notified? |
||||
end |
||||
|
||||
def watching_selected_includes_project? |
||||
watcher.user.notified_projects_ids.include?(watcher.watchable.project_id) |
||||
end |
||||
|
||||
def action |
||||
raise NotImplementedError, 'subclass responsibility' |
||||
end |
||||
end |
@ -0,0 +1,157 @@ |
||||
#-- 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. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe MemberMailer, type: :mailer do |
||||
let(:current_user) { FactoryBot.build_stubbed(:user) } |
||||
let(:member) do |
||||
FactoryBot.build_stubbed(:member, |
||||
principal: principal, |
||||
project: project, |
||||
roles: roles) |
||||
end |
||||
let(:principal) { FactoryBot.build_stubbed(:user) } |
||||
let(:project) { FactoryBot.build_stubbed(:project) } |
||||
let(:roles) { [FactoryBot.build_stubbed(:role), FactoryBot.build_stubbed(:role)] } |
||||
|
||||
shared_examples_for 'has a subject' do |key| |
||||
it "has a subject" do |
||||
if project |
||||
expect(subject.subject) |
||||
.to eql I18n.t(key, project: project.name) |
||||
else |
||||
expect(subject.subject) |
||||
.to eql I18n.t(key) |
||||
end |
||||
end |
||||
end |
||||
|
||||
shared_examples_for 'fails for a group' do |
||||
let(:principal) { FactoryBot.build_stubbed(:group) } |
||||
|
||||
it 'raises an argument error' do |
||||
# Calling .to in order to have the mail rendered |
||||
expect { subject.to } |
||||
.to raise_error ArgumentError |
||||
end |
||||
end |
||||
|
||||
shared_examples_for "sends a mail to the member's principal" do |
||||
let(:principal) { FactoryBot.build_stubbed(:group) } |
||||
|
||||
it 'raises an argument error' do |
||||
# Calling .to in order to have the mail rendered |
||||
expect { subject.to } |
||||
.to raise_error ArgumentError |
||||
end |
||||
end |
||||
|
||||
shared_examples_for 'sets the expected message_id header' do |
||||
it 'sets the expected message_id header' do |
||||
expect(subject['Message-ID'].value) |
||||
.to eql "<openproject.member-#{current_user.id}-#{member.id}.#{member.created_at.strftime('%Y%m%d%H%M%S')}@example.net>" |
||||
end |
||||
end |
||||
|
||||
shared_examples_for 'sets the expected openproject header' do |
||||
it 'sets the expected openproject header' do |
||||
expect(subject['X-OpenProject-Project'].value) |
||||
.to eql project.identifier |
||||
end |
||||
end |
||||
|
||||
shared_examples_for 'has the expected body' do |
||||
let(:body) { subject.body.parts.detect { |part| part['Content-Type'].value == 'text/html' }.body.to_s } |
||||
|
||||
it 'has the expected header' do |
||||
expect(body) |
||||
.to have_text(expected_header) |
||||
end |
||||
|
||||
it 'highlights the roles received' do |
||||
expected = <<~MSG |
||||
<ul> |
||||
<li> #{roles.first.name} </li> |
||||
<li> #{roles.last.name} </li> |
||||
</ul> |
||||
MSG |
||||
|
||||
expect(body) |
||||
.to be_html_eql(expected) |
||||
.at_path('body/ul') |
||||
end |
||||
end |
||||
|
||||
describe '#added_project' do |
||||
subject { MemberMailer.added_project(current_user, member) } |
||||
|
||||
it_behaves_like "sends a mail to the member's principal" |
||||
it_behaves_like 'has a subject', :'mail_member_added_project.subject' |
||||
it_behaves_like 'sets the expected message_id header' |
||||
it_behaves_like 'sets the expected openproject header' |
||||
it_behaves_like 'has the expected body' do |
||||
let(:expected_header) do |
||||
"#{current_user.name} added you as a member to the project '#{project.name}'." |
||||
end |
||||
end |
||||
it_behaves_like 'fails for a group' |
||||
end |
||||
|
||||
describe '#updated_project' do |
||||
subject { MemberMailer.updated_project(current_user, member) } |
||||
|
||||
it_behaves_like "sends a mail to the member's principal" |
||||
it_behaves_like 'has a subject', :'mail_member_updated_project.subject' |
||||
it_behaves_like 'sets the expected message_id header' |
||||
it_behaves_like 'sets the expected openproject header' |
||||
it_behaves_like 'has the expected body' do |
||||
let(:expected_header) do |
||||
"#{current_user.name} updated the roles you have in the project '#{project.name}'." |
||||
end |
||||
end |
||||
it_behaves_like 'fails for a group' |
||||
end |
||||
|
||||
describe '#updated_global' do |
||||
let(:project) { nil } |
||||
|
||||
subject { MemberMailer.updated_global(current_user, member) } |
||||
|
||||
it_behaves_like "sends a mail to the member's principal" |
||||
it_behaves_like 'has a subject', :'mail_member_updated_global.subject' |
||||
it_behaves_like 'sets the expected message_id header' |
||||
it_behaves_like 'has the expected body' do |
||||
let(:expected_header) do |
||||
"#{current_user.name} updated the roles you have globally." |
||||
end |
||||
end |
||||
it_behaves_like 'fails for a group' |
||||
end |
||||
end |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue