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

840 lines
25 KiB

[26688] In-app notifications (#9399) * Add bell icon to icon font * Add in app notification in top menu * Add fullscreen modal * Add notification modal and items * Style items * Toggle details of item * Mark all read * Add no results box * wip specification for event api * Add events table, query and index * Send out events from WP notification mailer job There we have the recipients present * Add cleanup job for older events with a setting * Hide bell notification when not logged * Add specs for events API index/show * Fix setting yml key * remove pry in event creation * Fix before hook in events API to after_validation * Fix polymorphic association raising exception for aggregated journals * Fix typo in read_ian * Fix yml entry for mentioned * Add read/unread post actions to event API and add specs * Wire up API to frontend * Fix order on events * Switch to unread in notification * Add event query * rename WPEventService * route wp mail sending over events * rename spec methods * author becomes watcher * correct message call signature * rename events to notifications * renname parameter to reflect notification nature * create author watcher for existing work packages * Merge unreadCount from store * Take a stab at polymorphic representers * Fix link generation in polymorphic resources For journals, no title is being generated however * Fix frontend model for context * Use timer for polling * add notification_setting data layer * Fix show resource spec * Fix duplicate class in notification bell item * Add minimal feature spec for notification * API for notification settings * Persist notifications * adapt work package notification creation to notification settings * extract notified_on_all * consolidate wp#recipients * concentrate wp notification in journal service * simplify methods * Remove unused patch endpoint * Add specs for rendering and parsing notification settings * Contract spec * Update service spec * adapt specs * Angular notifications frontend commit e29dced64699eb5f2443b9307c78343c9a58d1ee Author: Wieland Lindenthal <w.lindenthal@forkmerge.com> Date: Mon Jun 21 17:34:50 2021 +0200 Create Akita store and query for notification settings commit 1a45c26c1a0c147d15393e49d2625aca4851a64d Author: Wieland Lindenthal <w.lindenthal@forkmerge.com> Date: Mon Jun 21 11:09:25 2021 +0200 Remove tabs from notificaition settings page commit 0ea21e90c13a197f8bf2cfba1b60ddcff4e5e827 Author: Oliver Günther <mail@oliverguenther.de> Date: Sun Jun 20 21:55:48 2021 +0200 WIP in app settings * migrate notification data * add project visible filter to project query * Add inline-create and table display grouped by project * Add notifications under admin/users * Remove notifications partial * Rename notififcations store to user preferences store * Add setting for self_notified and hook that up to the backend * Add aria-label to table checkboxes * Restyle table and toolbar * replace remains of mail_notifications attribute * initialize notification settings for new user * adapt my_preferences references * reenable no self notified for documents * adapt specs * Avoid has_many :notifcation_settings Rails magically autosaves the user's preferences when the user gets saved, which somehow also tries to save the notfifications even when unchanged. This breaks some specs such as the avatar upload spec. As we can't update the assocation through rails anyway, just delegate to the user for reading instead. * Restore update method of notification settings * Restore update spec * fix spec syntax * lint scss * linting * Fix content_tag for bell icon * Add feature specs for notification settings * Disable ContentTag cop * use visible filter to get projects for notification The visible filter will reduce the project list down to the set of projects visible to the user provided as a parameter. This includes public projects. * test for actual mail sending * adapt me resource path this.apiV3Service.users.me changed its type in 0d6c0b6bc7620de94e00e72b36d6cbc1ec4c8db4 * Implement changed migration * Linting * Add actor to notification representer * Fix factory creating a duplicate WP journal * Add work packages loading and journal details to notification entry component * IAN basic facets, keep and expanded states. * Fix notification bell spec * Render body separately and add auto updating relative time * Add fixedTime title * Add actor to notification entry * Fix clicking links on work package and project * Tiny styling changes on entry row * Disable count in notification if larger than 99 (wont fit) * Introduce virtual scrolling to entry table * allow delaying & prevent mail sending if ain read Introduces a setting to delay mail sending after a journal aggregation time has expired. That way, users can confirm a notification in app. If they do before the delay expires, no mail is sent out additionally for that user. * consolidate notifications (in&out) into shared admin menu Co-authored-by: ulferts <jens.ulferts@googlemail.com> Co-authored-by: Wieland Lindenthal <w.lindenthal@forkmerge.com>
3 years ago
#-- 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'
# 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)
end
let(:recipient_login) { "johndoe" }
let(:other_user) do
FactoryBot.create(:user,
notification_settings: other_user_notification_settings)
end
let(:author) { user_property == :author ? recipient : other_user }
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, all: true),
FactoryBot.build(:in_app_notification_setting, all: true)
]
end
let(:other_user_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, all: false, involved: false, watched: false, mentioned: false),
FactoryBot.build(:in_app_notification_setting, all: false, involved: false, watched: false, mentioned: false)
]
end
let(:user_property) { nil }
let(:work_package) do
wp_attributes = { project: project,
author: other_user,
responsible: other_user,
assigned_to: other_user,
type: project.types.first }
if %i[responsible assigned_to].include?(user_property)
FactoryBot.create(:work_package,
**wp_attributes.merge(user_property => recipient))
elsif user_property == :watcher
FactoryBot.create(:work_package,
**wp_attributes).tap do |wp|
Watcher.new(watchable: wp, user: recipient).save(validate: false)
end
else
# Initialize recipient to have the same behaviour as if the recipient is assigned/responsible
recipient
FactoryBot.create(:work_package,
**wp_attributes)
end
end
let(:journal) { journal_1 }
let(:journal_1) { 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)
work_package.journals.last
end
let(:journal_2_with_status) do
work_package.status = FactoryBot.create(:status)
work_package.save(validate: false)
work_package.journals.last
end
let(:journal_2_with_priority) do
work_package.priority = FactoryBot.create(:priority)
work_package.save(validate: false)
work_package.journals.last
end
let(:send_notifications) { true }
let(:notification_setting) do
%w(work_package_added work_package_updated work_package_note_added status_updated work_package_priority_updated)
end
def call
described_class.call(journal, send_notifications)
end
before do
# make sure no other calls are made due to WP creation/update
allow(OpenProject::Notifications).to receive(:send) # ... and do nothing
login_as(author)
allow(Setting).to receive(:notified_events).and_return(notification_setting)
end
shared_examples_for 'creates notification' do
let(:sender) { author }
let(:event_reason) { :mentioned }
let(:event_channels) do
{
read_ian: false,
read_email: false
}
end
it 'creates a notification' do
events_service = instance_double(Notifications::CreateService)
allow(Notifications::CreateService)
.to receive(:new)
.with(user: sender)
.and_return(events_service)
allow(events_service)
.to receive(:call)
call
expect(events_service)
.to have_received(:call)
.with({ recipient: recipient,
reason: event_reason,
project: journal.project,
actor: journal.user,
journal: journal,
resource: journal.journable }.merge(event_channels))
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
[
FactoryBot.build(:mail_notification_setting, involved: true),
FactoryBot.build(:in_app_notification_setting, involved: true)
]
end
it_behaves_like 'creates notification' do
let(:event_reason) { :involved }
end
context 'assignee has in app notifications disabled' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, all: true),
FactoryBot.build(:in_app_notification_setting, involved: false)
]
end
it_behaves_like 'creates notification' do
let(:event_reason) { :involved }
let(:event_channels) do
{
read_ian: nil,
read_email: false
}
end
end
end
context 'assignee has mail notifications disabled' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, involved: false),
FactoryBot.build(:in_app_notification_setting, involved: true)
]
end
it_behaves_like 'creates notification' do
let(:event_reason) { :involved }
let(:event_channels) do
{
read_ian: false,
read_email: nil
}
end
end
end
context 'assignee has all notifications disabled' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, involved: false),
FactoryBot.build(:in_app_notification_setting, involved: false)
]
end
# Event creation will be prevented by the service
it_behaves_like 'creates notification' do
let(:event_reason) { :involved }
let(:event_channels) do
{
read_ian: nil,
read_email: nil
}
end
end
end
context 'assignee is not allowed to view work packages' do
let(:role) { FactoryBot.create(:role, permissions: []) }
it_behaves_like 'creates no notification'
end
context 'assignee is placeholder user' do
let(:recipient) { FactoryBot.create :placeholder_user }
it_behaves_like 'creates no notification'
end
end
context 'when user is responsible' do
let(:user_property) { :responsible }
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, involved: true),
FactoryBot.build(:in_app_notification_setting, involved: true)
]
end
it_behaves_like 'creates notification' do
let(:event_reason) { :involved }
end
context 'when responsible has in app notifications disabled' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, all: true),
FactoryBot.build(:in_app_notification_setting, involved: false)
]
end
it_behaves_like 'creates notification' do
let(:event_reason) { :involved }
let(:event_channels) do
{
read_ian: nil,
read_email: false
}
end
end
end
context 'when responsible has mail notifications disabled' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, involved: false),
FactoryBot.build(:in_app_notification_setting, involved: true)
]
end
it_behaves_like 'creates notification' do
let(:event_reason) { :involved }
let(:event_channels) do
{
read_ian: false,
read_email: nil
}
end
end
end
context 'when responsible has all notifications disabled' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, involved: false),
FactoryBot.build(:in_app_notification_setting, involved: false)
]
end
# Event creation will be prevented by the service
it_behaves_like 'creates notification' do
let(:event_reason) { :involved }
let(:event_channels) do
{
read_ian: nil,
read_email: nil
}
end
end
end
context 'when responsible is not allowed to view work packages' do
let(:role) { FactoryBot.create(:role, permissions: []) }
it_behaves_like 'creates no notification'
end
context 'when responsible is placeholder user' do
let(:recipient) { FactoryBot.create :placeholder_user }
it_behaves_like 'creates no notification'
end
end
context 'when user is watcher' do
let(:user_property) { :watcher }
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, watched: true),
FactoryBot.build(:in_app_notification_setting, watched: true)
]
end
it_behaves_like 'creates notification' do
let(:event_reason) { :watched }
end
context 'when watcher has in app notifications disabled' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, watched: true),
FactoryBot.build(:in_app_notification_setting, watched: false)
]
end
it_behaves_like 'creates notification' do
let(:event_reason) { :watched }
let(:event_channels) do
{
read_ian: nil,
read_email: false
}
end
end
end
context 'when watcher has mail notifications disabled' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, watched: false),
FactoryBot.build(:in_app_notification_setting, watched: true)
]
end
it_behaves_like 'creates notification' do
let(:event_reason) { :watched }
let(:event_channels) do
{
read_ian: false,
read_email: nil
}
end
end
end
context 'when watcher has all notifications disabled' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, watched: false),
FactoryBot.build(:in_app_notification_setting, watched: false)
]
end
# Event creation will be prevented by the service
it_behaves_like 'creates notification' do
let(:event_reason) { :watched }
let(:event_channels) do
{
read_ian: nil,
read_email: nil
}
end
end
end
context 'when watcher is not allowed to view work packages' do
let(:role) { FactoryBot.create(:role, permissions: []) }
it_behaves_like 'creates no notification'
end
end
context 'when user is notified about everything' do
let(:user_property) { nil }
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, all: true),
FactoryBot.build(:in_app_notification_setting, all: true)
]
end
it_behaves_like 'creates notification' do
let(:event_reason) { :subscribed }
end
context 'with in app notifications disabled' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, all: true),
FactoryBot.build(:in_app_notification_setting, all: false)
]
end
it_behaves_like 'creates notification' do
let(:event_reason) { :subscribed }
let(:event_channels) do
{
read_ian: nil,
read_email: false
}
end
end
end
context 'with mail notifications disabled' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, all: false),
FactoryBot.build(:in_app_notification_setting, all: true)
]
end
it_behaves_like 'creates notification' do
let(:event_reason) { :subscribed }
let(:event_channels) do
{
read_ian: false,
read_email: nil
}
end
end
end
context 'with all disabled' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, all: false),
FactoryBot.build(:in_app_notification_setting, all: false)
]
end
it_behaves_like 'creates no notification'
end
context 'with all disabled as a default but enabled in the project' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, all: false),
FactoryBot.build(:in_app_notification_setting, all: false),
FactoryBot.build(:mail_notification_setting, project: project, all: true),
FactoryBot.build(:in_app_notification_setting, project: project, all: true)
]
end
it_behaves_like 'creates notification' do
let(:event_reason) { :subscribed }
end
end
context 'with all enabled as a default but disabled in the project' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, all: true),
FactoryBot.build(:in_app_notification_setting, all: true),
FactoryBot.build(:mail_notification_setting, project: project, all: false),
FactoryBot.build(:in_app_notification_setting, project: project, all: false)
]
end
it_behaves_like 'creates no notification'
end
context 'when not allowed to view work packages' do
let(:role) { FactoryBot.create(:role, permissions: []) }
it_behaves_like 'creates no notification'
end
end
context 'when notification for work_package_added disabled' do
let(:notification_setting) { %w(work_package_updated work_package_note_added) }
let(:user_property) { :assigned_to }
it_behaves_like 'creates no notification'
end
context 'when the journal has a note' do
let(:journal) { journal_2_with_notes }
let(:user_property) { :assigned_to }
context 'notification for work_package_updated and work_package_note_added disabled' do
let(:notification_setting) { %w(work_package_added status_updated work_package_priority_updated) }
it_behaves_like 'creates no notification'
end
context 'notification for work_package_updated enabled' do
let(:notification_setting) { %w(work_package_updated) }
it_behaves_like 'creates notification' do
let(:event_reason) { :involved }
end
end
context 'notification for work_package_note_added enabled' do
let(:notification_setting) { %w(work_package_note_added) }
it_behaves_like 'creates notification' do
let(:event_reason) { :involved }
end
end
end
context 'when the journal has status update' do
let(:journal) { journal_2_with_status }
let(:user_property) { :assigned_to }
context 'notification for work_package_updated and status_updated disabled' do
let(:notification_setting) { %w(work_package_added work_package_note_added work_package_priority_updated) }
it_behaves_like 'creates no notification'
end
context 'notification for work_package_updated enabled' do
let(:notification_setting) { %w(work_package_updated) }
it_behaves_like 'creates notification' do
let(:event_reason) { :involved }
end
end
context 'notification for status_updated enabled' do
let(:notification_setting) { %w(status_updated) }
it_behaves_like 'creates notification' do
let(:event_reason) { :involved }
end
end
end
context 'when the journal has priority' do
let(:journal) { journal_2_with_priority }
let(:user_property) { :assigned_to }
context 'notification for work_package_updated and work_package_priority_updated disabled' do
let(:notification_setting) { %w(work_package_added work_package_note_added status_updated) }
it_behaves_like 'creates no notification'
end
context 'notification for work_package_updated enabled' do
let(:notification_setting) { %w(work_package_updated) }
it_behaves_like 'creates notification' do
let(:event_reason) { :involved }
end
end
context 'notification for work_package_priority_updated enabled' do
let(:notification_setting) { %w(work_package_priority_updated) }
it_behaves_like 'creates notification' do
let(:event_reason) { :involved }
end
end
end
context 'when the author has been deleted' do
let!(:deleted_user) { DeletedUser.first }
let(:user_property) { :assigned_to }
before do
work_package
author.destroy
end
it_behaves_like 'creates notification' do
let(:sender) { deleted_user }
let(:event_reason) { :involved }
end
end
context 'when user is mentioned' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, mentioned: true),
FactoryBot.build(:in_app_notification_setting, mentioned: true)
]
end
shared_examples_for 'group mention' do
context 'group member is allowed to view the work package' do
context 'user wants to receive notifications' do
it_behaves_like 'creates notification'
end
context 'user disabled mention notifications' do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, mentioned: false),
FactoryBot.build(:in_app_notification_setting, mentioned: false)
]
end
# Event creation will be prevented by the service
it_behaves_like 'creates notification' do
let(:event_channels) do
{
read_ian: nil,
read_email: nil
}
end
end
end
end
context 'group is not allowed to view the work package' do
let(:group_role) { FactoryBot.create(:role, permissions: []) }
let(:role) { FactoryBot.create(:role, permissions: []) }
it_behaves_like 'creates no notification'
context 'but group member is allowed individually' do
let(:role) { FactoryBot.create(:role, permissions: [:view_work_packages]) }
it_behaves_like 'creates notification'
end
end
end
shared_examples_for 'mentioned' do
context 'for users' do
context "mentioned is allowed to view the work package" do
context "The added text contains a login name" do
let(:note) { "Hello user:\"#{recipient_login}\"" }
context "that is pretty normal word" do
it_behaves_like 'creates notification'
end
context "that is an email address" do
let(:recipient_login) { "foo@bar.com" }
it_behaves_like 'creates notification'
end
end
context "The added text contains a user ID" do
let(:note) { "Hello user##{recipient.id}" }
it_behaves_like 'creates notification'
end
context "The added text contains a user mention tag in one way" do
let(:note) do
<<~NOTE
Hello <mention class="mention" data-id="#{recipient.id}" data-type="user" data-text="@#{recipient.name}">@#{recipient.name}</mention>
NOTE
end
it_behaves_like 'creates notification'
end
context "The added text contains a user mention tag in the other way" do
let(:note) do
<<~NOTE
Hello <mention class="mention" data-type="user" data-id="#{recipient.id}" data-text="@#{recipient.name}">@#{recipient.name}</mention>
NOTE
end
it_behaves_like 'creates notification'
end
context "the recipient turned off mention notifications" do
let(:recipient_notification_settings) do
[
FactoryBot.build(:mail_notification_setting, mentioned: false),
FactoryBot.build(:in_app_notification_setting, mentioned: false)
]
end
let(:note) do
"Hello user:\"#{recipient.login}\", hey user##{recipient.id}"
end
# Event creation will be prevented by the service
it_behaves_like 'creates notification' do
let(:event_channels) do
{
read_ian: nil,
read_email: nil
}
end
end
end
end
context "mentioned user is not allowed to view the work package" do
let(:role) { FactoryBot.create(:role, permissions: []) }
let(:note) do
"Hello user:#{recipient.login}, hey user##{recipient.id}"
end
it_behaves_like 'creates no notification'
end
end
context 'for groups' do
let(:group_role) { FactoryBot.create(:role, permissions: %i[view_work_packages]) }
let(:group) do
FactoryBot.create(:group, members: recipient) do |group|
Members::CreateService
.new(user: nil, contract_class: EmptyContract)
.call(project: project, principal: group, roles: [group_role])
end
end
context 'on a hash/id based mention' do
let(:note) do
"Hello group##{group.id}"
end
it_behaves_like 'group mention'
end
context 'on a tag based mention with the type after' do
let(:note) do
<<~NOTE
Hello <mention class="mention" data-id="#{group.id}" data-type="group" data-text="@#{group.name}">@#{group.name}</mention>
NOTE
end
it_behaves_like 'group mention'
end
context 'on a tag based mention with the type before' do
let(:note) do
<<~NOTE
Hello <mention data-type="group" class="mention" data-id="#{group.id}" data-text="@#{group.name}">@#{group.name}</mention>
NOTE
end
it_behaves_like 'group mention'
end
end
end
context 'in the journal notes' do
let(:journal) { journal_2_with_notes }
let(:journal_2_with_notes) do
work_package.add_journal author, note
work_package.save(validate: false)
work_package.journals.last
end
it_behaves_like 'mentioned'
end
context 'in the description' do
let(:journal) { journal_2_with_description }
let(:journal_2_with_description) do
work_package.description = note
work_package.save(validate: false)
work_package.journals.last
end
it_behaves_like 'mentioned'
end
context 'in the subject' do
let(:journal) { journal_2_with_subject }
let(:journal_2_with_subject) do
work_package.subject = note
work_package.save(validate: false)
work_package.journals.last
end
it_behaves_like 'mentioned'
end
end
context 'when aggregated journal is empty' do
let(:journal) { journal_2_empty_change }
let(:journal_2_empty_change) do
work_package.add_journal(author, 'temp')
work_package.save(validate: false)
work_package.journals.last.tap do |j|
j.update_column(:notes, nil)
end
end
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)
)
expect(Notifications::JournalWpNotificationService)
.to have_received(:call)
end
end
# rubocop:enable Rspec/MultipleMemoizedHelpers