Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions app/contracts/notifications/create_mention_from_api_contract.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 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 COPYRIGHT and LICENSE files for more details.
#++

module Notifications
# Validates notifications that are created on behalf of the current user
# mentioning another user in a resource, e.g. a document
# (see Notifications::CreateFromApiService).
#
# A resource type needs to opt in to this by defining `api_mention_permission`
# on its Notifications::CreateFromModelService strategy. That permission is required
# for the mentioning user while the strategy's `permission` is required for the
# mentioned user (recipient).
class CreateMentionFromApiContract < CreateContract
validate :validate_resource_mentionable
validate :validate_recipient_mentionable

protected

# Only mentions can be created this way. This replaces the check of the
# parent contract which only requires a reason to be present.
def validate_reason_present
errors.add(:reason, :inclusion) unless model.reason_mentioned?
end

def validate_resource_mentionable
if strategy.nil?
errors.add(:resource, :mentions_not_supported)
elsif !resource_visible?
errors.add(:base, :error_not_found)
elsif !user.allowed_in_project?(strategy.api_mention_permission, resource_project)
errors.add(:base, :error_unauthorized)
end
end

def validate_recipient_mentionable
return if model.recipient.nil? || strategy.nil? || !resource_visible?

if !recipient_active?
errors.add(:recipient, :not_an_active_user)
elsif !recipient_allowed?
errors.add(:recipient, :not_allowed)
end
end

private

def strategy
return @strategy if defined?(@strategy)

strategy = Notifications::CreateFromModelService.strategy_for(model.resource_type)

@strategy = strategy.respond_to?(:api_mention_permission) ? strategy : nil
end

def resource_visible?
return @resource_visible if defined?(@resource_visible)

@resource_visible = model.resource.present? && model.resource.visible?(user)
end

def recipient_active?
model.recipient.active? && !model.recipient.builtin?
end

def recipient_allowed?
model.recipient.allowed_in_project?(strategy.permission(nil, :mentioned), resource_project)
end

def resource_project
model.resource.project
end
end
end
3 changes: 2 additions & 1 deletion app/models/notification_setting.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ def self.email_settings
belongs_to :user

include Scopes::Scoped
scopes :applicable
scopes :applicable,
:enabled_for

# rubocop:disable Naming/PredicateMethod
def start_date_active
Expand Down
45 changes: 45 additions & 0 deletions app/models/notification_settings/scopes/enabled_for.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 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 COPYRIGHT and LICENSE files for more details.
#++

module NotificationSettings::Scopes
module EnabledFor
extend ActiveSupport::Concern

class_methods do
# Returns the notification settings that prevail in the given project
# (see #applicable) and have the given reason (e.g. :mentioned) enabled.
# A project specific setting overrides the global one in both directions.
def enabled_for(reason, project)
where(reason => true)
.applicable(project)
end
Comment on lines +39 to +42

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These auto injected things kinda get me off guard every time.

My feeling is that it would be nicer to have this simple scope explicit on the class it's being used. Unless it's used in multiple places, then I guess extracting into a concern kidna makes sense.

Still no action necessary, just me preferring explicitness.

end
end
end
111 changes: 111 additions & 0 deletions app/services/notifications/create_from_api_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 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 COPYRIGHT and LICENSE files for more details.
#++

module Notifications
# Creates a notification requested via the API. Currently, only mentions are supported,
# i.e. notifications for a user (recipient) being mentioned by the current user
# in a resource, e.g. a document. Contrary to notifications created by
# Notifications::CreateFromModelService, these notifications are not bound to a journal.
#
# Mentioning is validated by Notifications::CreateMentionFromApiContract. If the mention is valid
# but the recipient is not to be notified, because the recipient mentioned themselves
# or has disabled notifications for being mentioned, a successful result without a
# notification is returned.
#
# If the resource's strategy supports mails for the reason, the mail is sent right away,
# just like Notifications::WorkflowJob does for mentions in journals.
class CreateFromApiService
attr_reader :user

def initialize(user:)
@user = user
end

# @param reason [String, Symbol] has to be "mentioned"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the parameter has to be the same constant value, do we need it?

# @param resource_type [String] class name of the resource, e.g. "Document"
# @param resource_id [Integer, String]
# @param recipient_id [Integer, String]
def call(reason:, resource_type:, resource_id:, recipient_id:)
notification = build_notification(reason:, resource_type:, resource_id:, recipient_id:)

contract = Notifications::CreateMentionFromApiContract.new(notification, user)

return ServiceResult.failure(result: notification, errors: contract.errors) unless contract.valid?
return ServiceResult.success if skip_notification?(notification)

Notifications::CreateService
.new(user:, model: notification)
.call
.tap { |result| send_mail(result.result) if result.success? }
end

private

def build_notification(reason:, resource_type:, resource_id:, recipient_id:)
reason = reason.to_s
strategy = Notifications::CreateFromModelService.strategy_for(resource_type)

Notification.new(
reason: Notification.reasons.key?(reason) ? reason : nil,
# Only types having a strategy are kept. That way, arbitrary (non existing) class names,
# which would fail to load the resource, never end up on the notification. The contract
# reports the missing support for the resource type.
resource_type: strategy ? resource_type : nil,
resource_id:,
recipient_id:,
actor: user,
journal: nil,
read_ian: strategy&.supports_ian?(reason) ? false : nil,
mail_alert_sent: strategy&.supports_mail?(reason) ? false : nil,
mail_reminder_sent: nil
)
end

def send_mail(notification)
return unless notification.mail_alert_sent == false

# Enqueue mail asynchronously (not waiting for delivery)
Notifications::MailService
.new(notification)
.call
end

def skip_notification?(notification)
notification.recipient == user ||
!mention_notification_enabled?(notification)
end

def mention_notification_enabled?(notification)
NotificationSetting
.enabled_for(NotificationSetting::MENTIONED, notification.resource.project)
.exists?(user: notification.recipient)
end
end
end
29 changes: 22 additions & 7 deletions app/services/notifications/create_from_model_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ class Notifications::CreateFromModelService
QUOTED_LINES_PATTERN = /^ ?> .*$/
MENTION_PATTERN = Regexp.new(COMBINED_MENTION_PATTERN)

def self.strategy_for(resource_class_name)
# The resource_class_name can come from the API, so only plain class names are accepted
return unless resource_class_name.to_s.match?(/\A[A-Z]\w*\z/)

strategy_name = :"#{resource_class_name}Strategy"

const_get(strategy_name, false) if const_defined?(strategy_name, false)
end

def initialize(model)
self.model = model
end
Expand Down Expand Up @@ -135,8 +144,11 @@ def update_notification(recipient_id, reason)
end

def delete_outdated_notifications(current_recipient_ids)
# Scoping by resource is necessary as for models without journals (e.g. comments)
# journal is nil which would otherwise match all notifications without a journal,
# e.g. date alerts or mentions created via the API.
Notification
.where(journal:)
.where(resource:, journal:)
.where.not(recipient_id: current_recipient_ids)
.destroy_all
end
Expand Down Expand Up @@ -239,14 +251,19 @@ def settings_of_scheduled
end

def project_applicable_settings(user_scope, project, reason)
settings_for_allowed_users(user_scope, reason)
.applicable(project)
NotificationSetting
.enabled_for(reason, project)
.where(user: allowed_users(user_scope, reason))
end

def settings_for_allowed_users(user_scope, reason)
NotificationSetting
.where(reason => true)
.where(user: user_scope.where(id: User.allowed(strategy.permission(journal, reason), project)))
.where(user: allowed_users(user_scope, reason))
end

def allowed_users(user_scope, reason)
user_scope.where(id: User.allowed(strategy.permission(journal, reason), project))
end

# Returns the text of the model (work package description and subject, message subject and content)
Expand Down Expand Up @@ -354,9 +371,7 @@ def receivers_hash
end

def strategy
@strategy ||= if self.class.const_defined?(:"#{resource.class}Strategy")
"#{self.class}::#{resource.class}Strategy".constantize
end
@strategy ||= self.class.strategy_for(resource.class)
end

def supported?
Expand Down
6 changes: 6 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,12 @@ en:
date:
format: "%{message}"
taken: "A non-working day already exists for %{value}."
notification:
attributes:
recipient:
not_an_active_user: "is not an active user."
resource:
mentions_not_supported: "does not support mentioning users."
notifications:
at_least_one_channel: "At least one channel for sending notifications needs to be specified."
attributes:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Example: Notification created response
---
value:
_type: Notification
id: 42
_links:
resource:
href: '/api/v3/documents/5'
title: Meeting minutes
recipient:
href: '/api/v3/users/13'
title: Darth Nihilus
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Example: Notification mention create request
---
value:
reason: mentioned
_links:
resource:
href: '/api/v3/documents/5'
recipient:
href: '/api/v3/users/13'
Loading
Loading