Merge remote-tracking branch 'origin/release/11.4' into dev

pull/9610/head
Wieland Lindenthal 3 years ago
commit c0555a1b38
  1. 4
      .rubocop.yml
  2. 13
      app/models/comment.rb
  3. 2
      app/models/journal.rb
  4. 36
      app/models/journal/associated_journal.rb
  5. 2
      app/models/journal/attachable_journal.rb
  6. 6
      app/models/journal/base_journal.rb
  7. 4
      app/models/journal/customizable_journal.rb
  8. 3
      app/models/journal/message_journal.rb
  9. 2
      app/models/journal/news_journal.rb
  10. 3
      app/models/journal/wiki_content_journal.rb
  11. 15
      app/models/message.rb
  12. 15
      app/models/news.rb
  13. 7
      app/models/notification.rb
  14. 42
      app/models/notifications/scopes/recipient.rb
  15. 42
      app/models/notifications/scopes/unread_mail.rb
  16. 42
      app/models/notifications/scopes/unread_mail_digest.rb
  17. 3
      app/models/user.rb
  18. 49
      app/models/users/scopes/watcher_recipients.rb
  19. 4
      app/models/wiki_content.rb
  20. 311
      app/services/notifications/create_from_model_service.rb
  21. 43
      app/services/notifications/create_from_model_service/comment_strategy.rb
  22. 70
      app/services/notifications/create_from_model_service/message_strategy.rb
  23. 40
      app/services/notifications/create_from_model_service/news_strategy.rb
  24. 74
      app/services/notifications/create_from_model_service/wiki_content_strategy.rb
  25. 67
      app/services/notifications/create_from_model_service/work_package_strategy.rb
  26. 41
      app/services/notifications/create_service.rb
  27. 120
      app/services/notifications/journal_wiki_mail_service.rb
  28. 262
      app/services/notifications/journal_wp_notification_service.rb
  29. 77
      app/services/notifications/mail_service.rb
  30. 30
      app/services/notifications/mail_service/comment_strategy.rb
  31. 49
      app/services/notifications/mail_service/message_strategy.rb
  32. 49
      app/services/notifications/mail_service/news_strategy.rb
  33. 58
      app/services/notifications/mail_service/wiki_content_strategy.rb
  34. 48
      app/services/notifications/mail_service/work_package_strategy.rb
  35. 81
      app/workers/concerns/state_machine_job.rb
  36. 49
      app/workers/journals/completed_job.rb
  37. 21
      app/workers/mails/digest_job.rb
  38. 85
      app/workers/notifications/workflow_job.rb
  39. 28
      config/initializers/subscribe_listeners.rb
  40. 4
      lib/open_project/events.rb
  41. 7
      lib/plugins/acts_as_event/lib/acts_as_event.rb
  42. 20
      lib/plugins/acts_as_watchable/lib/acts_as_watchable.rb
  43. 41
      modules/bim/spec/seeders/demo_data_seeder_spec.rb
  44. 2
      modules/costs/app/models/time_entry.rb
  45. 15
      modules/documents/app/models/document.rb
  46. 2
      modules/documents/app/models/journal/document_journal.rb
  47. 63
      modules/documents/app/services/notifications/create_from_model_service/document_strategy.rb
  48. 48
      modules/documents/app/services/notifications/mail_service/document_strategy.rb
  49. 4
      modules/documents/spec/controllers/documents_controller_spec.rb
  50. 30
      modules/documents/spec/features/attachment_upload_spec.rb
  51. 29
      modules/documents/spec/models/document_spec.rb
  52. 162
      modules/documents/spec/services/notifications/create_from_model_service_document_spec.rb
  53. 88
      modules/documents/spec/services/notifications/mail_service_spec.rb
  54. 3
      modules/ldap_groups/spec/services/synchronization_spec.rb
  55. 9
      spec/controllers/news_controller_spec.rb
  56. 6
      spec/factories/journal_factory.rb
  57. 50
      spec/features/forums/message_spec.rb
  58. 102
      spec/features/news/creation_and_commenting_spec.rb
  59. 3
      spec/features/notifications/digest_mail_spec.rb
  60. 128
      spec/features/notifications/notification_center/notification_center_spec.rb
  61. 129
      spec/lib/acts_as_watchable/lib/acts_as_watchable_spec.rb
  62. 14
      spec/models/group_performance_spec.rb
  63. 21
      spec/models/journal_spec.rb
  64. 50
      spec/models/notifications/scopes/unread_mail_digest_spec.rb
  65. 50
      spec/models/notifications/scopes/unread_mail_spec.rb
  66. 39
      spec/models/watcher_spec.rb
  67. 6
      spec/models/work_package/openproject_notifications_spec.rb
  68. 107
      spec/models/work_package/work_package_action_mailer_spec.rb
  69. 21
      spec/models/work_package/work_package_acts_as_watchable_spec.rb
  70. 6
      spec/requests/api/v3/work_packages/update_resource_spec.rb
  71. 37
      spec/seeders/demo_data_seeder_spec.rb
  72. 129
      spec/services/notifications/create_from_journal_job_shared.rb
  73. 168
      spec/services/notifications/create_from_model_service_comment_spec.rb
  74. 364
      spec/services/notifications/create_from_model_service_message_spec.rb
  75. 109
      spec/services/notifications/create_from_model_service_news_spec.rb
  76. 315
      spec/services/notifications/create_from_model_service_wiki_spec.rb
  77. 146
      spec/services/notifications/create_from_model_service_work_package_spec.rb
  78. 132
      spec/services/notifications/create_service_spec.rb
  79. 85
      spec/services/notifications/journal_notification_service_spec.rb
  80. 240
      spec/services/notifications/journal_wiki_mail_service_spec.rb
  81. 412
      spec/services/notifications/mail_service_spec.rb
  82. 29
      spec/support/shared/acts_as_watchable.rb
  83. 158
      spec/workers/journals/completed_job_spec.rb
  84. 173
      spec/workers/mails/work_package_job_spec.rb
  85. 151
      spec/workers/notifications/journal_completed_job_integration_spec.rb
  86. 144
      spec/workers/notifications/workflow_job_spec.rb
  87. 22
      spec_legacy/unit/comment_spec.rb

@ -179,6 +179,10 @@ RSpec/LeadingSubject:
RSpec/NamedSubject:
Enabled: false
RSpec/FilePath:
Enabled: false
Style/Alias:
Enabled: false

@ -30,7 +30,7 @@
class Comment < ApplicationRecord
belongs_to :commented, polymorphic: true, counter_cache: true
belongs_to :author, class_name: 'User', foreign_key: 'author_id'
belongs_to :author, class_name: 'User'
validates :commented, :author, :comments, presence: true
@ -47,13 +47,8 @@ class Comment < ApplicationRecord
private
def send_news_comment_added_mail
return unless Setting.notified_events.include?('news_comment_added')
return unless commented.is_a?(News)
recipients = commented.recipients + commented.watcher_recipients
recipients.uniq.each do |user|
UserMailer.news_comment_added(user, self, User.current).deliver_later
end
OpenProject::Notifications.send(OpenProject::Events::NEWS_COMMENT_CREATED,
comment: self,
send_notification: true)
end
end

@ -50,6 +50,8 @@ class Journal < ApplicationRecord
has_many :attachable_journals, class_name: 'Journal::AttachableJournal', dependent: :destroy
has_many :customizable_journals, class_name: 'Journal::CustomizableJournal', dependent: :destroy
has_many :notifications, dependent: :destroy
# Scopes to all journals excluding the initial journal - useful for change
# logs like the history on issue#show
scope :changing, -> { where(['version > 1']) }

@ -0,0 +1,36 @@
#-- 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.
#++
# AssociatedJournals that belong to another journal reflecting
# an has_many relation (e.g. custom_values) on the journaled object.
class Journal::AssociatedJournal < ApplicationRecord
self.abstract_class = true
belongs_to :author, class_name: 'User'
belongs_to :journal
end

@ -28,7 +28,7 @@
# See docs/COPYRIGHT.rdoc for more details.
#++
class Journal::AttachableJournal < Journal::BaseJournal
class Journal::AttachableJournal < Journal::AssociatedJournal
self.table_name = 'attachable_journals'
belongs_to :attachment

@ -29,8 +29,8 @@
class Journal::BaseJournal < ApplicationRecord
self.abstract_class = true
belongs_to :journal
belongs_to :author, class_name: 'User', foreign_key: :author_id
has_one :journal, as: :data, inverse_of: :data, dependent: :destroy
belongs_to :author, class_name: 'User'
def journaled_attributes
attributes.symbolize_keys.select { |k, _| self.class.journaled_attributes.include? k }
@ -41,7 +41,7 @@ class Journal::BaseJournal < ApplicationRecord
end
def self.excluded_attributes
[primary_key.to_sym, inheritance_column.to_sym, :journal_id]
[primary_key.to_sym, inheritance_column.to_sym]
end
private_class_method :excluded_attributes
end

@ -28,8 +28,8 @@
# See docs/COPYRIGHT.rdoc for more details.
#++
class Journal::CustomizableJournal < Journal::BaseJournal
class Journal::CustomizableJournal < Journal::AssociatedJournal
self.table_name = 'customizable_journals'
belongs_to :custom_field, foreign_key: :custom_field_id
belongs_to :custom_field
end

@ -30,4 +30,7 @@
class Journal::MessageJournal < Journal::BaseJournal
self.table_name = 'message_journals'
belongs_to :forum
has_one :project, through: :forum
end

@ -30,4 +30,6 @@
class Journal::NewsJournal < Journal::BaseJournal
self.table_name = 'news_journals'
belongs_to :project
end

@ -30,4 +30,7 @@
class Journal::WikiContentJournal < Journal::BaseJournal
self.table_name = 'wiki_content_journals'
# The project does not change over the course of a wiki content lifetime
delegate :project, to: :journal
end

@ -67,8 +67,7 @@ class Message < ApplicationRecord
validates_length_of :subject, maximum: 255
after_create :add_author_as_watcher,
:update_last_reply_in_parent,
:send_message_posted_mail
:update_last_reply_in_parent
after_update :update_ancestors, if: :saved_change_to_forum_id?
after_destroy :reset_counters
@ -149,16 +148,4 @@ class Message < ApplicationRecord
watchers.reload
watcher_users.reload
end
def send_message_posted_mail
return unless Setting.notified_events.include?('message_posted')
to_mail = recipients +
root.watcher_recipients +
forum.watcher_recipients
to_mail.uniq.each do |user|
UserMailer.message_posted(user, self, User.current).deliver_later
end
end
end

@ -30,7 +30,7 @@
class News < ApplicationRecord
belongs_to :project
belongs_to :author, class_name: 'User', foreign_key: 'author_id'
belongs_to :author, class_name: 'User'
has_many :comments, -> {
order(:created_at)
}, as: :commented, dependent: :delete_all
@ -43,15 +43,14 @@ class News < ApplicationRecord
acts_as_event url: Proc.new { |o| { controller: '/news', action: 'show', id: o.id } }
acts_as_searchable columns: ["#{table_name}.title", "#{table_name}.summary", "#{table_name}.description"],
acts_as_searchable columns: %W[#{table_name}.title #{table_name}.summary #{table_name}.description],
include: :project,
references: :projects,
date_column: "#{table_name}.created_at"
acts_as_watchable
after_create :add_author_as_watcher,
:send_news_added_mail
after_create :add_author_as_watcher
scope :visible, ->(*args) do
includes(:project)
@ -106,12 +105,4 @@ class News < ApplicationRecord
def add_author_as_watcher
Watcher.create(watchable: self, user: author)
end
def send_news_added_mail
if Setting.notified_events.include?('news_added')
recipients.uniq.each do |user|
UserMailer.news_added(user, self, User.current).deliver_later
end
end
end
end

@ -21,8 +21,9 @@ class Notification < ApplicationRecord
belongs_to :journal
belongs_to :resource, polymorphic: true
scope :recipient, ->(user) { where(recipient_id: user.is_a?(User) ? user.id : user) }
include Scopes::Scoped
scopes :mail_digest_before
scopes :mail_digest_before,
:unread_mail,
:unread_mail_digest,
:recipient
end

@ -0,0 +1,42 @@
#-- 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.
#++
# Return mail notifications destined at the provided recipient
module Notifications::Scopes
module Recipient
extend ActiveSupport::Concern
class_methods do
def recipient(user)
where(recipient_id: user.is_a?(User) ? user.id : user)
end
end
end
end

@ -0,0 +1,42 @@
#-- 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.
#++
# Return mail notifications that are unread (have read_mail: false)
module Notifications::Scopes
module UnreadMail
extend ActiveSupport::Concern
class_methods do
def unread_mail
where(read_mail: false)
end
end
end
end

@ -0,0 +1,42 @@
#-- 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.
#++
# Return digest mail notifications that are unread (have read_digest_mail: false)
module Notifications::Scopes
module UnreadMailDigest
extend ActiveSupport::Concern
class_methods do
def unread_mail_digest
where(read_mail_digest: false)
end
end
end
end

@ -75,7 +75,8 @@ class User < Principal
scopes :find_by_login,
:newest,
:notified_on_all
:notified_on_all,
:watcher_recipients
def self.create_blocked_scope(scope, blocked)
scope.where(blocked_condition(blocked))

@ -0,0 +1,49 @@
#-- 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.
#++
# Returns a scope of users watching the instance that should be notified via whatever channel upon updates to the instance.
# The users need to have the necessary permissions to see the instance as defined by the watchable_permission.
# Additionally, the users need to have their mail notification setting set to watched: true.
module Users::Scopes
module WatcherRecipients
extend ActiveSupport::Concern
class_methods do
def watcher_recipients(model)
model
.possible_watcher_users
.where(id: NotificationSetting
.applicable(model.project)
.where(watched: true, user_id: model.watcher_users)
.select(:user_id))
end
end
end
end

@ -31,8 +31,8 @@
class WikiContent < ApplicationRecord
extend DeprecatedAlias
belongs_to :page, class_name: 'WikiPage', foreign_key: 'page_id'
belongs_to :author, class_name: 'User', foreign_key: 'author_id'
belongs_to :page, class_name: 'WikiPage'
belongs_to :author, class_name: 'User'
validates_length_of :comments, maximum: 255, allow_nil: true
attr_accessor :comments

@ -0,0 +1,311 @@
#-- 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 Notifications::CreateFromModelService
MENTION_USER_ID_PATTERN =
'<mention[^>]*(?:data-type="user"[^>]*data-id="(\d+)")|(?:data-id="(\d+)"[^>]*data-type="user")[^>]*>)|(?:\buser#(\d+)\b'
.freeze
MENTION_USER_LOGIN_PATTERN =
'\buser:"(.+?)"'.freeze
MENTION_GROUP_ID_PATTERN =
'<mention[^>]*(?:data-type="group"[^>]*data-id="(\d+)")|(?:data-id="(\d+)"[^>]*data-type="group")[^>]*>)|(?:\bgroup#(\d+)\b'
.freeze
MENTION_PATTERN = Regexp.new("(?:#{MENTION_USER_ID_PATTERN})|(?:#{MENTION_USER_LOGIN_PATTERN})|(?:#{MENTION_GROUP_ID_PATTERN})")
def initialize(model)
self.model = model
end
def call(send_notifications)
result = ServiceResult.new success: !abort_sending?(send_notifications)
return result if result.failure?
notification_receivers.each do |recipient_id, channel_reasons|
call = create_notification(recipient_id, channel_reasons)
result.add_dependent!(call)
end
result
end
private
attr_accessor :model
def create_notification(recipient_id, channel_reasons)
notification_attributes = {
recipient_id: recipient_id,
project: project,
resource: resource,
journal: journal,
actor: user_with_fallback
}.merge(channel_attributes(channel_reasons))
Notifications::CreateService
.new(user: user_with_fallback)
.call(notification_attributes)
end
def channel_attributes(channel_reasons)
channel_attributes_mail(channel_reasons)
.merge(channel_attributes_mail_digest(channel_reasons))
.merge(channel_attributes_ian(channel_reasons))
end
def channel_attributes_mail(channel_reasons)
{
read_mail: strategy.supports_mail? && channel_reasons.keys.include?('mail') ? false : nil,
reason_mail: strategy.supports_mail? && channel_reasons['mail']&.first
}
end
def channel_attributes_mail_digest(channel_reasons)
{
read_mail_digest: strategy.supports_mail_digest? && channel_reasons.keys.include?('mail_digest') ? false : nil,
reason_mail_digest: strategy.supports_mail_digest? && channel_reasons['mail_digest']&.first
}
end
def channel_attributes_ian(channel_reasons)
{
read_ian: strategy.supports_ian? && channel_reasons.keys.include?('in_app') ? false : nil,
reason_ian: strategy.supports_ian? && channel_reasons['in_app']&.first
}
end
def notification_receivers
receivers = receivers_hash
strategy.reasons.each do |reason|
add_receivers_by_reason(receivers, reason)
end
remove_self_recipient(receivers)
receivers
end
def add_receivers_by_reason(receivers, reason)
add_receiver(receivers, send(:"settings_of_#{reason}"), reason)
end
def settings_of_mentioned
applicable_settings(mentioned_ids,
project,
:mentioned)
end
def settings_of_involved
scope = User
.where(id: group_or_user_ids(journal.data.assigned_to))
.or(User.where(id: group_or_user_ids(journal.data.responsible)))
applicable_settings(scope,
project,
:involved)
end
def settings_of_subscribed
applicable_settings(strategy.subscribed_users(model),
project,
:all)
end
def settings_of_watched
applicable_settings(strategy.watcher_users(model),
project,
:watched)
end
def settings_of_commented
return NotificationSetting.none unless journal.notes?
applicable_settings(User.all,
project,
:work_package_commented)
end
def settings_of_created
return NotificationSetting.none unless journal.initial?
applicable_settings(User.all,
project,
:work_package_created)
end
def settings_of_processed
return NotificationSetting.none unless !journal.initial? && journal.details.has_key?(:status_id)
applicable_settings(User.all,
project,
:work_package_processed)
end
def settings_of_prioritized
return NotificationSetting.none unless !journal.initial? && journal.details.has_key?(:priority_id)
applicable_settings(User.all,
project,
:work_package_prioritized)
end
def settings_of_scheduled
if journal.initial? || !(journal.details.has_key?(:start_date) || journal.details.has_key?(:due_date))
return NotificationSetting.none
end
applicable_settings(User.all,
project,
:work_package_scheduled)
end
def applicable_settings(user_scope, project, reason)
NotificationSetting
.applicable(project)
.where(reason => true)
.where(user: user_scope.where(id: User.allowed(strategy.permission, project)))
end
def text_for_mentions
potential_text = ""
potential_text << journal.notes if journal.try(:notes)
%i[description subject].each do |field|
details = journal.details[field]
if details.present?
potential_text << "\n#{Redmine::Helpers::Diff.new(*details.reverse).additions.join(' ')}"
end
end
potential_text
end
def mentioned_ids
matches = mention_matches
base_scope = User
.includes(:groups)
.references(:groups_users)
by_id = base_scope.where(id: matches[:user_ids])
by_login = base_scope.where(login: matches[:user_login_names])
by_group = base_scope.where(groups_users: { id: matches[:group_ids] })
by_id
.or(by_login)
.or(by_group)
end
def send_notification?(send_notifications)
send_notifications && ::UserMailer.perform_deliveries
end
def mention_matches
text = text_for_mentions
user_ids_tag_after,
user_ids_tag_before,
user_ids_hash,
user_login_names,
group_ids_tag_after,
group_ids_tag_before,
group_ids_hash = text
.scan(MENTION_PATTERN)
.transpose
.each(&:compact!)
{
user_ids: [user_ids_tag_after, user_ids_tag_before, user_ids_hash].flatten.compact,
user_login_names: [user_login_names].flatten.compact,
group_ids: [group_ids_tag_after, group_ids_tag_before, group_ids_hash].flatten.compact
}
end
def abort_sending?(send_notifications)
!send_notification?(send_notifications) ||
model.nil? ||
!model.class.exists?(id: model.id) ||
journal&.noop? ||
!supported?
end
def group_or_user_ids(association)
association.is_a?(Group) ? association.user_ids : association&.id
end
def user_with_fallback
user || DeletedUser.first
end
def add_receiver(receivers, collection, reason)
collection.each do |notification|
receivers[notification.user_id][notification.channel] << reason
end
end
def remove_self_recipient(receivers)
receivers.delete(user_with_fallback.id) if !user_with_fallback.pref.self_notified?
end
def receivers_hash
Hash.new do |hash, user|
hash[user] = Hash.new do |channel_hash, channel|
channel_hash[channel] = []
end
end
end
def strategy
@strategy ||= if self.class.const_defined?("#{resource.class}Strategy")
"#{self.class}::#{resource.class}Strategy".constantize
end
end
def supported?
strategy.present?
end
def user
strategy.user(model)
end
def project
strategy.project(model)
end
def resource
model.is_a?(Journal) ? model.journable : model
end
def journal
model.is_a?(Journal) ? model : nil
end
end

@ -1,3 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 the OpenProject GmbH
@ -26,27 +28,40 @@
# See docs/COPYRIGHT.rdoc for more details.
#++
class Mails::NotificationJob < ApplicationJob
queue_with_priority :notification
module Notifications::CreateFromModelService::CommentStrategy
def self.reasons
%i(watched subscribed)
end
def perform(notification)
ensure_supported(notification)
def self.permission
:view_news
end
return if ian_read?(notification)
def self.supports_ian?
false
end
Mails::WorkPackageJob
.perform_now(notification.journal, notification.recipient_id, notification.actor_id)
def self.supports_mail_digest?
false
end
private
def self.supports_mail?
true
end
def self.subscribed_users(comment)
User.notified_on_all(project(comment))
end
def self.watcher_users(comment)
User.watcher_recipients(comment.commented)
end
def ensure_supported(notification)
unless notification.journal && notification.journal.journable.is_a?(WorkPackage)
raise ArgumentError, "Only notification for work package journals are currently supported"
end
def self.project(comment)
comment.commented.project
end
def ian_read?(notification)
notification.read_ian
def self.user(comment)
comment.author
end
end

@ -0,0 +1,70 @@
#-- 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.
#++
module Notifications::CreateFromModelService::MessageStrategy
def self.reasons
%i(watched subscribed)
end
def self.permission
:view_messages
end
def self.supports_ian?
false
end
def self.supports_mail_digest?
false
end
def self.supports_mail?
true
end
def self.subscribed_users(journal)
User.notified_on_all(journal.data.project)
end
def self.watcher_users(journal)
message = journal.journable
User.watcher_recipients(message.root)
.or(User.watcher_recipients(message.forum))
end
def self.project(journal)
journal.data.project
end
def self.user(journal)
journal.user
end
end

@ -28,31 +28,41 @@
# See docs/COPYRIGHT.rdoc for more details.
#++
class Mails::WorkPackageJob < Mails::DeliverJob
include Mails::WithSender
module Notifications::CreateFromModelService::NewsStrategy
def self.reasons
%i(subscribed)
end
def self.permission
:view_news
end
def perform(journal_id, recipient_id, author_id)
@journal_id = journal_id
super(recipient_id, author_id)
def self.supports_ian?
false
end
def render_mail
return nil unless journal # abort, assuming that the underlying WP was deleted
def self.supports_mail_digest?
false
end
def self.supports_mail?
true
end
def self.subscribed_users(journal)
if journal.initial?
UserMailer.work_package_added(recipient, journal, sender)
User.notified_on_all(journal.data.project)
else
UserMailer.work_package_updated(recipient, journal, sender)
# No notification on updating a news
User.none
end
end
private
def journal
@journal ||= Journal.find_by(id: @journal_id)
def self.project(journal)
journal.data.project
end
def work_package
@work_package ||= journal.journable
def self.user(journal)
journal.user
end
end

@ -0,0 +1,74 @@
#-- 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.
#++
module Notifications::CreateFromModelService::WikiContentStrategy
def self.reasons
%i(watched subscribed)
end
def self.permission
:view_wiki_pages
end
def self.supports_ian?
false
end
def self.supports_mail_digest?
false
end
def self.supports_mail?
true
end
def self.subscribed_users(journal)
User.notified_on_all(journal.data.project)
end
def self.watcher_users(journal)
page = journal.journable.page
if journal.initial?
User.watcher_recipients(page.wiki)
else
User.watcher_recipients(page.wiki)
.or(User.watcher_recipients(page))
end
end
def self.project(journal)
journal.data.project
end
def self.user(journal)
journal.user
end
end

@ -0,0 +1,67 @@
#-- 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.
#++
module Notifications::CreateFromModelService::WorkPackageStrategy
def self.reasons
%i(mentioned involved watched subscribed commented created processed prioritized scheduled)
end
def self.permission
:view_work_packages
end
def self.supports_ian?
true
end
def self.supports_mail_digest?
true
end
def self.supports_mail?
true
end
def self.subscribed_users(journal)
User.notified_on_all(journal.data.project)
end
def self.watcher_users(journal)
User.watcher_recipients(journal.journable)
end
def self.project(journal)
journal.data.project
end
def self.user(journal)
journal.user
end
end

@ -29,45 +29,4 @@
#++
class Notifications::CreateService < ::BaseServices::Create
protected
def after_perform(call)
super.tap do |super_call|
schedule_mail_jobs(super_call.result) if super_call.success?
end
end
def schedule_mail_jobs(notification)
# Explicitly checking for false here as the nil value means that no
# notification should be sent at all.
schedule_direct_mail_job(notification) if notification.read_mail == false
schedule_digest_mail_job(notification) if notification.read_mail_digest == false
end
def schedule_direct_mail_job(notification)
Mails::NotificationJob
.set(wait: Setting.notification_email_delay_minutes.minutes)
.perform_later(notification)
end
def schedule_digest_mail_job(notification)
# This alone is vulnerable to the edge case of the Mails::DigestJob
# having started but not completed when a new digest notification is generated.
# To cope with it, the Mails::DigestJob as its first action sets all digest notifications
# to being handled even though they are still processed.
# See the DigestJob for more details.
return if digest_job_already_scheduled?(notification)
Mails::DigestJob
.set(wait_until: Mails::DigestJob.execution_time(notification.recipient))
.perform_later(notification.recipient)
end
def digest_job_already_scheduled?(notification)
Notification
.mail_digest_before(recipient: notification.recipient,
time: notification.created_at)
.where.not(id: notification.id)
.exists?
end
end

@ -1,120 +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 Notifications::JournalWikiMailService
class << self
def call(journal, send_mails)
new(journal)
.call(send_mails)
end
end
attr_reader :journal
def initialize(journal)
@journal = journal
end
def call(send_mails)
return unless send_mail?(send_mails)
if journal.initial?
send_content_added_mail
else
send_content_updated_mail
end
end
private
def send_mail?(send_mails)
send_mails && ::UserMailer.perform_deliveries && !journal.noop?
end
def send_content_added_mail
send_content(create_recipients, :wiki_content_added)
end
def send_content_updated_mail
send_content(update_recipients, :wiki_content_updated)
end
def notification_disabled?(name)
!Setting.notified_events.include?(name)
end
# Returns the mail addresses of users that should be notified
def recipients
project
.notified_users
.select { |user| wiki_content.visible?(user) }
end
def send_content(recipients, method)
return if notification_disabled?(method.to_s)
recipients.uniq.each do |user|
UserMailer
.send(method, user, wiki_content, journal_user)
.deliver_later
end
end
def create_recipients
recipients +
wiki.watcher_recipients
end
def update_recipients
recipients +
wiki.watcher_recipients +
page.watcher_recipients
end
def wiki_content
journal.journable
end
def page
wiki_content.page
end
def wiki
page.wiki
end
def project
wiki.project
end
def journal_user
journal.user || DeletedUser.first
end
end

@ -1,262 +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 Notifications::JournalWpNotificationService
MENTION_USER_ID_PATTERN =
'<mention[^>]*(?:data-type="user"[^>]*data-id="(\d+)")|(?:data-id="(\d+)"[^>]*data-type="user")[^>]*>)|(?:\buser#(\d+)\b'
.freeze
MENTION_USER_LOGIN_PATTERN =
'\buser:"(.+?)"'.freeze
MENTION_GROUP_ID_PATTERN =
'<mention[^>]*(?:data-type="group"[^>]*data-id="(\d+)")|(?:data-id="(\d+)"[^>]*data-type="group")[^>]*>)|(?:\bgroup#(\d+)\b'
.freeze
MENTION_PATTERN = Regexp.new("(?:#{MENTION_USER_ID_PATTERN})|(?:#{MENTION_USER_LOGIN_PATTERN})|(?:#{MENTION_GROUP_ID_PATTERN})")
class << self
def call(journal, send_notifications)
return nil if abort_sending?(journal, send_notifications)
notification_receivers(journal).each do |recipient_id, channel_reasons|
create_notification(journal,
recipient_id,
channel_reasons)
end
end
private
def create_notification(journal, recipient_id, channel_reasons)
notification_attributes = {
recipient_id: recipient_id,
project: journal.project,
resource: journal.journable,
journal: journal,
actor: user_with_fallback(journal)
}.merge(channel_attributes(channel_reasons))
Notifications::CreateService
.new(user: user_with_fallback(journal))
.call(notification_attributes)
end
def channel_attributes(channel_reasons)
{
read_mail: channel_reasons.keys.include?('mail') ? false : nil,
read_mail_digest: channel_reasons.keys.include?('mail_digest') ? false : nil,
read_ian: channel_reasons.keys.include?('in_app') ? false : nil,
reason_ian: channel_reasons['in_app']&.first,
reason_mail: channel_reasons['mail']&.first,
reason_mail_digest: channel_reasons['mail_digest']&.first
}
end
def notification_receivers(journal)
receivers = receivers_hash
%i(mentioned involved watched subscribed commented created processed prioritized scheduled).each do |reason|
add_receivers_by_reason(receivers, journal, reason)
end
remove_self_recipient(receivers, journal)
receivers
end
def add_receivers_by_reason(receivers, journal, reason)
add_receiver(receivers, send(:"settings_of_#{reason}", journal), reason)
end
def settings_of_mentioned(journal)
applicable_settings(mentioned_ids(journal),
journal.data.project,
:mentioned)
end
def settings_of_involved(journal)
scope = User
.where(id: group_or_user_ids(journal.data.assigned_to))
.or(User.where(id: group_or_user_ids(journal.data.responsible)))
applicable_settings(scope,
journal.data.project,
:involved)
end
def settings_of_subscribed(journal)
project = journal.data.project
applicable_settings(User.notified_on_all(project),
project,
:all)
end
def settings_of_watched(journal)
work_package = journal.journable
applicable_settings(work_package.watcher_users,
work_package.project,
:watched)
end
def settings_of_commented(journal)
return NotificationSetting.none unless journal.notes?
applicable_settings(User.all,
journal.data.project,
:work_package_commented)
end
def settings_of_created(journal)
return NotificationSetting.none unless journal.initial?
applicable_settings(User.all,
journal.data.project,
:work_package_created)
end
def settings_of_processed(journal)
return NotificationSetting.none unless !journal.initial? && journal.details.has_key?(:status_id)
applicable_settings(User.all,
journal.data.project,
:work_package_processed)
end
def settings_of_prioritized(journal)
return NotificationSetting.none unless !journal.initial? && journal.details.has_key?(:priority_id)
applicable_settings(User.all,
journal.data.project,
:work_package_prioritized)
end
def settings_of_scheduled(journal)
if journal.initial? || !(journal.details.has_key?(:start_date) || journal.details.has_key?(:due_date))
return NotificationSetting.none
end
applicable_settings(User.all,
journal.data.project,
:work_package_scheduled)
end
def applicable_settings(user_scope, project, reason)
NotificationSetting
.applicable(project)
.where(reason => true)
.where(user: user_scope.where(id: User.allowed(:view_work_packages, project)))
end
def text_for_mentions(journal)
potential_text = ""
potential_text << journal.notes if journal.try(:notes)
%i[description subject].each do |field|
details = journal.details[field]
if details.present?
potential_text << "\n#{Redmine::Helpers::Diff.new(*details.reverse).additions.join(' ')}"
end
end
potential_text
end
def mentioned_ids(journal)
matches = mention_matches(journal)
base_scope = User
.includes(:groups)
.references(:groups_users)
by_id = base_scope.where(id: matches[:user_ids])
by_login = base_scope.where(login: matches[:user_login_names])
by_group = base_scope.where(groups_users: { id: matches[:group_ids] })
by_id
.or(by_login)
.or(by_group)
end
def send_notification?(journal, send_notifications)
send_notifications && ::UserMailer.perform_deliveries
end
def mention_matches(journal)
text = text_for_mentions(journal)
user_ids_tag_after,
user_ids_tag_before,
user_ids_hash,
user_login_names,
group_ids_tag_after,
group_ids_tag_before,
group_ids_hash = text
.scan(MENTION_PATTERN)
.transpose
.each(&:compact!)
{
user_ids: [user_ids_tag_after, user_ids_tag_before, user_ids_hash].flatten.compact,
user_login_names: [user_login_names].flatten.compact,
group_ids: [group_ids_tag_after, group_ids_tag_before, group_ids_hash].flatten.compact
}
end
def abort_sending?(journal, send_notifications)
!send_notification?(journal, send_notifications) || journal.noop?
end
def group_or_user_ids(association)
association.is_a?(Group) ? association.user_ids : association&.id
end
def user_with_fallback(journal)
journal.user || DeletedUser.first
end
def add_receiver(receivers, collection, reason)
collection.each do |notification|
receivers[notification.user_id][notification.channel] << reason
end
end
def remove_self_recipient(receivers, journal)
receivers.delete(journal.user_id) if receivers[journal.user_id] && !user_with_fallback(journal).pref.self_notified?
end
def receivers_hash
Hash.new do |hash, user|
hash[user] = Hash.new do |channel_hash, channel|
channel_hash[channel] = []
end
end
end
end
end

@ -0,0 +1,77 @@
#-- 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 Notifications::MailService
def initialize(notification)
self.notification = notification
end
def call
ensure_supported
return if ian_read?
strategy.send_mail(notification)
end
private
attr_accessor :notification
def ensure_supported
unless supported?
raise ArgumentError, "Sending mails for notifications is not supported for #{strategy_model}"
end
end
def ian_read?
notification.read_ian
end
def strategy
@strategy ||= if self.class.const_defined?("#{strategy_model}Strategy")
"#{self.class}::#{strategy_model}Strategy".constantize
end
end
def strategy_model
journal&.journable_type || resource&.class
end
def journal
notification.journal
end
def resource
notification.resource
end
def supported?
strategy.present?
end
end

@ -1,5 +1,3 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 the OpenProject GmbH
@ -28,26 +26,24 @@
# See docs/COPYRIGHT.rdoc for more details.
#++
class Notifications::JournalNotificationService
module Notifications::MailService::CommentStrategy
class << self
def call(journal, send_mails)
enqueue_notification(journal, send_mails) if supported?(journal)
end
private
def send_mail(notification)
return if notification_disabled?
def enqueue_notification(journal, send_mails)
Notifications::JournalCompletedJob
.set(wait_until: delivery_time)
.perform_later(journal.id, send_mails)
UserMailer
.news_comment_added(
notification.recipient,
notification.resource,
notification.resource.author || DeletedUser.first
)
.deliver_later
end
def delivery_time
Setting.journal_aggregation_time_minutes.to_i.minutes.from_now
end
private
def supported?(journal)
%w(WorkPackage WikiContent).include?(journal.journable_type)
def notification_disabled?
Setting.notified_events.exclude?('news_comment_added')
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 Notifications::MailService::MessageStrategy
class << self
def send_mail(notification)
return if notification_disabled?
UserMailer
.message_posted(
notification.recipient,
notification.resource,
notification.actor || DeletedUser.first
)
.deliver_later
end
private
def notification_disabled?
Setting.notified_events.exclude?('message_posted')
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 Notifications::MailService::NewsStrategy
class << self
def send_mail(notification)
return if notification_disabled? || !notification.journal.initial?
UserMailer
.news_added(
notification.recipient,
notification.journal.journable,
notification.journal.user || DeletedUser.first
)
.deliver_later
end
private
def notification_disabled?
Setting.notified_events.exclude?('news_added')
end
end
end

@ -0,0 +1,58 @@
#-- 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 Notifications::MailService::WikiContentStrategy
class << self
def send_mail(notification)
method = mailer_method(notification)
return if notification_disabled?(method.to_s)
UserMailer
.send(method,
notification.recipient,
notification.journal.journable,
notification.journal.user || DeletedUser.first)
.deliver_later
end
private
def mailer_method(notification)
if notification.journal.initial?
:wiki_content_added
else
:wiki_content_updated
end
end
def notification_disabled?(name)
Setting.notified_events.exclude?(name)
end
end
end

@ -0,0 +1,48 @@
#-- 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 Notifications::MailService::WorkPackageStrategy
class << self
def send_mail(notification)
journal = notification.journal
UserMailer
.send(mailer_method(notification),
notification.recipient,
journal,
notification.journal.user || DeletedUser.first)
.deliver_later
end
private
def mailer_method(notification)
notification.journal.initial? ? :work_package_added : :work_package_updated
end
end
end

@ -0,0 +1,81 @@
#-- 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.
#++
# A StateMachineJob is a job that consists of multiple steps to complete where a step needs
# to be finished before the next step is to be taken. Between each step, an amount of time may have to pass.
# A job including this concern can define step-blocks that will be executed one after another.
# A step receives the return value of the previous step-block as input. In case a waiting time is specified
# the job will reschedule itself.
module StateMachineJob
extend ActiveSupport::Concern
included do
def perform(state, *args)
results = instance_exec(*args, &states[state][:block])
to = states[state][:to]
switch_state(to, *results) if to
end
class_attribute :states,
instance_writer: false,
default: {}
# Defines a new step, that can then be executed either by starting at this step
# (by providing the step name as the first parameter of a call to #perform), or as a step
# in a chain of steps.
# @param name<Symbol> The name of the step that serves as an identifier to subsequent calls.
# @param to<Symbol, NilClass> The name of the step triggered after this step. If the value is *nil*, no subsequent
# step will be triggered.
# @param wait<Lambda> The result of the lambda dictates the amount of time the job waits before the next step is
# executed
# @param block<Proc> The code to execute as part of the step. The block will be executed in the context of the
# job instance, not the class.
def self.state(name, to: nil, wait: nil, &block)
states[name] = { to: to, wait: wait, block: block }
end
private
def switch_state(to, *results)
wait = states[to][:wait]
if wait
self
.class
.set(wait: wait.call)
.perform_later(to, *results)
else
perform(to, *results)
end
end
end
end

@ -28,15 +28,47 @@
# See docs/COPYRIGHT.rdoc for more details.
#++
class Notifications::JournalCompletedJob < ApplicationJob
class Journals::CompletedJob < ApplicationJob
queue_with_priority :notification
class << self
def schedule(journal, send_mails)
return unless supported?(journal)
set(wait_until: delivery_time)
.perform_later(journal.id, send_mails)
end
def aggregated_event(journal)
case journal.journable_type
when WikiContent.name
OpenProject::Events::AGGREGATED_WIKI_JOURNAL_READY
when WorkPackage.name
OpenProject::Events::AGGREGATED_WORK_PACKAGE_JOURNAL_READY
when News.name
OpenProject::Events::AGGREGATED_NEWS_JOURNAL_READY
when Message.name
OpenProject::Events::AGGREGATED_MESSAGE_JOURNAL_READY
end
end
private
def delivery_time
Setting.journal_aggregation_time_minutes.to_i.minutes.from_now
end
def supported?(journal)
aggregated_event(journal).present?
end
end
def perform(journal_id, send_mails)
journal = Journal.find_by(id: journal_id)
# If the WP has been deleted the journal will have been deleted, too.
# Or the journal might have been replaced
return unless journal
return if journal.nil?
notify_journal_complete(journal, send_mails)
end
@ -44,19 +76,8 @@ class Notifications::JournalCompletedJob < ApplicationJob
private
def notify_journal_complete(journal, send_mails)
OpenProject::Notifications.send(notification_event_type(journal),
OpenProject::Notifications.send(self.class.aggregated_event(journal),
journal: journal,
send_mail: send_mails)
end
def notification_event_type(journal)
case journal.journable_type
when WikiContent.name
OpenProject::Events::AGGREGATED_WIKI_JOURNAL_READY
when WorkPackage.name
OpenProject::Events::AGGREGATED_WORK_PACKAGE_JOURNAL_READY
else
raise 'Unsupported journal created event type'
end
end
end

@ -30,11 +30,32 @@
class Mails::DigestJob < Mails::DeliverJob
class << self
def schedule(notification)
# This alone is vulnerable to the edge case of the Mails::DigestJob
# having started but not completed when a new digest notification is generated.
# To cope with it, the Mails::DigestJob as its first action sets all digest notifications
# to being handled even though they are still processed.
return if digest_job_already_scheduled?(notification)
set(wait_until: execution_time(notification.recipient))
.perform_later(notification.recipient)
end
private
def execution_time(user)
zone = (user.time_zone || ActiveSupport::TimeZone.new('UTC'))
zone.parse(Setting.notification_email_digest_time) + 1.day
end
def digest_job_already_scheduled?(notification)
Notification
.mail_digest_before(recipient: notification.recipient,
time: notification.created_at)
.where.not(id: notification.id)
.exists?
end
end
private

@ -0,0 +1,85 @@
#-- 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.
#++
# Governs the workflow of how journals are passed through:
# 1) The notifications for any event (e.g. journal creation) is to be created as fast as possible
# so that it becomes visible as an in app notification. If the resource passed in is indeed a journal,
# it might get replaced later on (by a subsequent journal). This will lead to notifications being removed.
# 2) After the journal aggregation time has passed as well as the desired delay, the direct email is sent out.
# 3) At the same time (TODO: but it could already have been triggered after the aggregation time has passed)
# the digest is scheduled.
# This order has to be kept to ensure that the notifications are created before email sending is attempted. If it weren't
# guaranteed, with the notifications created in one job and the mails send in another, the mail sending job might get executed
# without any notifications being created which would result in no emails being sent at all. An alternative would be to
# decouple notification creation and mail sending from another. But then, in app notifications being read could not prevent
# mails being sent out.
class Notifications::WorkflowJob < ApplicationJob
include ::StateMachineJob
queue_with_priority :notification
# In case a resource (e.g. journal) cannot be deserialized (which means fetching it from the db)
# the resource has been removed which might happen. In that case, no notifications
# need to be sent out any more.
discard_on ActiveJob::DeserializationError
state :create_notifications,
to: :send_mails do |resource, send_notification|
Notifications::CreateFromModelService
.new(resource)
.call(send_notification)
.all_results
.map(&:id)
end
state :send_mails,
wait: -> {
Setting.notification_email_delay_minutes.minutes + Setting.journal_aggregation_time_minutes.to_i.minutes
} do |*notification_ids|
next unless notification_ids
Notification
.where(id: notification_ids)
.unread_mail
.each do |notification|
Notifications::MailService
.new(notification)
.call
end
Notification
.where(id: notification_ids)
.unread_mail_digest
.each do |notification|
Mails::DigestJob
.schedule(notification)
end
end
end

@ -29,18 +29,17 @@
#++
OpenProject::Notifications.subscribe(OpenProject::Events::JOURNAL_CREATED) do |payload|
Notifications::JournalNotificationService.call(payload[:journal], payload[:send_notification])
end
# The aggregated journal ready listeners in effect are run inside a delayed job
# since they are called by (in effect) by a background job triggered within the
# Notifications::JournalNotificationService
OpenProject::Notifications.subscribe(OpenProject::Events::AGGREGATED_WORK_PACKAGE_JOURNAL_READY) do |payload|
Notifications::JournalWpNotificationService.call(payload[:journal], payload[:send_mail])
end
# A job is scheduled that creates notifications (in app if supported) right away and schedules
# jobs to be run for mail and digest mails.
Notifications::WorkflowJob
.perform_later(:create_notifications,
payload[:journal],
payload[:send_notification])
OpenProject::Notifications.subscribe(OpenProject::Events::AGGREGATED_WIKI_JOURNAL_READY) do |payload|
Notifications::JournalWikiMailService.call(payload[:journal], payload[:send_mail])
# A job is scheduled for the end of the journal aggregation time. If the journal does still exist
# at the end (it might be replaced because another journal was created within that timeframe)
# that job generates a OpenProject::Events::AGGREGATED_..._JOURNAL_READY event.
Journals::CompletedJob.schedule(payload[:journal], payload[:send_notification])
end
OpenProject::Notifications.subscribe(OpenProject::Events::WATCHER_ADDED) do |payload|
@ -70,3 +69,10 @@ OpenProject::Notifications.subscribe(OpenProject::Events::MEMBER_UPDATED) do |pa
member: payload[:member],
message: payload[:message])
end
OpenProject::Notifications.subscribe(OpenProject::Events::NEWS_COMMENT_CREATED) do |payload|
Notifications::WorkflowJob
.perform_later(:create_notifications,
payload[:comment],
payload[:send_notification])
end

@ -39,6 +39,8 @@ module OpenProject
module Events
AGGREGATED_WORK_PACKAGE_JOURNAL_READY = "aggregated_work_package_journal_ready".freeze
AGGREGATED_WIKI_JOURNAL_READY = "aggregated_wiki_journal_ready".freeze
AGGREGATED_NEWS_JOURNAL_READY = "aggregated_news_journal_ready".freeze
AGGREGATED_MESSAGE_JOURNAL_READY = "aggregated_message_journal_ready".freeze
ATTACHMENT_CREATED = 'attachment_created'.freeze
@ -51,6 +53,8 @@ module OpenProject
TIME_ENTRY_CREATED = "time_entry_created".freeze
NEWS_COMMENT_CREATED = "news_comment_created".freeze
PROJECT_CREATED = "project_created".freeze
PROJECT_UPDATED = "project_updated".freeze
PROJECT_RENAMED = "project_renamed".freeze

@ -97,13 +97,6 @@ module Redmine
end
end
# Returns users that should be notified
def recipients
notified = []
notified = project.notified_users if project
notified.select { |user| visible?(user) }
end
module ClassMethods
end
end

@ -161,26 +161,6 @@ module Redmine
watcher_user_ids.any? { |uid| uid == user.id })
end
# Returns a scope of users watching the instance that should be notified via mail upon updates to the instance.
# The users need to have the necessary permissions to see the instance as defined by the watchable_permission.
# Additionally, the users need to have their mail notification setting set to watched: true or all: true.
def watcher_recipients
possible_watcher_users
.where(id: watcher_users_with_active_notification)
end
protected
# Ensure that only watcher users with mail=all notification or mail=watched are returned here.
def watcher_users_with_active_notification
mail_settings = NotificationSetting.applicable(project).mail
mail_settings
.where(all: true, user_id: watcher_users)
.or(mail_settings.where(watched: true, user_id: watcher_users))
.select(:user_id)
end
module ClassMethods
def acts_as_watchable_permission
acts_as_watchable_options[:permission] || "view_#{name.underscore.pluralize}".to_sym

@ -30,29 +30,28 @@
require 'spec_helper'
describe 'seeds' do
context 'BIM edition', with_config: { edition: 'bim' } do
it 'create the demo data' do
expect { ::Bim::BasicDataSeeder.new.seed! }.not_to raise_error
expect { AdminUserSeeder.new.seed! }.not_to raise_error
expect { DemoDataSeeder.new.seed! }.not_to raise_error
describe RootSeeder,
'BIM edition',
with_config: { edition: 'bim' },
with_settings: { journal_aggregation_time_minutes: 0 } do
it 'create the demo data' do
expect { described_class.new.do_seed! }.not_to raise_error
expect(User.not_builtin.where(admin: true).count).to eq 1
expect(Project.count).to eq 4
expect(WorkPackage.count).to eq 76
expect(Wiki.count).to eq 3
expect(Query.count).to eq 25
expect(Group.count).to eq 8
expect(Type.count).to eq 7
expect(Status.count).to eq 4
expect(IssuePriority.count).to eq 4
expect(Projects::Status.count).to eq 4
expect(Bim::IfcModels::IfcModel.count).to eq 3
expect(User.not_builtin.where(admin: true).count).to eq 1
expect(Project.count).to eq 4
expect(WorkPackage.count).to eq 76
expect(Wiki.count).to eq 3
expect(Query.count).to eq 25
expect(Group.count).to eq 8
expect(Type.count).to eq 7
expect(Status.count).to eq 4
expect(IssuePriority.count).to eq 4
expect(Projects::Status.count).to eq 4
expect(Bim::IfcModels::IfcModel.count).to eq 3
perform_enqueued_jobs
perform_enqueued_jobs
expect(ActionMailer::Base.deliveries)
.to be_empty
end
expect(ActionMailer::Base.deliveries)
.to be_empty
end
end

@ -34,7 +34,7 @@ class TimeEntry < ApplicationRecord
belongs_to :project
belongs_to :work_package
belongs_to :user
belongs_to :activity, class_name: 'TimeEntryActivity', foreign_key: 'activity_id'
belongs_to :activity, class_name: 'TimeEntryActivity'
belongs_to :rate, -> { where(type: %w[HourlyRate DefaultHourlyRate]) }, class_name: 'Rate'
acts_as_customizable

@ -30,7 +30,7 @@
class Document < ApplicationRecord
belongs_to :project
belongs_to :category, class_name: "DocumentCategory", foreign_key: "category_id"
belongs_to :category, class_name: "DocumentCategory"
acts_as_attachable delete_permission: :manage_documents,
add_permission: :manage_documents
@ -62,7 +62,6 @@ class Document < ApplicationRecord
}
after_initialize :set_default_category
after_create :notify_document_created
def visible?(user = User.current)
!user.nil? && user.allowed_to?(:view_documents, project)
@ -82,16 +81,4 @@ class Document < ApplicationRecord
@updated_at = nil
super
end
private
def notify_document_created
return unless Setting.notified_events.include?('document_added')
recipients.each do |user|
next if user == User.current && !User.current.pref.self_notified?
DocumentsMailer.document_added(user, self).deliver_now
end
end
end

@ -30,4 +30,6 @@
class Journal::DocumentJournal < Journal::BaseJournal
self.table_name = "document_journals"
belongs_to :project
end

@ -0,0 +1,63 @@
#-- 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.
#++
module Notifications::CreateFromModelService::DocumentStrategy
def self.reasons
%i(subscribed)
end
def self.permission
:view_documents
end
def self.supports_ian?
false
end
def self.supports_mail_digest?
false
end
def self.supports_mail?
true
end
def self.subscribed_users(journal)
User.notified_on_all(project(journal))
end
def self.project(journal)
journal.data.project
end
def self.user(journal)
journal.user
end
end

@ -0,0 +1,48 @@
#-- 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 Notifications::MailService::DocumentStrategy
class << self
def send_mail(notification)
return if notification_disabled? || !notification.journal.initial?
DocumentsMailer
.document_added(
notification.recipient,
notification.resource
)
.deliver_later
end
private
def notification_disabled?
Setting.notified_events.exclude?('document_added')
end
end
end

@ -154,10 +154,6 @@ describe DocumentsController do
it "should redirect to the documents-page" do
expect(response).to redirect_to project_documents_path(project.identifier)
end
it "should send out mails with notifications to members of the project with :view_documents-permission" do
expect(ActionMailer::Base.deliveries.size).to eql 1
end
end
end

@ -29,13 +29,23 @@
require 'spec_helper'
require 'features/page_objects/notification'
describe 'Upload attachment to documents', js: true do
describe 'Upload attachment to documents',
js: true,
with_settings: {
journal_aggregation_time_minutes: 0,
notified_events: %w(document_added)
} do
let!(:user) do
FactoryBot.create :user,
member_in_project: project,
member_with_permissions: %i[view_documents
manage_documents]
end
let!(:other_user) do
FactoryBot.create :user,
member_in_project: project,
member_with_permissions: %i[view_documents]
end
let!(:category) do
FactoryBot.create(:document_category)
end
@ -61,7 +71,9 @@ describe 'Upload attachment to documents', js: true do
editor.drag_attachment image_fixture.path, 'Image uploaded on creation'
expect(page).to have_selector('attachment-list-item', text: 'image.png')
click_on 'Create'
perform_enqueued_jobs do
click_on 'Create'
end
# Expect it to be present on the index page
expect(page).to have_selector('.document-category-elements--header', text: 'New documentation')
@ -88,13 +100,25 @@ describe 'Upload attachment to documents', js: true do
editor.drag_attachment image_fixture.path, 'Image uploaded the second time'
expect(page).to have_selector('attachment-list-item', text: 'image.png', count: 2)
click_on 'Save'
perform_enqueued_jobs do
click_on 'Save'
end
# Expect both images to be present on the show page
expect(page).to have_selector('#content img', count: 2)
expect(page).to have_content('Image uploaded on creation')
expect(page).to have_content('Image uploaded the second time')
expect(page).to have_selector('attachment-list-item', text: 'image.png', count: 2)
# Expect a mail to be sent to the user having subscribed to all notifications
expect(ActionMailer::Base.deliveries.size)
.to be 1
expect(ActionMailer::Base.deliveries.last.to)
.to match_array [other_user.mail]
expect(ActionMailer::Base.deliveries.last.subject)
.to include 'New documentation'
end
end

@ -54,24 +54,6 @@ describe Document do
end.to change { Document.count }.by 1
end
it "should send out email-notifications" do
allow(valid_document).to receive(:recipients).and_return([user])
Setting.notified_events = Setting.notified_events << 'document_added'
expect do
valid_document.save
end.to change { ActionMailer::Base.deliveries.size }.by 1
end
it "should send notifications to the recipients of the project" do
allow(project).to receive(:notified_users).and_return([admin])
document = FactoryBot.create(:document, project: project)
expect(document.recipients).not_to be_empty
expect(document.recipients.count).to eql 1
expect(document.recipients.map(&:mail)).to include admin.mail
end
it "should set a default-category, if none is given" do
default_category = FactoryBot.create :document_category, name: 'Technical documentation', is_default: true
document = Document.new(project: project, title: "New Document")
@ -112,15 +94,4 @@ describe Document do
it { expect(document.event_datetime.to_i).to eq(now.to_i) }
end
it "calls the DocumentsMailer, when a new document has been added" do
document = FactoryBot.build(:document)
# make sure, that we have actually someone to notify
allow(document).to receive(:recipients).and_return([user])
# ... and notifies are actually sent out
Setting.notified_events = Setting.notified_events << 'document_added'
expect(DocumentsMailer).to receive(:document_added).and_return(mail)
document.save
end
end

@ -0,0 +1,162 @@
#-- 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'
require Rails.root.join('spec/services/notifications/create_from_journal_job_shared')
describe Notifications::CreateFromModelService, 'document', with_settings: { journal_aggregation_time_minutes: 0 } do
subject(:call) do
described_class.new(journal).call(send_notifications)
end
include_context 'with CreateFromJournalJob context'
shared_let(:project) { FactoryBot.create(:project) }
let(:permissions) { [:view_documents] }
let(:send_notifications) { true }
let(:resource) do
FactoryBot.create(:document,
project: project)
end
let(:journal) { resource.journals.last }
let(:author) { other_user }
current_user { other_user }
before do
recipient
end
describe '#perform' do
context 'with a newly created document' do
context 'with the user having registered for all notifications' do
it_behaves_like 'creates notification' do
let(:notification_channel_reasons) do
{
read_ian: nil,
reason_ian: false,
read_mail: false,
reason_mail: :subscribed,
read_mail_digest: nil,
reason_mail_digest: false
}
end
end
end
context 'with the user having registered for involved notifications' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false.merge(involved: true)),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false.merge(involved: true)),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false.merge(involved: true))
]
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for no notifications' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false)
]
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for all notifications but lacking permissions' do
before do
recipient.members.destroy_all
end
it_behaves_like 'creates no notification'
end
end
context 'with an updated document' do
before do
resource.title = 'A new subject'
resource.save!
end
context 'with the user having registered for all notifications' do
it_behaves_like 'creates notification' do
let(:notification_channel_reasons) do
{
read_ian: nil,
reason_ian: false,
read_mail: false,
reason_mail: :subscribed,
read_mail_digest: nil,
reason_mail_digest: false
}
end
end
end
context 'with the user having registered for involved notifications' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false.merge(involved: true)),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false.merge(involved: true)),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false.merge(involved: true))
]
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for no notifications' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false)
]
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for all notifications but lacking permissions' do
before do
recipient.members.destroy_all
end
it_behaves_like 'creates no notification'
end
end
end
end

@ -30,8 +30,8 @@
require 'spec_helper'
describe Mails::NotificationJob, type: :model do
subject(:job) { instance.perform(notification) }
describe Notifications::MailService, type: :model do
subject(:call) { instance.call }
let(:recipient) do
FactoryBot.build_stubbed(:user)
@ -39,31 +39,67 @@ describe Mails::NotificationJob, type: :model do
let(:actor) do
FactoryBot.build_stubbed(:user)
end
let(:instance) { described_class.new }
let(:instance) { described_class.new(notification) }
context 'with a work package journal notification' do
let(:journal) { FactoryBot.build_stubbed(:work_package_journal) }
context 'with a document journal notification' do
let(:journal) do
FactoryBot.build_stubbed(:journal,
journable: FactoryBot.build_stubbed(:document)).tap do |j|
allow(j)
.to receive(:initial?)
.and_return(initial_journal)
end
end
let(:read_ian) { false }
let(:notification) do
FactoryBot.build_stubbed(:notification,
journal: journal,
resource: journal.journable,
recipient: recipient,
actor: actor,
read_ian: read_ian)
end
let(:notification_setting) { %w(document_added) }
let(:mail) do
mail = instance_double(ActionMailer::MessageDelivery)
allow(DocumentsMailer)
.to receive(:document_added)
.and_return(mail)
allow(mail)
.to receive(:deliver_later)
mail
end
let(:initial_journal) { true }
before do
allow(Mails::WorkPackageJob)
.to receive(:perform_now)
mail
allow(Setting).to receive(:notified_events).and_return(notification_setting)
end
it 'sends a mail' do
call
expect(DocumentsMailer)
.to have_received(:document_added)
.with(recipient,
journal.journable)
expect(mail)
.to have_received(:deliver_later)
end
context 'with the notification not read in app already' do
it 'sends a mail' do
job
context 'with the event being disabled' do
let(:notification_setting) { %w(wiki_content_updated) }
expect(Mails::WorkPackageJob)
.to have_received(:perform_now)
.with(notification.journal, notification.recipient_id, notification.actor_id)
it 'sends no mail' do
call
expect(DocumentsMailer)
.not_to have_received(:document_added)
end
end
@ -71,26 +107,22 @@ describe Mails::NotificationJob, type: :model do
let(:read_ian) { true }
it 'sends no mail' do
job
call
expect(Mails::WorkPackageJob)
.not_to have_received(:perform_now)
expect(DocumentsMailer)
.not_to have_received(:document_added)
end
end
end
context 'with a different journal notification' do
let(:journal) { FactoryBot.build_stubbed(:message_journal) }
let(:notification) do
FactoryBot.build_stubbed(:notification,
journal: journal,
recipient: recipient,
actor: actor)
end
context 'with the journal not being the initial one' do
let(:initial_journal) { false }
it 'raises an error' do
expect { job }
.to raise_error(ArgumentError)
it 'sends no mail' do
call
expect(DocumentsMailer)
.not_to have_received(:document_added)
end
end
end
end

@ -162,7 +162,8 @@ describe LdapGroups::SynchronizeGroupsService, with_ee: %i[ldap_groups] do
group_foo.destroy
expect { synced_foo.reload }.to raise_error ActiveRecord::RecordNotFound
expect(group_bar.users).to match_array([user_aa729, user_bb459, user_cc414])
expect(group_bar.users)
.to match_array([user_aa729.reload, user_bb459.reload, user_cc414.reload])
expect(::LdapGroups::Membership.where(group_id: synced_foo_id)).to be_empty
end

@ -103,9 +103,8 @@ describe NewsController, type: :controller do
end
describe '#create' do
context 'with news_added notifications',
with_settings: { notified_events: %w(news_added) } do
it 'persists a news item and delivers email notifications' do
context 'with news_added notifications' do
it 'persists a news item' do
become_member_with_permissions(project, user)
post :create,
@ -124,10 +123,6 @@ describe NewsController, type: :controller do
expect(news.description).to eq 'This is the description'
expect(news.author).to eq user
expect(news.project).to eq project
perform_enqueued_jobs
expect(ActionMailer::Base.deliveries.size).to eq(1)
end
end

@ -53,5 +53,11 @@ FactoryBot.define do
activity_type { 'messages' }
data { FactoryBot.build(:journal_message_journal) }
end
factory :news_journal, class: 'Journal' do
journable_type { 'News' }
activity_type { 'news' }
data { FactoryBot.build(:journal_message_journal) }
end
end
end

@ -29,27 +29,34 @@
require 'spec_helper'
describe 'messages', type: :feature, js: true do
let(:forum) { FactoryBot.create(:forum) }
let(:forum) do
FactoryBot.create(:forum)
end
let(:user) do
FactoryBot.create :user,
member_in_project: forum.project,
member_through_role: role
member_through_role: role,
notification_settings: [FactoryBot.build(:notification_setting, all: false)]
end
let(:other_user) do
FactoryBot.create :user,
FactoryBot.create(:user,
member_in_project: forum.project,
member_through_role: role
member_through_role: role,
notification_settings: [FactoryBot.build(:notification_setting, all: false)]).tap do |u|
forum.watcher_users << u
end
end
let(:role) { FactoryBot.create(:role, permissions: [:add_messages]) }
let(:index_page) { Pages::Messages::Index.new(forum.project) }
before do
other_user
login_as user
end
scenario 'adding, checking replies, replying' do
it 'adding, checking replies, replying' do
index_page.visit!
click_link forum.name
@ -63,7 +70,10 @@ describe 'messages', type: :feature, js: true do
SeleniumHubWaiter.wait
create_page.add_text 'There is no message here'
show_page = create_page.click_save
show_page = perform_enqueued_jobs do
create_page.click_save
end
show_page.expect_current_path
@ -75,13 +85,29 @@ describe 'messages', type: :feature, js: true do
index_page.expect_listed(subject: 'The message is',
replies: 0)
# Register as a watcher to later on get mails
click_link 'Watch'
# Creating a message will have sent a mail to the other user who was already watching the forum
expect(ActionMailer::Base.deliveries.size)
.to be 1
expect(ActionMailer::Base.deliveries.last.to)
.to match_array [other_user.mail]
expect(ActionMailer::Base.deliveries.last.subject)
.to include 'The message is'
# Replying as other user
login_as other_user
show_page.visit!
show_page.expect_no_replies
reply = show_page.reply 'But, but there should be one'
reply = perform_enqueued_jobs do
show_page.reply 'But, but there should be one'
end
show_page.expect_current_path(reply)
show_page.expect_num_replies(1)
@ -96,6 +122,16 @@ describe 'messages', type: :feature, js: true do
replies: 1,
last_message: 'RE: The message is')
# Creating a reply will have sent a mail to the first user who was watching the forum
expect(ActionMailer::Base.deliveries.size)
.to be 2
expect(ActionMailer::Base.deliveries.last.to)
.to match_array [user.mail]
expect(ActionMailer::Base.deliveries.last.subject)
.to include 'RE: The message is'
# Quoting as first user again
login_as user

@ -0,0 +1,102 @@
#-- 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 'News creation and commenting', type: :feature, js: true do
let(:project) { FactoryBot.create(:project) }
let!(:other_user) do
FactoryBot.create(:user,
member_in_project: project,
member_with_permissions: %i[])
end
current_user do
FactoryBot.create(:user,
member_in_project: project,
member_with_permissions: %i[manage_news comment_news])
end
it 'allows creating new and commenting it all of which will result in notifications and mails' do
visit project_news_index_path(project)
within '.toolbar-items' do
click_link 'News'
end
# Create the news
fill_in 'Title', with: 'My new news'
fill_in 'Summary', with: 'The news summary'
perform_enqueued_jobs do
click_button 'Create'
end
# The new news is visible on the index page
expect(page)
.to have_link('My new news')
expect(page)
.to have_content 'The news summary'
# Creating the news will have sent out mails
expect(ActionMailer::Base.deliveries.size)
.to be 1
expect(ActionMailer::Base.deliveries.last.to)
.to match_array [other_user.mail]
expect(ActionMailer::Base.deliveries.last.subject)
.to include 'My new news'
click_link 'My new news'
comment_editor = ::Components::WysiwygEditor.new
comment_editor.set_markdown "A new **text**"
perform_enqueued_jobs do
click_button 'Add comment'
end
# The new comment is visible on the show page
expect(page)
.to have_content "A new text"
# Creating the news comment will have sent out mails
expect(ActionMailer::Base.deliveries.size)
.to be 2
expect(ActionMailer::Base.deliveries.last.to)
.to match_array [other_user.mail]
expect(ActionMailer::Base.deliveries.last.subject)
.to include 'My new news'
end
end

@ -125,8 +125,7 @@ describe "Digest email", type: :feature, js: true do
# perform_enqueued_jobs block, the digest job would be executed right away
# so that the second update would trigger a new digest. But we want to test
# that only one digest is sent out
perform_enqueued_jobs
perform_enqueued_jobs
5.times { perform_enqueued_jobs }
expect(ActionMailer::Base.deliveries.length)
.to be 1

@ -1,46 +1,63 @@
require 'spec_helper'
require 'support/components/notifications/center'
describe "Notification center", type: :feature, js: true do
shared_let(:project) { FactoryBot.create :project }
shared_let(:work_package) { FactoryBot.create :work_package, project: project }
shared_let(:second_work_package) { FactoryBot.create :work_package, project: project }
shared_let(:recipient) do
describe "Notification center", type: :feature, js: true, with_settings: { journal_aggregation_time_minutes: 0 } do
# Notice that the setup in this file here is not following the normal rules as
# it also tests notification creation.
let!(:project) { FactoryBot.create :project }
let!(:recipient) do
# Needs to take place before the work package is created so that the notification listener is set up
FactoryBot.create :user,
member_in_project: project,
member_with_permissions: %i[view_work_packages]
end
shared_let(:notification) do
FactoryBot.create :notification,
recipient: recipient,
project: project,
resource: work_package,
journal: work_package.journals.last
let!(:other_user) do
FactoryBot.create(:user)
end
shared_let(:second_notification) do
FactoryBot.create :notification,
recipient: recipient,
project: project,
resource: second_work_package,
journal: work_package.journals.last
let(:work_package) do
FactoryBot.create :work_package, project: project, author: other_user
end
let(:work_package2) do
FactoryBot.create :work_package, project: project, author: other_user
end
let(:notification) do
# Will have been created via the JOURNAL_CREATED event listeners
work_package.journals.first.notifications.first
end
let(:notification2) do
# Will have been created via the JOURNAL_CREATED event listeners
work_package2.journals.first.notifications.first
end
let(:center) { ::Components::Notifications::Center.new }
let(:activity_tab) { ::Components::WorkPackages::Activities.new(work_package) }
let(:split_screen) { ::Pages::SplitWorkPackage.new work_package }
let(:notifications) do
[notification, notification2]
end
before do
# The notifications need to be created as a different user
# as they are otherwise swallowed to avoid self notification.
User.execute_as(other_user) do
perform_enqueued_jobs do
notifications
end
end
end
describe 'notification for a new journal' do
current_user { recipient }
it 'will not show all details of the journal' do
allow(notification.journal).to receive(:initial?).and_return true
visit home_path
center.expect_bell_count 2
center.open
center.expect_work_package_item notification
center.expect_work_package_item second_notification
center.expect_work_package_item notification2
center.click_item notification
split_screen.expect_open
@ -57,7 +74,7 @@ describe "Notification center", type: :feature, js: true do
center.open
center.expect_work_package_item notification
center.expect_work_package_item second_notification
center.expect_work_package_item notification2
center.mark_all_read
center.expect_bell_count 0
@ -65,7 +82,7 @@ describe "Notification center", type: :feature, js: true do
expect(notification.read_ian).to be_truthy
center.expect_no_item notification
center.expect_no_item second_notification
center.expect_no_item notification2
end
it 'can open the split screen of the notification' do
@ -75,8 +92,9 @@ describe "Notification center", type: :feature, js: true do
center.click_item notification
split_screen.expect_open
center.expect_item_not_read notification
center.expect_work_package_item second_notification
center.expect_work_package_item notification2
center.mark_notification_as_read notification
@ -90,26 +108,31 @@ describe "Notification center", type: :feature, js: true do
center.open
center.expect_no_item notification
center.expect_work_package_item second_notification
center.expect_work_package_item notification2
end
context 'with multiple notifications per work package' do
# In this context we have four notifications for two work packages.
shared_let(:third_notification) do
FactoryBot.create :notification,
recipient: recipient,
project: project,
resource: second_work_package,
journal: work_package.journals.last
let(:notification3) do
work_package2.journal_notes = 'A new notification is created here on wp 2'
work_package2.save!
# Will have been created via the JOURNAL_CREATED event listeners
work_package2.journals.last.notifications.first
end
shared_let(:fourth_notification) do
FactoryBot.create :notification,
recipient: recipient,
project: project,
resource: work_package,
journal: work_package.journals.last
let(:notification4) do
work_package.subject = 'Changing the subject'
work_package.save!
# Will have been created via the JOURNAL_CREATED event listeners
work_package.journals.last.notifications.first
end
let(:split_screen2) { ::Pages::SplitWorkPackage.new work_package2 }
let(:notifications) do
[notification, notification2, notification3, notification4]
end
let(:second_split_screen) { ::Pages::SplitWorkPackage.new second_work_package }
it 'aggregates notifications per work package and sets all as read when opened' do
visit home_path
@ -119,28 +142,35 @@ describe "Notification center", type: :feature, js: true do
center.expect_number_of_notifications 2
# Click on first list item, which should be the youngest notification
center.click_item fourth_notification
center.click_item notification4
split_screen.expect_open
center.mark_notification_as_read fourth_notification
center.mark_notification_as_read notification4
retry_block do
notification.reload
raise "Expected notification to be marked read" unless notification.read_ian
notification4.reload
raise "Expected notification to be marked read" unless notification4.read_ian
end
expect(second_notification.reload.read_ian).to be_falsey
expect(third_notification.reload.read_ian).to be_falsey
expect(fourth_notification.reload.read_ian).to be_truthy
expect(notification.reload.read_ian).to be_truthy
expect(notification2.reload.read_ian).to be_falsey
expect(notification3.reload.read_ian).to be_falsey
expect(notification4.reload.read_ian).to be_truthy
# Click on second list item, which should be the youngest notification that does
# not belong to the work package that represents the first list item.
center.click_item third_notification
center.click_item notification3
split_screen2.expect_open
center.mark_notification_as_read notification3
retry_block do
notification3.reload
raise "Expected notification to be marked read" unless notification3.read_ian
end
second_split_screen.expect_open
center.mark_notification_as_read third_notification
expect(second_notification.reload.read_ian).to be_truthy
expect(third_notification.reload.read_ian).to be_truthy
expect(notification2.reload.read_ian).to be_truthy
expect(notification3.reload.read_ian).to be_truthy
end
end
end

@ -1,129 +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.
#++
require 'spec_helper'
describe 'acts_as_watchable including model (e.g. WikiPage)', type: :model do
let(:klass) { WikiPage }
let(:project) { wiki.project }
let(:wiki) { FactoryBot.create(:wiki) }
let(:instance) { FactoryBot.create(:wiki_page, wiki: wiki) }
describe '#watcher_recipients' do
subject(:watcher_recipients) do
instance.watcher_recipients
end
let(:watcher_all_notifications) do
FactoryBot.create(:watcher,
watchable: instance,
user: watcher_user_all_notifications)
end
let(:watcher_user_all_notifications) do
FactoryBot.create(:user,
member_in_project: project,
member_with_permissions: %i(view_wiki_pages))
end
let(:watcher_watched_notifications) do
FactoryBot.create(:watcher,
watchable: instance,
user: watcher_user_watched_notifications)
end
let(:watcher_user_watched_notifications) do
FactoryBot.create(:user,
member_in_project: project,
member_with_permissions: %i(view_wiki_pages)).tap do |user|
user.notification_settings.mail.update_all(involved: false,
mentioned: false,
watched: true,
all: false)
end
end
let(:watcher_no_notifications) do
FactoryBot.create(:watcher,
watchable: instance,
user: watcher_user_no_notifications)
end
let(:watcher_user_no_notifications) do
FactoryBot.create(:user,
member_in_project: project,
member_with_permissions: %i(view_wiki_pages)).tap do |user|
user.notification_settings.mail.update_all(involved: false,
mentioned: false,
watched: false,
all: false)
end
end
let(:watcher_no_permission) do
FactoryBot.create(:watcher,
:skip_validate,
watchable: instance,
user: watcher_user_no_permission)
end
let(:watcher_user_no_permission) do
FactoryBot.create(:user,
member_in_project: project,
member_with_permissions: %i())
end
let(:watcher_locked) do
FactoryBot.create(:watcher,
:skip_validate,
watchable: instance,
user: watcher_user_no_permission)
end
let(:watcher_user_locked) do
FactoryBot.create(:locked_user,
member_in_project: project,
member_with_permissions: %i(view_wiki_pages))
end
let(:non_watcher_user_all_notifications) do
FactoryBot.create(:user,
member_in_project: project,
member_with_permissions: %i(view_wiki_pages))
end
before do
watcher_all_notifications
watcher_watched_notifications
watcher_no_notifications
watcher_no_permission
non_watcher_user_all_notifications
end
it 'includes users watching the instance and having notification settings and permissions' do
expect(watcher_recipients)
.to match_array([watcher_user_all_notifications, watcher_user_watched_notifications])
end
end
end

@ -75,22 +75,20 @@ describe Group, type: :model do
allow(::OpenProject::Notifications)
.to receive(:send)
puts "Destroying group ..."
start = Time.now.to_i
Groups::DeleteService
.new(user: User.system, contract_class: EmptyContract, model: group)
.call
perform_enqueued_jobs
perform_enqueued_jobs do
Groups::DeleteService
.new(user: User.system, contract_class: EmptyContract, model: group)
.call
end
@seconds = Time.now.to_i - start
puts "Destroyed group in #{@seconds} seconds"
expect(@seconds < 10).to eq true
end
it 'should reassign the work package to nobody and clean up the journals' do
it 'reassigns the work package to nobody and cleans up the journals' do
expect(::OpenProject::Notifications)
.to have_received(:send)
.with(OpenProject::Events::MEMBER_DESTROYED, any_args)

@ -37,4 +37,25 @@ describe Journal,
.to be_nil
end
end
describe '#notifications' do
let(:work_package) { FactoryBot.create(:work_package) }
let(:journal) { work_package.journals.first }
let!(:notification) do
FactoryBot.create(:notification,
journal: journal,
resource: work_package,
project: work_package.project)
end
it 'has a notifications association' do
expect(journal.notifications)
.to match_array([notification])
end
it 'destroys the associated notifications upon journal destruction' do
expect { journal.destroy }
.to change(Notification, :count).from(1).to(0)
end
end
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.
#++
require 'spec_helper'
describe Notifications::Scopes::UnreadMailDigest, type: :model do
describe '.unread_digest_mail' do
subject(:scope) { ::Notification.unread_mail_digest }
let(:no_mail_notification) { FactoryBot.create(:notification, read_mail_digest: nil) }
let(:unread_mail_notification) { FactoryBot.create(:notification, read_mail_digest: false) }
let(:read_mail_notification) { FactoryBot.create(:notification, read_mail_digest: true) }
before do
no_mail_notification
unread_mail_notification
read_mail_notification
end
it 'contains the notifications with read_mail: false' do
expect(scope)
.to match_array([unread_mail_notification])
end
end
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.
#++
require 'spec_helper'
describe Notifications::Scopes::UnreadMail, type: :model do
describe '.unread_mail' do
subject(:scope) { ::Notification.unread_mail }
let(:no_mail_notification) { FactoryBot.create(:notification, read_mail: nil) }
let(:unread_mail_notification) { FactoryBot.create(:notification, read_mail: false) }
let(:read_mail_notification) { FactoryBot.create(:notification, read_mail: true) }
before do
no_mail_notification
unread_mail_notification
read_mail_notification
end
it 'contains the notifications with read_mail: false' do
expect(scope)
.to match_array([unread_mail_notification])
end
end
end

@ -256,43 +256,4 @@ describe Watcher, type: :model, with_mail: false do
.to match_array([saved_user])
end
end
describe '#watcher_recipients' do
before do
saved_watchable.watchers.create(user: saved_user)
end
context 'with a user `all` notifications' do
let(:notification_settings) do
[FactoryBot.build(:mail_notification_setting, all: true)]
end
it 'returns the user' do
expect(saved_watchable.watcher_recipients)
.to match_array([saved_user])
end
end
context 'with a user `watched` notification' do
let(:notification_settings) do
[FactoryBot.build(:mail_notification_setting, watched: true)]
end
it 'returns the user' do
expect(saved_watchable.watcher_recipients)
.to match_array([saved_user])
end
end
context 'with a user without the `watched` notification' do
let(:notification_settings) do
[FactoryBot.build(:mail_notification_setting, watched: false)]
end
it 'is empty' do
expect(saved_watchable.watcher_recipients)
.to be_empty
end
end
end
end

@ -32,7 +32,7 @@ require 'spec_helper'
##
# Tests that email notifications will be sent upon creating or changing a work package.
describe WorkPackage, type: :model do
describe WorkPackage, type: :model, with_settings: { journal_aggregation_time_minutes: 0 } do
describe 'OpenProject notifications' do
shared_let(:admin) { FactoryBot.create :admin }
@ -56,7 +56,7 @@ describe WorkPackage, type: :model do
OpenProject::Notifications.unsubscribe(OpenProject::Events::AGGREGATED_WORK_PACKAGE_JOURNAL_READY, subscription)
end
context 'after creation' do
context 'when after creation' do
before do
work_package
perform_enqueued_jobs
@ -67,7 +67,7 @@ describe WorkPackage, type: :model do
end
end
describe 'after update' do
describe 'when after update' do
before do
work_package

@ -1,107 +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.
#++
require 'spec_helper'
describe WorkPackage, type: :model do
describe ActionMailer::Base do
let(:user_1) do
FactoryBot.create(:user,
mail: 'dlopper@somenet.foo',
member_in_project: project)
end
let(:user_2) do
FactoryBot.create(:user,
mail: 'jsmith@somenet.foo',
member_in_project: project)
end
let(:project) { FactoryBot.create(:project) }
let(:work_package) { FactoryBot.build(:work_package, project: project) }
before do
allow(work_package).to receive(:recipients).and_return([user_1])
allow(work_package).to receive(:watcher_recipients).and_return([user_2])
Journal::NotificationConfiguration.with true do
perform_enqueued_jobs do
work_package.save
end
end
end
subject { ActionMailer::Base.deliveries.size }
it do
expect(subject).to eq 2
end
context 'stale object' do
before do
wp = WorkPackage.find(work_package.id)
wp.subject = 'Subject update'
wp.save!
ActionMailer::Base.deliveries.clear
work_package.subject = 'A different subject update'
begin
work_package.save!
rescue StandardError
nil
end
end
it { is_expected.to eq(0) }
end
context 'no notification' do
before do
ActionMailer::Base.deliveries.clear # clear mails sent due to prior WP creation
Journal::NotificationConfiguration.with false do
work_package.save!
end
end
it { is_expected.to eq(0) }
end
context 'group_assigned_work_package' do
let(:group) { FactoryBot.create(:group, members: user_1) }
before do
work_package.assigned_to = group
end
subject { work_package.recipients }
it { is_expected.to include(user_1) }
end
end
end

@ -42,25 +42,4 @@ describe WorkPackage, type: :model do
let(:watch_permission) { :view_work_packages }
let(:project) { model_instance.project }
end
# This is not really a trait of acts as watchable but rather of
# the work package observer + journal observer
context 'notifications' do
let(:number_of_recipients) { (work_package.recipients | work_package.watcher_recipients).length }
let(:current_user) { FactoryBot.create :user }
before do
allow(UserMailer).to receive_message_chain :work_package_updated, :deliver
# Ensure notification setting to be set in a way that will trigger e-mails.
allow(Setting).to receive(:notified_events).and_return(%w(work_package_updated))
expect(UserMailer).to receive(:work_package_updated).exactly(number_of_recipients).times
allow(User).to receive(:current).and_return(current_user)
end
it 'sends one delayed mail notification for each watcher recipient' do
work_package.update description: 'Any new description'
end
end
end

@ -125,7 +125,7 @@ describe 'API v3 Work package resource',
context 'not set' do
let(:params) { update_params }
it { expect(Notifications::JournalCompletedJob).to have_been_enqueued.at_least(1) }
it { expect(Journals::CompletedJob).to have_been_enqueued.at_least(1) }
end
context 'disabled' do
@ -133,7 +133,7 @@ describe 'API v3 Work package resource',
let(:params) { update_params }
it do
expect(Notifications::JournalCompletedJob)
expect(Journals::CompletedJob)
.to have_been_enqueued
.at_least(1)
end
@ -144,7 +144,7 @@ describe 'API v3 Work package resource',
let(:params) { update_params }
it do
expect(Notifications::JournalCompletedJob)
expect(Journals::CompletedJob)
.to have_been_enqueued
.at_least(1)
end

@ -30,27 +30,26 @@
require 'spec_helper'
describe 'seeds' do
context 'standard edition', with_config: { edition: 'standard' } do
it 'create the demo data' do
expect { StandardSeeder::BasicDataSeeder.new.seed! }.not_to raise_error
expect { AdminUserSeeder.new.seed! }.not_to raise_error
expect { DemoDataSeeder.new.seed! }.not_to raise_error
describe RootSeeder,
'standard edition',
with_config: { edition: 'standard' },
with_settings: { journal_aggregation_time_minutes: 0 } do
it 'create the demo data' do
expect { described_class.new.do_seed! }.not_to raise_error
expect(User.where(admin: true).count).to eq 1
expect(Project.count).to eq 2
expect(WorkPackage.count).to eq 33
expect(Wiki.count).to eq 2
expect(Query.where.not(hidden: true).count).to eq 7
expect(Query.count).to eq 25
expect(Projects::Status.count).to eq 2
expect(Role.where(type: 'Role').count).to eq 5
expect(GlobalRole.count).to eq 1
expect(User.where(admin: true).count).to eq 1
expect(Project.count).to eq 2
expect(WorkPackage.count).to eq 33
expect(Wiki.count).to eq 2
expect(Query.where.not(hidden: true).count).to eq 7
expect(Query.count).to eq 25
expect(Projects::Status.count).to eq 2
expect(Role.where(type: 'Role').count).to eq 5
expect(GlobalRole.count).to eq 1
perform_enqueued_jobs
perform_enqueued_jobs
expect(ActionMailer::Base.deliveries)
.to be_empty
end
expect(ActionMailer::Base.deliveries)
.to be_empty
end
end

@ -0,0 +1,129 @@
#-- 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'
shared_context 'with CreateFromJournalJob context' do
shared_let(:project) { FactoryBot.create(:project_with_types) }
let(:permissions) { [] }
let(:recipient) do
FactoryBot.create(:user,
notification_settings: recipient_notification_settings,
member_in_project: project,
member_through_role: FactoryBot.create(:role, permissions: permissions),
login: recipient_login,
preferences: {
no_self_notified: recipient_no_self_notified
})
end
let(:recipient_login) { "johndoe" }
let(:recipient_no_self_notified) { true }
let(:other_user) do
notification_settings = [
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false)
]
FactoryBot.create(:user,
notification_settings: notification_settings)
end
let(:notification_settings_all_false) do
{
all: false,
involved: false,
watched: false,
mentioned: false,
work_package_commented: false,
work_package_processed: false,
work_package_created: false,
work_package_scheduled: false,
work_package_prioritized: false
}
end
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, all: true),
FactoryBot.build(:in_app_notification_setting, all: true),
FactoryBot.build(:mail_digest_notification_setting, all: true)
]
end
let(:send_notifications) { true }
shared_examples_for 'creates notification' do
let(:sender) { author }
let(:notification_channel_reasons) do
{
read_ian: false,
reason_ian: :mentioned,
read_mail: false,
reason_mail: :mentioned,
read_mail_digest: false,
reason_mail_digest: :mentioned
}
end
let(:notification) { FactoryBot.build_stubbed(:notification) }
it 'creates a notification and returns it' do
notifications_service = instance_double(Notifications::CreateService)
allow(Notifications::CreateService)
.to receive(:new)
.with(user: sender)
.and_return(notifications_service)
allow(notifications_service)
.to receive(:call)
.and_return(ServiceResult.new(success: true, result: notification))
expect(call.all_results)
.to match_array([notification])
expect(notifications_service)
.to have_received(:call)
.with({ recipient_id: recipient.id,
project: project,
actor: sender,
journal: journal,
resource: resource }.merge(notification_channel_reasons))
end
end
shared_examples_for 'creates no notification' do
it 'creates no notification' do
allow(Notifications::CreateService)
.to receive(:new)
.and_call_original
call
expect(Notifications::CreateService)
.not_to have_received(:new)
end
end
end

@ -0,0 +1,168 @@
#-- 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'
require_relative './create_from_journal_job_shared'
describe Notifications::CreateFromModelService, 'comment', with_settings: { journal_aggregation_time_minutes: 0 } do
subject(:call) do
described_class.new(resource).call(send_notifications)
end
include_context 'with CreateFromJournalJob context'
shared_let(:project) { FactoryBot.create(:project) }
shared_let(:news) { FactoryBot.create(:news, project: project) }
let(:permissions) { [] }
let(:send_notifications) { true }
let(:resource) do
FactoryBot.create(:comment,
commented: news,
author: author,
comments: 'Some text')
end
let(:journal) { nil }
let(:author) { other_user }
current_user { other_user }
before do
recipient
end
describe '#perform' do
context 'with a newly created comment' do
context 'with the user having registered for all notifications' do
it_behaves_like 'creates notification' do
let(:notification_channel_reasons) do
{
read_ian: nil,
reason_ian: false,
read_mail: false,
reason_mail: :subscribed,
read_mail_digest: nil,
reason_mail_digest: false
}
end
end
end
context 'with the user having registered for involved notifications' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false.merge(involved: true)),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false.merge(involved: true)),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false.merge(involved: true))
]
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for no notifications' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false)
]
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for watcher notifications and watching the news' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false.merge(watched: true))
]
end
before do
news.watcher_users << recipient
end
it_behaves_like 'creates notification' do
let(:notification_channel_reasons) do
{
read_ian: nil,
reason_ian: false,
read_mail: false,
reason_mail: :watched,
read_mail_digest: nil,
reason_mail_digest: false
}
end
end
end
context 'with the user not having registered for watcher notifications and watching the news' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false)
]
end
before do
news.watcher_users << recipient
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for watcher notifications and not watching the news' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false.merge(watched: true))
]
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for all notifications but lacking permissions' do
before do
recipient.members.destroy_all
end
it_behaves_like 'creates no notification'
end
end
end
end

@ -0,0 +1,364 @@
#-- 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'
require_relative './create_from_journal_job_shared'
describe Notifications::CreateFromModelService, 'message', with_settings: { journal_aggregation_time_minutes: 0 } do
subject(:call) do
described_class.new(journal).call(send_notifications)
end
include_context 'with CreateFromJournalJob context'
shared_let(:project) { FactoryBot.create(:project) }
shared_let(:forum) { FactoryBot.create(:forum, project: project) }
let(:permissions) { [:view_messages] }
let(:send_notifications) { true }
let(:resource) do
FactoryBot.create(:message,
forum: forum,
parent: root_message)
end
let(:journal) { resource.journals.last }
let(:author) { other_user }
let(:root_message) do
FactoryBot.create(:message,
forum: forum)
end
current_user { other_user }
before do
recipient
end
describe '#perform' do
context 'with a newly created message' do
context 'with the user having registered for all notifications' do
it_behaves_like 'creates notification' do
let(:notification_channel_reasons) do
{
read_ian: nil,
reason_ian: false,
read_mail: false,
reason_mail: :subscribed,
read_mail_digest: nil,
reason_mail_digest: false
}
end
end
end
context 'with the user having registered for involved notifications' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false.merge(involved: true)),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false.merge(involved: true)),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false.merge(involved: true))
]
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for no notifications' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false)
]
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for watcher notifications and watching the forum' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false.merge(watched: true))
]
end
before do
forum.watcher_users << recipient
end
it_behaves_like 'creates notification' do
let(:notification_channel_reasons) do
{
read_ian: nil,
reason_ian: false,
read_mail: false,
reason_mail: :watched,
read_mail_digest: nil,
reason_mail_digest: false
}
end
end
end
context 'with the user not having registered for watcher notifications and watching the forum' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false)
]
end
before do
forum.watcher_users << recipient
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for watcher notifications and not watching the forum nor root message' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false.merge(watched: true))
]
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for watcher notifications and watching the root' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false.merge(watched: true))
]
end
before do
root_message.watcher_users << recipient
end
it_behaves_like 'creates notification' do
let(:notification_channel_reasons) do
{
read_ian: nil,
reason_ian: false,
read_mail: false,
reason_mail: :watched,
read_mail_digest: nil,
reason_mail_digest: false
}
end
end
end
context 'with the user not having registered for watcher notifications and watching the root' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false)
]
end
before do
root_message.watcher_users << recipient
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for all notifications but lacking permissions' do
before do
recipient.members.destroy_all
end
it_behaves_like 'creates no notification'
end
end
context 'with an updated message' do
before do
resource.subject = 'A new subject'
resource.save!
end
context 'with the user having registered for all notifications' do
it_behaves_like 'creates notification' do
let(:notification_channel_reasons) do
{
read_ian: nil,
reason_ian: false,
read_mail: false,
reason_mail: :subscribed,
read_mail_digest: nil,
reason_mail_digest: false
}
end
end
end
context 'with the user having registered for involved notifications' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false.merge(involved: true)),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false.merge(involved: true)),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false.merge(involved: true))
]
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for no notifications' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false)
]
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for watcher notifications and watching the forum' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false.merge(watched: true))
]
end
before do
forum.watcher_users << recipient
end
it_behaves_like 'creates notification' do
let(:notification_channel_reasons) do
{
read_ian: nil,
reason_ian: false,
read_mail: false,
reason_mail: :watched,
read_mail_digest: nil,
reason_mail_digest: false
}
end
end
end
context 'with the user not having registered for watcher notifications and watching the forum' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false)
]
end
before do
forum.watcher_users << recipient
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for watcher notifications and not watching the forum nor root message' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false.merge(watched: true))
]
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for watcher notifications and watching the root' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false.merge(watched: true))
]
end
before do
root_message.watcher_users << recipient
end
it_behaves_like 'creates notification' do
let(:notification_channel_reasons) do
{
read_ian: nil,
reason_ian: false,
read_mail: false,
reason_mail: :watched,
read_mail_digest: nil,
reason_mail_digest: false
}
end
end
end
context 'with the user not having registered for watcher notifications and watching the root' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false)
]
end
before do
root_message.watcher_users << recipient
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for all notifications but lacking permissions' do
before do
recipient.members.destroy_all
end
it_behaves_like 'creates no notification'
end
end
end
end

@ -0,0 +1,109 @@
#-- 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'
require_relative './create_from_journal_job_shared'
describe Notifications::CreateFromModelService, 'news', with_settings: { journal_aggregation_time_minutes: 0 } do
subject(:call) do
described_class.new(journal).call(send_notifications)
end
include_context 'with CreateFromJournalJob context'
let(:journable) { FactoryBot.build_stubbed(:news) }
let(:resource) { FactoryBot.create(:news, project: project) }
# view_news is a public permission
let(:permissions) { [] }
let(:send_notifications) { true }
let(:journal) { resource.journals.last }
let(:author) { other_user }
current_user { other_user }
before do
recipient
end
describe '#call' do
context 'with a newly created news do' do
context 'with the user having registered for all notifications' do
it_behaves_like 'creates notification' do
let(:notification_channel_reasons) do
{
read_ian: nil,
reason_ian: false,
read_mail: false,
reason_mail: :subscribed,
read_mail_digest: nil,
reason_mail_digest: false
}
end
end
end
context 'with the user having registered for involved notifications' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false.merge(involved: true)),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false.merge(involved: true)),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false.merge(involved: true))
]
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for no notifications' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false)
]
end
it_behaves_like 'creates no notification'
end
end
context 'with an updated news' do
before do
resource.description = "Some new text to create a journal"
resource.save!
end
context 'with the user having registered for all notifications' do
it_behaves_like 'creates no notification'
end
end
end
end

@ -0,0 +1,315 @@
#-- 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'
require_relative './create_from_journal_job_shared'
describe Notifications::CreateFromModelService, 'wiki', with_settings: { journal_aggregation_time_minutes: 0 } do
subject(:call) do
described_class.new(journal).call(send_notifications)
end
include_context 'with CreateFromJournalJob context'
shared_let(:project) { FactoryBot.create(:project) }
shared_let(:wiki) { FactoryBot.create(:wiki, project: project) }
let(:permissions) { [:view_wiki_pages] }
let(:send_notifications) { true }
let(:wiki_page) do
FactoryBot.create(:wiki_page,
wiki: wiki,
content: FactoryBot.build(:wiki_content,
author: other_user))
end
let(:resource) { wiki_page.content }
let(:journal) { resource.journals.last }
let(:author) { other_user }
current_user { other_user }
before do
recipient
end
describe '#perform' do
context 'with a newly created wiki page' do
context 'with the user having registered for all notifications' do
it_behaves_like 'creates notification' do
let(:notification_channel_reasons) do
{
read_ian: nil,
reason_ian: false,
read_mail: false,
reason_mail: :subscribed,
read_mail_digest: nil,
reason_mail_digest: false
}
end
end
end
context 'with the user having registered for involved notifications' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false.merge(involved: true)),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false.merge(involved: true)),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false.merge(involved: true))
]
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for no notifications' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false)
]
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for watcher notifications and watching the wiki' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false.merge(watched: true))
]
end
before do
wiki.watcher_users << recipient
end
it_behaves_like 'creates notification' do
let(:notification_channel_reasons) do
{
read_ian: nil,
reason_ian: false,
read_mail: false,
reason_mail: :watched,
read_mail_digest: nil,
reason_mail_digest: false
}
end
end
end
context 'with the user not having registered for watcher notifications and watching the wiki' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false)
]
end
before do
wiki.watcher_users << recipient
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for watcher notifications and not watching the wiki' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false.merge(watched: true))
]
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for all notifications but lacking permissions' do
let(:permissions) { [] }
it_behaves_like 'creates no notification'
end
end
context 'with an updated wiki page' do
before do
resource.text = "Some new text to create a journal"
resource.save!
end
context 'with the user having registered for all notifications' do
it_behaves_like 'creates notification' do
let(:notification_channel_reasons) do
{
read_ian: nil,
reason_ian: false,
read_mail: false,
reason_mail: :subscribed,
read_mail_digest: nil,
reason_mail_digest: false
}
end
end
end
context 'with the user having registered for involved notifications' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false.merge(involved: true)),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false.merge(involved: true)),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false.merge(involved: true))
]
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for no notifications' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false)
]
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for watcher notifications and watching the wiki' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false.merge(watched: true))
]
end
before do
wiki.watcher_users << recipient
end
it_behaves_like 'creates notification' do
let(:notification_channel_reasons) do
{
read_ian: nil,
reason_ian: false,
read_mail: false,
reason_mail: :watched,
read_mail_digest: nil,
reason_mail_digest: false
}
end
end
end
context 'with the user not having registered for watcher notifications and watching the wiki' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false)
]
end
before do
wiki.watcher_users << recipient
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for watcher notifications and not watching the wiki nor the page' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false.merge(watched: true))
]
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for watcher notifications and watching the page' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false.merge(watched: true)),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false.merge(watched: true))
]
end
before do
wiki_page.watcher_users << recipient
end
it_behaves_like 'creates notification' do
let(:notification_channel_reasons) do
{
read_ian: nil,
reason_ian: false,
read_mail: false,
reason_mail: :watched,
read_mail_digest: nil,
reason_mail_digest: false
}
end
end
end
context 'with the user not having registered for watcher notifications and watching the page' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false)
]
end
before do
wiki_page.watcher_users << recipient
end
it_behaves_like 'creates no notification'
end
context 'with the user having registered for all notifications but lacking permissions' do
let(:permissions) { [] }
it_behaves_like 'creates no notification'
end
end
end
end

@ -28,55 +28,19 @@
# See docs/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
require_relative './create_from_journal_job_shared'
# rubocop:disable RSpec/MultipleMemoizedHelpers
describe Notifications::JournalWpNotificationService, with_settings: { journal_aggregation_time_minutes: 0 } do
let(:project) { FactoryBot.create(:project_with_types) }
let(:role) { FactoryBot.create(:role, permissions: [:view_work_packages]) }
let(:recipient) do
FactoryBot.create(:user,
notification_settings: recipient_notification_settings,
member_in_project: project,
member_through_role: role,
login: recipient_login,
preferences: {
no_self_notified: recipient_no_self_notified
})
end
let(:recipient_login) { "johndoe" }
let(:recipient_no_self_notified) { true }
let(:other_user) do
FactoryBot.create(:user,
notification_settings: other_user_notification_settings)
describe Notifications::CreateFromModelService,
'work_package',
with_settings: { journal_aggregation_time_minutes: 0 } do
subject(:call) do
described_class.new(journal).call(send_notifications)
end
include_context 'with CreateFromJournalJob context'
let(:permissions) { [:view_work_packages] }
let(:author) { user_property == :author ? recipient : other_user }
let(:notification_settings_all_false) do
{
all: false,
involved: false,
watched: false,
mentioned: false,
work_package_commented: false,
work_package_processed: false,
work_package_created: false,
work_package_scheduled: false,
work_package_prioritized: false
}
end
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, all: true),
FactoryBot.build(:in_app_notification_setting, all: true),
FactoryBot.build(:mail_digest_notification_setting, all: true)
]
end
let(:other_user_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, **notification_settings_all_false),
FactoryBot.build(:in_app_notification_setting, **notification_settings_all_false),
FactoryBot.build(:mail_digest_notification_setting, **notification_settings_all_false)
]
end
let(:user_property) { nil }
let(:work_package) do
wp_attributes = {
@ -101,10 +65,9 @@ describe Notifications::JournalWpNotificationService, with_settings: { journal_a
FactoryBot.create(:work_package,
**wp_attributes)
end
end
let(:journal) { journal_1 }
let(:journal_1) { work_package.journals.first }
let(:resource) { work_package }
let(:journal) { work_package.journals.first }
let(:journal_2_with_notes) do
work_package.add_journal author, 'something I have to say'
work_package.save(validate: false)
@ -130,11 +93,6 @@ describe Notifications::JournalWpNotificationService, with_settings: { journal_a
work_package.save(validate: false)
work_package.journals.last
end
let(:send_notifications) { true }
def call
described_class.call(journal, send_notifications)
end
before do
# make sure no other calls are made due to WP creation/update
@ -143,54 +101,6 @@ describe Notifications::JournalWpNotificationService, with_settings: { journal_a
login_as(author)
end
shared_examples_for 'creates notification' do
let(:sender) { author }
let(:notification_channel_reasons) do
{
read_ian: false,
reason_ian: :mentioned,
read_mail: false,
reason_mail: :mentioned,
read_mail_digest: false,
reason_mail_digest: :mentioned
}
end
it 'creates a notification' do
notifications_service = instance_double(Notifications::CreateService)
allow(Notifications::CreateService)
.to receive(:new)
.with(user: sender)
.and_return(notifications_service)
allow(notifications_service)
.to receive(:call)
call
expect(notifications_service)
.to have_received(:call)
.with({ recipient_id: recipient.id,
project: journal.project,
actor: sender,
journal: journal,
resource: journal.journable }.merge(notification_channel_reasons))
end
end
shared_examples_for 'creates no notification' do
it 'creates no notification' do
allow(Notifications::CreateService)
.to receive(:new)
.and_call_original
call
expect(Notifications::CreateService)
.not_to have_received(:new)
end
end
context 'when user is assignee' do
let(:user_property) { :assigned_to }
let(:recipient_notification_settings) do
@ -296,7 +206,7 @@ describe Notifications::JournalWpNotificationService, with_settings: { journal_a
end
context 'assignee is not allowed to view work packages' do
let(:role) { FactoryBot.create(:role, permissions: []) }
let(:permissions) { [] }
it_behaves_like 'creates no notification'
end
@ -428,7 +338,7 @@ describe Notifications::JournalWpNotificationService, with_settings: { journal_a
end
context 'when responsible is not allowed to view work packages' do
let(:role) { FactoryBot.create(:role, permissions: []) }
let(:permissions) { [] }
it_behaves_like 'creates no notification'
end
@ -560,7 +470,7 @@ describe Notifications::JournalWpNotificationService, with_settings: { journal_a
end
context 'when watcher is not allowed to view work packages' do
let(:role) { FactoryBot.create(:role, permissions: []) }
let(:permissions) { [] }
it_behaves_like 'creates no notification'
end
@ -729,7 +639,7 @@ describe Notifications::JournalWpNotificationService, with_settings: { journal_a
end
context 'when not allowed to view work packages' do
let(:role) { FactoryBot.create(:role, permissions: []) }
let(:permissions) { [] }
it_behaves_like 'creates no notification'
end
@ -1194,12 +1104,12 @@ describe Notifications::JournalWpNotificationService, with_settings: { journal_a
context 'group is not allowed to view the work package' do
let(:group_role) { FactoryBot.create(:role, permissions: []) }
let(:role) { FactoryBot.create(:role, permissions: []) }
let(:permissions) { [] }
it_behaves_like 'creates no notification'
context 'but group member is allowed individually' do
let(:role) { FactoryBot.create(:role, permissions: [:view_work_packages]) }
let(:permissions) { [:view_work_packages] }
it_behaves_like 'creates notification' do
let(:notification_channel_reasons) do
@ -1330,7 +1240,7 @@ describe Notifications::JournalWpNotificationService, with_settings: { journal_a
end
context "with the mentioned user not being allowed to view the work package" do
let(:role) { FactoryBot.create(:role, permissions: []) }
let(:permissions) { [] }
let(:note) do
"Hello user:#{recipient.login}, hey user##{recipient.id}"
end
@ -1466,20 +1376,12 @@ describe Notifications::JournalWpNotificationService, with_settings: { journal_a
it_behaves_like 'creates no notification'
end
end
describe 'initialization' do
it 'subscribes the listener' do
allow(Notifications::JournalWpNotificationService)
.to receive(:call)
OpenProject::Notifications.send(
OpenProject::Events::AGGREGATED_WORK_PACKAGE_JOURNAL_READY,
journal: FactoryBot.build(:journal)
)
context 'when the journal is deleted' do
before do
journal.destroy
end
expect(Notifications::JournalWpNotificationService)
.to have_received(:call)
it_behaves_like 'creates no notification'
end
end
# rubocop:enable Rspec/MultipleMemoizedHelpers

@ -32,135 +32,5 @@ require 'spec_helper'
require 'services/base_services/behaves_like_create_service'
describe Notifications::CreateService, type: :model do
let(:mail_digest_before) { false }
before do
scope = double('scope')
allow(Notification)
.to receive(:mail_digest_before)
.with(recipient: model_instance.recipient, time: model_instance.created_at)
.and_return(scope)
allow(scope)
.to receive(:where)
.and_return(scope)
allow(scope)
.to receive(:not)
.and_return(scope)
allow(scope)
.to receive(:exists?)
.and_return(mail_digest_before)
end
it_behaves_like 'BaseServices create service' do
let(:call_attributes) do
{}
end
context 'when successful' do
before do
allow(set_attributes_service)
.to receive(:call) do |attributes|
model_instance.attributes = attributes
set_attributes_result
end
end
context 'when mail ought to be send', { with_settings: { notification_email_delay_minutes: 30 } } do
let(:call_attributes) do
{
read_mail: false
}
end
it 'schedules a delayed notification job' do
allow(Time)
.to receive(:now)
.and_return(Time.now)
expect { subject }
.to have_enqueued_job(Mails::NotificationJob)
.with({ "_aj_globalid" => "gid://open-project/Notification/#{model_instance.id}" })
.at(Time.now + Setting.notification_email_delay_minutes.minutes)
end
end
context 'when mail not ought to be send' do
let(:call_attributes) do
{
read_mail: nil
}
end
it 'schedules no notification job' do
expect { subject }
.not_to have_enqueued_job(Mails::NotificationJob)
end
end
context 'when digests ought to be send' do
let(:call_attributes) do
{
read_mail_digest: false
}
end
before do
allow(model_instance.recipient)
.to receive(:time_zone)
.and_return(ActiveSupport::TimeZone['Tijuana'])
end
it 'schedules a digest mail job' do
expected_time = ActiveSupport::TimeZone['Tijuana'].parse(Setting.notification_email_digest_time) + 1.day
expect { subject }
.to have_enqueued_job(Mails::DigestJob)
.with({ "_aj_globalid" => "gid://open-project/User/#{model_instance.recipient.id}" })
.at(expected_time)
end
end
context 'when digests ought to be send and there is already a digest job scheduled' do
let(:mail_digest_before) { true }
let(:call_attributes) do
{
read_mail_digest: false
}
end
it 'schedules no digest mail job' do
expect { subject }
.not_to have_enqueued_job(Mails::DigestJob)
end
end
context 'when digests not ought to be send' do
let(:call_attributes) do
{
read_mail_digest: nil
}
end
it 'schedules no digest mail job' do
expect { subject }
.not_to have_enqueued_job(Mails::DigestJob)
end
end
end
context 'when unsuccessful' do
let(:model_save_result) { false }
it 'schedules no job' do
expect { subject }
.not_to have_enqueued_job(Mails::NotificationJob)
end
end
end
it_behaves_like 'BaseServices create service'
end

@ -1,85 +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.
#++
require 'spec_helper'
describe Notifications::JournalNotificationService do
let(:journal) { FactoryBot.build_stubbed(:journal, journable: journable) }
let(:send_mails) { true }
shared_examples_for 'enqueues a notification' do
before do
# Freeze time
allow(Time)
.to receive(:current)
.and_return(Time.current)
notification_set = double('notification set')
expect(Notifications::JournalCompletedJob)
.to receive(:set)
.with(wait_until: Setting.journal_aggregation_time_minutes.to_i.minutes.from_now)
.and_return(notification_set)
expect(notification_set)
.to receive(:perform_later)
.with(journal.id, send_mails)
end
it 'fulfills expectations' do
described_class.call(journal, send_mails)
end
end
shared_examples_for 'enqueues no notification' do
before do
expect(Notifications::JournalCompletedJob)
.not_to receive(:set)
end
it 'fulfills expectations' do
described_class.call(journal, send_mails)
end
end
context 'for a work package journal' do
let(:journable) { FactoryBot.build_stubbed(:stubbed_work_package) }
it_behaves_like 'enqueues a notification'
end
context 'for a wiki content journal' do
let(:journable) { FactoryBot.build_stubbed(:wiki_content) }
it_behaves_like 'enqueues a notification'
end
context 'for a news journal' do
let(:journable) { FactoryBot.build_stubbed(:news) }
it_behaves_like 'enqueues no notification'
end
end

@ -1,240 +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.
#++
require 'spec_helper'
describe Notifications::JournalWikiMailService do
let(:project) do
FactoryBot.build_stubbed(:project).tap do |p|
allow(p)
.to receive(:notified_users)
.and_return([project_notified_user_with_permission, project_notified_user_wo_permission])
end
end
let(:wiki) do
FactoryBot.build_stubbed(:wiki, project: project).tap do |w|
allow(w)
.to receive(:watcher_recipients)
.and_return([wiki_watcher_user])
end
end
let(:wiki_page) do
FactoryBot.build_stubbed(:wiki_page, wiki: wiki).tap do |w|
allow(w)
.to receive(:watcher_recipients)
.and_return([wiki_page_watcher_user])
end
end
let(:wiki_content) do
FactoryBot.build_stubbed(:wiki_content, page: wiki_page).tap do |wc|
allow(wc)
.to receive(:visible?)
.with(project_notified_user_with_permission)
.and_return(true)
allow(wc)
.to receive(:visible?)
.with(project_notified_user_wo_permission)
.and_return(false)
end
end
let(:journal) do
FactoryBot.build_stubbed(:wiki_content_journal, journable: wiki_content, user: current_user).tap do |j|
allow(j)
.to receive(:initial?)
.and_return(journal_initial)
allow(j)
.to receive(:noop?)
.and_return(journal_noop)
end
end
let(:journal_initial) { true }
let(:journal_noop) { false }
let(:notification_setting) { %w(wiki_content_added wiki_content_updated) }
let(:current_user) do
FactoryBot.build_stubbed(:user)
end
let(:project_notified_user_with_permission) do
FactoryBot.build_stubbed(:user)
end
let(:project_notified_user_wo_permission) do
FactoryBot.build_stubbed(:user)
end
let(:wiki_watcher_user) do
FactoryBot.build_stubbed(:user)
end
let(:wiki_page_watcher_user) do
FactoryBot.build_stubbed(:user)
end
context '.call' do
let(:subject) { described_class.call(journal, send_mails) }
let(:send_mails) { true }
before do
allow(Setting).to receive(:notified_events).and_return(notification_setting)
end
shared_examples_for 'sends no mails' do
it 'sends no mails' do
expect(UserMailer)
.not_to receive(:wiki_content_updated)
expect(UserMailer)
.not_to receive(:wiki_content_added)
subject
end
end
context 'with the settings allowing email sending for newly added content' do
let(:notification_setting) { %w(wiki_content_added) }
context 'for an initial journal' do
let(:journal_initial) { true }
it 'sends mails to users listening on all changes and to watchers of the wiki' do
[project_notified_user_with_permission, wiki_watcher_user].each do |u|
mailer = double('mailer')
expect(UserMailer)
.to receive(:wiki_content_added)
.with(u, wiki_content, current_user)
.and_return(mailer)
expect(mailer)
.to receive(:deliver_later)
end
subject
end
context 'with send_mails set to false' do
let(:send_mails) { false }
it_behaves_like 'sends no mails'
end
context 'with perform_deliveries set to false' do
before do
allow(UserMailer)
.to receive(:perform_deliveries)
.and_return(false)
end
it_behaves_like 'sends no mails'
end
context 'with the journal being a noop' do
let(:journal_noop) { true }
it_behaves_like 'sends no mails'
end
end
context 'for a non initial journal' do
let(:journal_initial) { false }
it_behaves_like 'sends no mails'
end
end
context 'with the settings allowing email sending for updated content' do
let(:notification_setting) { %w(wiki_content_updated) }
context 'for a non initial journal' do
let(:journal_initial) { false }
it 'sends mails to users listening on all changes and to watchers of the wiki' do
[project_notified_user_with_permission, wiki_watcher_user, wiki_page_watcher_user].each do |u|
mailer = double('mailer')
expect(UserMailer)
.to receive(:wiki_content_updated)
.with(u, wiki_content, current_user)
.and_return(mailer)
expect(mailer)
.to receive(:deliver_later)
end
subject
end
context 'with send_mails set to false' do
let(:send_mails) { false }
it_behaves_like 'sends no mails'
end
context 'with perform_deliveries set to false' do
before do
allow(UserMailer)
.to receive(:perform_deliveries)
.and_return(false)
end
it_behaves_like 'sends no mails'
end
context 'with the journal being a noop' do
let(:journal_noop) { true }
it_behaves_like 'sends no mails'
end
end
context 'for an initial journal' do
let(:journal_initial) { true }
it_behaves_like 'sends no mails'
end
end
end
it 'listener is subscribed' do
journal = double('journal')
send_mail = true
expect(Notifications::JournalWikiMailService)
.to receive(:call)
.with(journal, send_mail)
OpenProject::Notifications.send(OpenProject::Events::AGGREGATED_WIKI_JOURNAL_READY,
journal: journal,
send_mail: send_mail)
end
end

@ -0,0 +1,412 @@
#-- 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 Notifications::MailService, type: :model do
subject(:call) { instance.call }
let(:recipient) do
FactoryBot.build_stubbed(:user)
end
let(:actor) do
FactoryBot.build_stubbed(:user)
end
let(:instance) { described_class.new(notification) }
context 'with a work package journal notification' do
let(:journal) do
FactoryBot.build_stubbed(:work_package_journal).tap do |j|
allow(j)
.to receive(:initial?)
.and_return(journal_initial)
end
end
let(:read_ian) { false }
let(:notification) do
FactoryBot.build_stubbed(:notification,
journal: journal,
recipient: recipient,
actor: actor,
read_ian: read_ian)
end
let(:journal_initial) { false }
let(:mail) do
mail = instance_double(ActionMailer::MessageDelivery)
allow(UserMailer)
.to receive(:work_package_added)
.and_return(mail)
allow(UserMailer)
.to receive(:work_package_updated)
.and_return(mail)
allow(mail)
.to receive(:deliver_later)
mail
end
before do
mail
end
context 'with the notification being for an initial journal' do
let(:journal_initial) { true }
it 'sends a mail' do
call
expect(UserMailer)
.to have_received(:work_package_added)
.with(recipient,
journal,
journal.user)
expect(mail)
.to have_received(:deliver_later)
end
end
context 'with the notification being for an update journal' do
let(:journal_initial) { false }
it 'sends a mail' do
call
expect(UserMailer)
.to have_received(:work_package_updated)
.with(recipient,
journal,
journal.user)
expect(mail)
.to have_received(:deliver_later)
end
end
context 'with the notification read in app already' do
let(:read_ian) { true }
it 'sends no mail' do
call
expect(UserMailer)
.not_to have_received(:work_package_added)
expect(UserMailer)
.not_to have_received(:work_package_updated)
end
end
end
context 'with a wiki_content journal notification' do
let(:journal) do
FactoryBot.build_stubbed(:wiki_content_journal,
journable: FactoryBot.build_stubbed(:wiki_content)).tap do |j|
allow(j)
.to receive(:initial?)
.and_return(journal_initial)
end
end
let(:read_ian) { false }
let(:notification) do
FactoryBot.build_stubbed(:notification,
journal: journal,
recipient: recipient,
actor: actor,
read_ian: read_ian)
end
let(:notification_setting) { %w(wiki_content_added wiki_content_updated) }
let(:mail) do
mail = instance_double(ActionMailer::MessageDelivery)
allow(UserMailer)
.to receive(:wiki_content_added)
.and_return(mail)
allow(UserMailer)
.to receive(:wiki_content_updated)
.and_return(mail)
allow(mail)
.to receive(:deliver_later)
mail
end
let(:journal_initial) { false }
before do
mail
allow(Setting).to receive(:notified_events).and_return(notification_setting)
end
context 'with the notification being for an initial journal' do
let(:journal_initial) { true }
it 'sends a mail' do
call
expect(UserMailer)
.to have_received(:wiki_content_added)
.with(recipient,
journal.journable,
journal.user)
expect(mail)
.to have_received(:deliver_later)
end
end
context 'with the notification being for an initial journal but the event is disabled' do
let(:journal_initial) { true }
let(:notification_setting) { %w(wiki_content_updated) }
it 'sends no mail' do
call
expect(UserMailer)
.not_to have_received(:wiki_content_added)
end
end
context 'with the notification being for an update journal' do
let(:journal_initial) { false }
it 'sends a mail' do
call
expect(UserMailer)
.to have_received(:wiki_content_updated)
.with(recipient,
journal.journable,
journal.user)
expect(mail)
.to have_received(:deliver_later)
end
end
context 'with the notification being for an update journal but the event is disabled' do
let(:journal_initial) { false }
let(:notification_setting) { %w(wiki_content_added) }
it 'sends no mail' do
call
expect(UserMailer)
.not_to have_received(:wiki_content_updated)
end
end
context 'with the notification read in app already' do
let(:read_ian) { true }
it 'sends no mail' do
call
expect(UserMailer)
.not_to have_received(:wiki_content_added)
expect(UserMailer)
.not_to have_received(:wiki_content_updated)
end
end
end
context 'with a news journal notification' do
let(:journal) do
FactoryBot.build_stubbed(:news_journal,
journable: FactoryBot.build_stubbed(:news)).tap do |j|
allow(j)
.to receive(:initial?)
.and_return(journal_initial)
end
end
let(:notification) do
FactoryBot.build_stubbed(:notification,
journal: journal,
recipient: recipient,
actor: actor)
end
let(:notification_setting) { %w(news_added) }
let(:mail) do
mail = instance_double(ActionMailer::MessageDelivery)
allow(UserMailer)
.to receive(:news_added)
.and_return(mail)
allow(mail)
.to receive(:deliver_later)
mail
end
let(:journal_initial) { false }
before do
mail
allow(Setting).to receive(:notified_events).and_return(notification_setting)
end
context 'with the notification being for an initial journal' do
let(:journal_initial) { true }
it 'sends a mail' do
call
expect(UserMailer)
.to have_received(:news_added)
.with(recipient,
journal.journable,
journal.user)
expect(mail)
.to have_received(:deliver_later)
end
end
context 'with the notification being for an initial journal but the event is disabled' do
let(:journal_initial) { true }
let(:notification_setting) { %w() }
it 'sends no mail' do
call
expect(UserMailer)
.not_to have_received(:news_added)
end
end
# This case should not happen as no notification is created in this case that would
# trigger the NotificationJob. But as this might change, this test case is in place.
context 'with the notification being for an update journal' do
let(:journal_initial) { false }
it 'sends no mail' do
call
expect(UserMailer)
.not_to have_received(:news_added)
end
end
end
context 'with a message journal notification' do
let(:journal) do
FactoryBot.build_stubbed(:message_journal,
journable: FactoryBot.build_stubbed(:message))
end
let(:read_ian) { false }
let(:notification) do
FactoryBot.build_stubbed(:notification,
journal: journal,
resource: journal.journable,
recipient: recipient,
actor: actor,
read_ian: read_ian)
end
let(:notification_setting) { %w(message_posted) }
let(:mail) do
mail = instance_double(ActionMailer::MessageDelivery)
allow(UserMailer)
.to receive(:message_posted)
.and_return(mail)
allow(mail)
.to receive(:deliver_later)
mail
end
before do
mail
allow(Setting).to receive(:notified_events).and_return(notification_setting)
end
it 'sends a mail' do
call
expect(UserMailer)
.to have_received(:message_posted)
.with(recipient,
journal.journable,
actor)
expect(mail)
.to have_received(:deliver_later)
end
context 'with the event being disabled' do
let(:notification_setting) { %w(wiki_content_updated) }
it 'sends no mail' do
call
expect(UserMailer)
.not_to have_received(:message_posted)
end
end
context 'with the notification read in app already' do
let(:read_ian) { true }
it 'sends no mail' do
call
expect(UserMailer)
.not_to have_received(:message_posted)
end
end
end
context 'with a different journal notification' do
# This is actually not supported by now but serves as a test
let(:journal) do
FactoryBot.build_stubbed(:journal,
journable: FactoryBot.build_stubbed(:user))
end
let(:notification) do
FactoryBot.build_stubbed(:notification,
journal: journal,
recipient: recipient,
actor: actor)
end
it 'raises an error' do
expect { call }
.to raise_error(ArgumentError)
end
end
end

@ -164,35 +164,6 @@ MESSAGE
end
end
describe '#watcher_recipients' do
before do
watching_user
model_instance.reload
end
subject { model_instance.watcher_recipients }
it 'has the watching user' do
is_expected.to match_array([watching_user])
end
context 'when the permission to watch has been removed' do
before do
if is_public_permission
watching_user.memberships.destroy_all
else
watcher_role.remove_permission! watch_permission
end
model_instance.reload
end
it 'is empty' do
is_expected.to match_array([])
end
end
end
describe '#watched_by?' do
before do
watching_user

@ -0,0 +1,158 @@
#-- 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 Journals::CompletedJob, type: :model do
let(:send_mail) { true }
let(:journal) do
FactoryBot.build_stubbed(:journal, journable: journable).tap do |j|
allow(Journal)
.to receive(:find)
.with(j.id.to_s)
.and_return(j)
allow(Journal)
.to receive(:find_by)
.with(id: j.id)
.and_return(j)
allow(Journal)
.to receive(:exists?)
.with(id: j.id)
.and_return(true)
end
end
describe '.schedule' do
subject { described_class.schedule(journal, send_mail) }
shared_examples_for 'enqueues a JournalCompletedJob' do
before do
allow(Time)
.to receive(:current)
.and_return(Time.current)
end
it 'enqueues a JournalCompletedJob' do
expect { subject }
.to have_enqueued_job(described_class)
.at(Setting.journal_aggregation_time_minutes.to_i.minutes.from_now)
.with(journal.id,
send_mail)
end
end
shared_examples_for 'enqueues no job' do
it 'enqueues no JournalCompletedJob' do
expect { subject }
.not_to have_enqueued_job(described_class)
end
end
context 'with a work_package' do
let(:journable) { FactoryBot.build_stubbed(:work_package) }
it_behaves_like 'enqueues a JournalCompletedJob'
end
context 'with a wiki page' do
let(:journable) { FactoryBot.build_stubbed(:wiki_content) }
it_behaves_like 'enqueues a JournalCompletedJob'
end
context 'with a news' do
let(:journable) { FactoryBot.build_stubbed(:news) }
it_behaves_like 'enqueues a JournalCompletedJob'
end
end
describe '#perform' do
subject { described_class.new.perform(journal.id, send_mail) }
shared_examples_for 'sends a notification' do |event|
it 'sends a notification' do
allow(OpenProject::Notifications)
.to receive(:send)
subject
expect(OpenProject::Notifications)
.to have_received(:send)
.with(event,
journal: journal,
send_mail: send_mail)
end
end
context 'with a work packages' do
let(:journable) { FactoryBot.build_stubbed(:work_package) }
it_behaves_like 'sends a notification',
OpenProject::Events::AGGREGATED_WORK_PACKAGE_JOURNAL_READY
end
context 'with wiki page content' do
let(:journable) { FactoryBot.build_stubbed(:wiki_content) }
it_behaves_like 'sends a notification',
OpenProject::Events::AGGREGATED_WIKI_JOURNAL_READY
end
context 'with a news' do
let(:journable) { FactoryBot.build_stubbed(:news) }
it_behaves_like 'sends a notification',
OpenProject::Events::AGGREGATED_NEWS_JOURNAL_READY
end
context 'with a non non-existant journal' do
let(:journable) { FactoryBot.build_stubbed(:work_package) }
before do
allow(Journal)
.to receive(:find_by)
.with(id: journal.id)
.and_return(nil)
end
it 'sends no notification' do
allow(OpenProject::Notifications)
.to receive(:send)
subject
expect(OpenProject::Notifications)
.not_to have_received(:send)
end
end
end
end

@ -1,173 +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.
#++
require 'spec_helper'
describe Mails::WorkPackageJob, type: :model do
let(:project) { FactoryBot.create(:project) }
let(:role) { FactoryBot.create(:role, permissions: [:view_work_packages]) }
let(:recipient) do
FactoryBot.create(:user, member_in_project: project, member_through_role: role)
end
let(:author) { FactoryBot.create(:user) }
let(:work_package) do
FactoryBot.create(:work_package,
project: project,
author: author)
end
let(:journal) { work_package.journals.first }
let(:instance) { described_class.new }
subject { instance.perform(journal.id, recipient.id, author.id) }
before do
# make sure no actual calls make it into the UserMailer
allow(UserMailer).to receive(:work_package_added).and_return(double('mail', deliver_now: nil))
allow(UserMailer).to receive(:work_package_updated).and_return(double('mail', deliver_now: nil))
end
it 'sends a mail' do
expect(UserMailer)
.to receive(:work_package_added)
.with(recipient, an_instance_of(Journal), author)
subject
end
context 'non-existant journal' do
before do
journal.destroy
end
it 'sends no mail' do
expect(UserMailer).not_to receive(:work_package_added)
subject
end
end
context 'non-existant author' do
before do
author.destroy
end
it 'sends a mail' do
expect(UserMailer).to receive(:work_package_added)
subject
end
it 'uses the deleted user as author' do
expect(UserMailer)
.to receive(:work_package_added)
.with(anything, anything, DeletedUser.first)
subject
end
end
context 'outdated journal' do
before do
# make sure there is a later journal, that supersedes the original one
work_package.subject = 'changed subject'
work_package.save!
end
it 'raises no observable error' do
expect { subject }.not_to raise_error
end
end
context 'update journal' do
let(:journal) { work_package.journals.last }
before do
work_package.add_journal(FactoryBot.create(:user), 'a comment')
work_package.save!
end
it 'sends an update mail' do
expect(UserMailer).to receive(:work_package_updated)
subject
end
it 'sends a mail for the journal' do
expected = Journal.last
expect(UserMailer).to receive(:work_package_updated) do |_recipient, journal, _author|
expect(journal.id).to eq expected.id
expect(journal.notes_id).to eq expected.notes_id
double('mail', deliver_now: nil)
end
subject
end
end
describe 'impersonation' do
describe 'the recipient should become the current user during mail creation' do
before do
expect(UserMailer).to receive(:work_package_added) do
expect(User.current).to eql(recipient)
double('mail', deliver_now: nil)
end
end
it { subject }
end
context 'for a known current user' do
let(:current_user) { FactoryBot.create(:user) }
it 'resets to the previous current user after running' do
User.current = current_user
subject
expect(User.current).to eql(current_user)
end
end
end
describe 'exceptions during delivery' do
before do
mail = double('mail')
allow(mail).to receive(:deliver_now).and_raise(SocketError)
expect(UserMailer).to receive(:work_package_added).and_return(mail)
end
it 'raises the error' do
expect { subject }.to raise_error(SocketError)
end
end
describe 'exceptions during rendering' do
before do
expect(UserMailer).to receive(:work_package_added).and_raise('not today!')
end
it 'swallows the error' do
expect { subject }.not_to raise_error
end
end
end

@ -1,151 +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.
#++
require 'spec_helper'
describe Notifications::JournalCompletedJob, type: :model do
let(:project) { FactoryBot.create(:project) }
let(:permissions) { [:view_work_packages] }
let(:recipient) do
FactoryBot.create(:user, member_in_project: project, member_with_permissions: permissions, login: "johndoe")
end
let(:author) { FactoryBot.create(:user, login: "marktwain") }
let(:send_mail) { true }
subject { described_class.new.perform(journal.id, send_mail) }
before do
# make sure no other calls are made due to WP creation/update
allow(OpenProject::Notifications).to receive(:send) # ... and do nothing
end
context 'for work packages' do
let(:work_package) do
FactoryBot.create(:work_package,
project: project,
author: author,
assigned_to: recipient)
end
let(:journal) { journal1 }
let(:journal1) { work_package.journals.first }
let(:journal2) do
work_package.add_journal author, 'something I have to say'
work_package.save(validate: false)
work_package.journals.last
end
shared_examples_for 'sends notification' do
it 'sends a notification' do
expect(OpenProject::Notifications)
.to receive(:send)
.with(OpenProject::Events::AGGREGATED_WORK_PACKAGE_JOURNAL_READY,
journal: an_instance_of(Journal),
send_mail: send_mail)
subject
end
end
shared_examples_for 'sends no notification' do
it 'sends no notification' do
expect(OpenProject::Notifications)
.not_to receive(:send)
subject
end
end
it_behaves_like 'sends notification'
context 'non-existant journal' do
before do
journal.destroy
end
it_behaves_like 'sends no notification'
end
describe 'journal creation' do
context 'with the work package being created' do
before do
FactoryBot.create(:work_package, project: project)
end
it_behaves_like 'sends notification'
end
context 'with the work package being updated' do
before do
work_package.add_journal(author)
work_package.subject = 'A change to the issue'
work_package.save!(validate: false)
end
it_behaves_like 'sends notification'
end
context 'with the journal being updated with a note' do
before do
work_package.add_journal(author, 'This update has a note')
work_package.save!(validate: false)
end
it_behaves_like 'sends notification'
end
end
end
context 'with wiki page content' do
let(:wiki_page_content) do
wiki = FactoryBot.create(:wiki,
project: project)
FactoryBot.create(:wiki_page_with_content, wiki: wiki).content
end
let(:journal) { journal1 }
let(:journal1) { wiki_page_content.journals.first }
let(:journal2) do
wiki_page_content.add_journal author, 'something I have to say'
wiki_page_content.save(validate: false)
wiki_page_content.journals.last
end
it 'sends a notification' do
expect(OpenProject::Notifications)
.to receive(:send)
.with(OpenProject::Events::AGGREGATED_WIKI_JOURNAL_READY,
journal: an_instance_of(Journal),
send_mail: send_mail)
subject
end
end
end

@ -0,0 +1,144 @@
#-- 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 Notifications::WorkflowJob, type: :model do
subject(:perform_job) do
described_class.new.perform(state, *arguments)
end
let(:send_notification) { true }
let(:notifications) do
[FactoryBot.build_stubbed(:notification),
FactoryBot.build_stubbed(:notification)]
end
describe '#perform' do
context 'with the :create_notifications state' do
let(:state) { :create_notifications }
let(:arguments) { [resource, send_notification] }
let(:resource) { FactoryBot.build_stubbed(:comment) }
let!(:create_service) do
service_instance = instance_double(Notifications::CreateFromModelService)
service_result = instance_double(ServiceResult)
allow(Notifications::CreateFromModelService)
.to receive(:new)
.with(resource)
.and_return(service_instance)
allow(service_instance)
.to receive(:call)
.with(send_notification)
.and_return(service_result)
allow(service_result)
.to receive(:all_results)
.and_return(notifications)
service_instance
end
it 'calls the service to create notifications' do
perform_job
expect(create_service)
.to have_received(:call)
.with(send_notification)
end
it 'schedules a delayed WorkflowJob' do
allow(Time)
.to receive(:current)
.and_return(Time.current)
expected_time = Time.current +
Setting.notification_email_delay_minutes.minutes +
Setting.journal_aggregation_time_minutes.to_i.minutes
expect { perform_job }
.to enqueue_job(described_class)
.with(:send_mails, *notifications.map(&:id))
.at(expected_time)
end
end
context 'with the :send_mails state' do
let(:state) { :send_mails }
let(:arguments) { notifications.map(&:id) }
let!(:mail_service) do
service_instance = instance_double(Notifications::MailService,
call: nil)
allow(Notifications::MailService)
.to receive(:new)
.with(notifications.first)
.and_return(service_instance)
service_instance
end
let!(:digest_job) do
allow(Mails::DigestJob)
.to receive(:schedule)
end
before do
scope = class_double(Notification,
unread_mail: [notifications.first],
unread_mail_digest: [notifications.last])
allow(Notification)
.to receive(:where)
.with(id: notifications.map(&:id))
.and_return(scope)
end
it 'sends mails for all notifications that are marked to send mails' do
perform_job
expect(mail_service)
.to have_received(:call)
end
it 'schedules a digest job for all notifications that are marked for the digest' do
perform_job
expect(Mails::DigestJob)
.to have_received(:schedule)
.with(notifications.last)
end
end
end
end

@ -72,28 +72,6 @@ describe Comment, type: :model do
assert_equal 'something useful', comment.text
end
it 'should create should send notification with settings' do
# news needs a project in order to be notified
# see Redmine::Acts::Journalized::Deprecated#recipients
project = FactoryBot.create(:project)
user = FactoryBot.create(:user, member_in_project: project)
# author is automatically added as watcher
# this makes #user to receive a notification
news = FactoryBot.create(:news, project: project, author: user)
# with notifications for that event turned on
allow(Setting).to receive(:notified_events).and_return(['news_comment_added'])
assert_difference 'ActionMailer::Base.deliveries.size', 1 do
Comment.create!(commented: news, author: user, comments: 'more useful stuff')
end
# with notifications for that event turned off
allow(Setting).to receive(:notified_events).and_return([])
assert_no_difference 'ActionMailer::Base.deliveries.size' do
Comment.create!(commented: news, author: user, comments: 'more useful stuff')
end
end
# TODO: testing #destroy really needed?
it 'should destroy' do
# just setup

Loading…
Cancel
Save