separate date alert creation for individual users

pull/11880/head
ulferts 2 years ago
parent a149e9a787
commit 85bdcaf9c3
No known key found for this signature in database
GPG Key ID: A205708DE1284017
  1. 36
      app/workers/notifications/create_date_alerts_notifications_job.rb
  2. 16
      app/workers/notifications/create_date_alerts_notifications_job/alertable_work_packages.rb
  3. 41
      app/workers/notifications/create_date_alerts_notifications_job/service.rb
  4. 63
      app/workers/notifications/schedule_date_alerts_notifications_job.rb
  5. 65
      app/workers/notifications/schedule_date_alerts_notifications_job/service.rb
  6. 18
      config/initializers/cronjobs.rb
  7. 37
      db/migrate/20230105073117_remove_renamed_date_alert_job.rb
  8. 4
      spec/features/notifications/notification_center/notification_center_date_alerts_spec.rb
  9. 191
      spec/workers/notifications/create_date_alerts_notifications_job_spec.rb
  10. 198
      spec/workers/notifications/schedule_date_alerts_notifications_job_spec.rb

@ -23,41 +23,17 @@
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# #
# See docs/COPYRIGHT.rdoc for more details. # See COPYRIGHT and LICENSE files for more details.
#++ #++
module Notifications module Notifications
# Creates date alerts for users whose local time is 1:00 am. class CreateDateAlertsNotificationsJob < ApplicationJob
class CreateDateAlertsNotificationsJob < Cron::CronJob def perform(user)
# runs every quarter of an hour, so 00:00, 00:15,..., 15:30, 15:45, 16:00, ...
self.cron_expression = '*/15 * * * *'
def perform
return unless EnterpriseToken.allows_to?(:date_alerts) return unless EnterpriseToken.allows_to?(:date_alerts)
service = Service.new(times_from_scheduled_to_execution) Service
service.call .new(user)
end .call
# Returns times from scheduled execution time to current time in 15 minutes
# steps.
#
# As scheduled execution time can be different from current time by more
# than 15 minutes when workers are busy, all times at 15 minutes interval
# between scheduled time and current time need to be considered to match
# with 1:00am in a time zone.
def times_from_scheduled_to_execution
time = scheduled_time
times = []
begin
times << time
time += 15.minutes
end while time < Time.current
times
end
def scheduled_time
self.class.delayed_job.run_at.then { |t| t.change(min: t.min / 15 * 15) }
end end
end end
end end

@ -23,7 +23,7 @@
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# #
# See docs/COPYRIGHT.rdoc for more details. # See COPYRIGHT and LICENSE files for more details.
#++ #++
class Notifications::CreateDateAlertsNotificationsJob::AlertableWorkPackages class Notifications::CreateDateAlertsNotificationsJob::AlertableWorkPackages
@ -34,19 +34,21 @@ class Notifications::CreateDateAlertsNotificationsJob::AlertableWorkPackages
end end
def alertable_for_start def alertable_for_start
find_alertables alertable_for("start_alert")
.filter_map { |row| row["id"] if row["start_alert"] }
.then { |ids| WorkPackage.where(id: ids) }
end end
def alertable_for_due def alertable_for_due
find_alertables alertable_for("due_alert")
.filter_map { |row| row["id"] if row["due_alert"] }
.then { |ids| WorkPackage.where(id: ids) }
end end
private private
def alertable_for(alert)
find_alertables
.filter_map { |row| row["id"] if row[alert] }
.then { |ids| WorkPackage.where(id: ids) }
end
def find_alertables def find_alertables
@find_alertables ||= ActiveRecord::Base.connection.execute(query).to_a @find_alertables ||= ActiveRecord::Base.connection.execute(query).to_a
end end

@ -26,36 +26,23 @@
# See COPYRIGHT and LICENSE files for more details. # See COPYRIGHT and LICENSE files for more details.
#++ #++
# Creates date alerts notifications for users whose local time is 1am for the
# given run_times.
class Notifications::CreateDateAlertsNotificationsJob::Service class Notifications::CreateDateAlertsNotificationsJob::Service
attr_reader :run_times def initialize(user)
@user = user
# @param run_times [Array<DateTime>] the times for which the service is run.
# Must be multiple of 15 minutes (xx:00, xx:15, xx:30, or xx:45).
def initialize(run_times)
@run_times = run_times
end end
def call def call
return unless EnterpriseToken.allows_to?(:date_alerts) return unless EnterpriseToken.allows_to?(:date_alerts)
time_zones = time_zones_covering_1am_local_time Time.use_zone(user.time_zone) do
return if time_zones.empty? send_date_alert_notifications(user)
# warning: there may be a subtle bug here: if many run_times are given, time
# zones will have different time shifting. This should be ok: as the period
# covered is small this should not have any impact. If the period is more
# than 23h, then the day will change.
Time.use_zone(time_zones.first) do
User.with_time_zone(time_zones).find_each do |user|
send_date_alert_notifications(user)
end
end end
end end
private private
attr_accessor :user
def send_date_alert_notifications(user) def send_date_alert_notifications(user)
alertables = Notifications::CreateDateAlertsNotificationsJob::AlertableWorkPackages.new(user) alertables = Notifications::CreateDateAlertsNotificationsJob::AlertableWorkPackages.new(user)
create_date_alert_notifications(user, alertables.alertable_for_start, :date_alert_start_date) create_date_alert_notifications(user, alertables.alertable_for_start, :date_alert_start_date)
@ -86,20 +73,4 @@ class Notifications::CreateDateAlertsNotificationsJob::Service
reason: reason:
) )
end end
def time_zones_covering_1am_local_time
UserPreferences::UpdateContract
.assignable_time_zones
.select { |time_zone| executing_at_1am_for_timezone?(time_zone) }
.map { |time_zone| time_zone.tzinfo.canonical_zone.name }
end
def executing_at_1am_for_timezone?(time_zone)
run_times.any? { |time| is_1am?(time, time_zone) }
end
def is_1am?(time, time_zone)
local_time = time.in_time_zone(time_zone)
local_time.strftime('%H:%M') == '01:00'
end
end end

@ -0,0 +1,63 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 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
# Creates date alert jobs for users whose local time is 1:00 am.
class ScheduleDateAlertsNotificationsJob < Cron::CronJob
# runs every quarter of an hour, so 00:00, 00:15,..., 15:30, 15:45, 16:00, ...
self.cron_expression = '*/15 * * * *'
def perform
return unless EnterpriseToken.allows_to?(:date_alerts)
service = Service.new(times_from_scheduled_to_execution)
service.call
end
# Returns times from scheduled execution time to current time in 15 minutes
# steps.
#
# As scheduled execution time can be different from current time by more
# than 15 minutes when workers are busy, all times at 15 minutes interval
# between scheduled time and current time need to be considered to match
# with 1:00am in a time zone.
def times_from_scheduled_to_execution
time = scheduled_time
times = []
begin
times << time
time += 15.minutes
end while time < Time.current
times
end
def scheduled_time
self.class.delayed_job.run_at.then { |t| t.change(min: t.min / 15 * 15) }
end
end
end

@ -0,0 +1,65 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 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.
#++
# Creates date alerts notifications for users whose local time is 1am for the
# given run_times.
class Notifications::ScheduleDateAlertsNotificationsJob::Service
attr_reader :run_times
# @param run_times [Array<DateTime>] the times for which the service is run.
# Must be multiple of 15 minutes (xx:00, xx:15, xx:30, or xx:45).
def initialize(run_times)
@run_times = run_times
end
def call
return unless EnterpriseToken.allows_to?(:date_alerts)
User.with_time_zone(time_zones_covering_1am_local_time).find_each do |user|
Notifications::CreateDateAlertsNotificationsJob.perform_later(user)
end
end
private
def time_zones_covering_1am_local_time
UserPreferences::UpdateContract
.assignable_time_zones
.select { |time_zone| executing_at_1am_for_timezone?(time_zone) }
.map { |time_zone| time_zone.tzinfo.canonical_zone.name }
end
def executing_at_1am_for_timezone?(time_zone)
run_times.any? { |time| is_1am?(time, time_zone) }
end
def is_1am?(time, time_zone)
local_time = time.in_time_zone(time_zone)
local_time.strftime('%H:%M') == '01:00'
end
end

@ -2,14 +2,14 @@
OpenProject::Application.configure do |application| OpenProject::Application.configure do |application|
application.config.to_prepare do application.config.to_prepare do
::Cron::CronJob.register! ::Cron::ClearOldSessionsJob, Cron::CronJob.register! Cron::ClearOldSessionsJob,
::Cron::ClearTmpCacheJob, Cron::ClearTmpCacheJob,
::Cron::ClearUploadedFilesJob, Cron::ClearUploadedFilesJob,
::OAuth::CleanupJob, OAuth::CleanupJob,
::PaperTrailAudits::CleanupJob, PaperTrailAudits::CleanupJob,
::Attachments::CleanupUncontaineredJob, Attachments::CleanupUncontaineredJob,
::Notifications::CreateDateAlertsNotificationsJob, Notifications::ScheduleDateAlertsNotificationsJob,
::Notifications::ScheduleReminderMailsJob, Notifications::ScheduleReminderMailsJob,
::Ldap::SynchronizationJob Ldap::SynchronizationJob
end end
end end

@ -0,0 +1,37 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 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.
#++
class RemoveRenamedDateAlertJob < ActiveRecord::Migration[6.0]
def up
# The job has been renamed to Notifications::ScheduleDateAlertsNotificationsJob.
# The new job will be added on restarting the application.
Delayed::Job
.where('handler LIKE ?', "%job_class: Notifications::CreateDateAlertsNotificationsJob%")
.delete_all
end
end

@ -166,13 +166,15 @@ describe "Notification center date alerts", js: true, with_settings: { journal_a
end end
def run_create_date_alerts_notifications_job def run_create_date_alerts_notifications_job
create_date_alerts_service = Notifications::CreateDateAlertsNotificationsJob::Service.new([timezone_time('1:00', time_zone)]) create_date_alerts_service = Notifications::ScheduleDateAlertsNotificationsJob::Service
.new([timezone_time('1:00', time_zone)])
travel_to(timezone_time('1:04', time_zone)) travel_to(timezone_time('1:04', time_zone))
create_date_alerts_service.call create_date_alerts_service.call
end end
before do before do
run_create_date_alerts_notifications_job run_create_date_alerts_notifications_job
perform_enqueued_jobs
login_as user login_as user
visit notifications_center_path visit notifications_center_path
end end

@ -23,7 +23,7 @@
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# #
# See docs/COPYRIGHT.rdoc for more details. # See COPYRIGHT and LICENSE files for more details.
#++ #++
require 'spec_helper' require 'spec_helper'
@ -48,43 +48,19 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: %
shared_let(:in_3_days) { today + 3.days } shared_let(:in_3_days) { today + 3.days }
shared_let(:in_7_days) { today + 7.days } shared_let(:in_7_days) { today + 7.days }
shared_let(:user_paris) do shared_let(:user) do
create( create(
:user, :user,
firstname: 'Paris', firstname: 'Paris',
preferences: { time_zone: timezone_paris.name } preferences: { time_zone: timezone_paris.name }
) )
end end
shared_let(:user_berlin) do
create(
:user,
firstname: 'Berlin',
preferences: { time_zone: timezone_berlin.name }
)
end
shared_let(:user_kathmandu) do
create(
:user,
firstname: 'Kathmandu',
preferences: { time_zone: timezone_kathmandu.name }
)
end
shared_let(:alertable_work_packages) do shared_let(:alertable_work_packages) do
create_list(:work_package, 2, project:, author: user_paris) create_list(:work_package, 2, project:, author: user)
end
let(:scheduled_job) do
described_class.ensure_scheduled!
described_class.delayed_job
end end
before do let(:job) { described_class }
# We need to access the job as stored in the database to get at the run_at time persisted there
allow(ActiveJob::Base)
.to receive(:queue_adapter)
.and_return(ActiveJob::QueueAdapters::DelayedJobAdapter.new)
end
define :have_a_start_date_alert_notification_for do |work_package| define :have_a_start_date_alert_notification_for do |work_package|
match do |user| match do |user|
@ -113,9 +89,9 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: %
when WorkPackage then %i[id start_date due_date assigned_to_id responsible_id] when WorkPackage then %i[id start_date due_date assigned_to_id responsible_id]
end end
formatted_pairs = object formatted_pairs = object
.slice(*keys) .slice(*keys)
.map { |k, v| "#{k}: #{v.is_a?(Date) ? v.to_s : v.inspect}" } .map { |k, v| "#{k}: #{v.is_a?(Date) ? v.to_s : v.inspect}" }
.join(', ') .join(', ')
"#<#{formatted_pairs}>" "#<#{formatted_pairs}>"
end end
end end
@ -147,19 +123,15 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: %
when WorkPackage then %i[id start_date due_date assigned_to_id responsible_id] when WorkPackage then %i[id start_date due_date assigned_to_id responsible_id]
end end
formatted_pairs = object formatted_pairs = object
.slice(*keys) .slice(*keys)
.map { |k, v| "#{k}: #{v.is_a?(Date) ? v.to_s : v.inspect}" } .map { |k, v| "#{k}: #{v.is_a?(Date) ? v.to_s : v.inspect}" }
.join(', ') .join(', ')
"#<#{formatted_pairs}>" "#<#{formatted_pairs}>"
end end
end end
def set_scheduled_time(run_at)
scheduled_job.update_column(:run_at, run_at)
end
def alertable_work_package(attributes = {}) def alertable_work_package(attributes = {})
assignee = attributes.slice(:responsible, :responsible_id).any? ? nil : user_paris assignee = attributes.slice(:responsible, :responsible_id).any? ? nil : user
attributes = attributes.reverse_merge( attributes = attributes.reverse_merge(
start_date: in_1_day, start_date: in_1_day,
assigned_to: assignee assigned_to: assignee
@ -179,9 +151,8 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: %
end end
def run_job(scheduled_at: '1:00', local_time: '1:04', timezone: timezone_paris) def run_job(scheduled_at: '1:00', local_time: '1:04', timezone: timezone_paris)
set_scheduled_time(timezone_time(scheduled_at, timezone))
travel_to(timezone_time(local_time, timezone)) do travel_to(timezone_time(local_time, timezone)) do
scheduled_job.reload.invoke_job job.perform_now(user)
yield yield
end end
@ -193,57 +164,63 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: %
closed_work_package = alertable_work_package(status: status_closed) closed_work_package = alertable_work_package(status: status_closed)
run_job do run_job do
expect(user_paris).to have_a_start_date_alert_notification_for(open_work_package) expect(user).to have_a_start_date_alert_notification_for(open_work_package)
expect(user_paris).not_to have_a_start_date_alert_notification_for(closed_work_package) expect(user).not_to have_a_start_date_alert_notification_for(closed_work_package)
end end
end end
it 'creates date alert notifications only for users whose local time is 1:00 am when the job is executed' do it 'creates date alert notifications if user is assigned to the work package' do
work_package_for_paris_user = alertable_work_package(assigned_to: user_paris, work_package_assigned = alertable_work_package(assigned_to: user)
start_date: timezone_paris.today + 1.day)
work_package_for_kathmandu_user = alertable_work_package(assigned_to: user_kathmandu,
start_date: timezone_kathmandu.today + 1.day)
run_job(timezone: timezone_paris) do run_job do
expect(user_paris).to have_a_start_date_alert_notification_for(work_package_for_paris_user) expect(user).to have_a_start_date_alert_notification_for(work_package_assigned)
expect(user_kathmandu).not_to have_a_start_date_alert_notification_for(work_package_for_kathmandu_user)
end end
end
# change timezone to cover Kathmandu it 'creates date alert notifications if user is accountable of the work package' do
run_job(timezone: timezone_kathmandu) do work_package_accountable = alertable_work_package(responsible: user)
expect(user_kathmandu).to have_a_start_date_alert_notification_for(work_package_for_kathmandu_user)
run_job do
expect(user).to have_a_start_date_alert_notification_for(work_package_accountable)
end end
end end
it 'creates date alert notifications if user is assigned to or accountable of the work package' do it 'creates date alert notifications if user is watcher of the work package' do
work_package_assigned = alertable_work_package(assigned_to: user_paris) work_package_watched = alertable_work_package(responsible: nil)
work_package_accountable = alertable_work_package(responsible: user_paris) build(:watcher, watchable: work_package_watched, user:).save(validate: false)
run_job do run_job do
expect(user_paris).to have_a_start_date_alert_notification_for(work_package_assigned) expect(user).to have_a_start_date_alert_notification_for(work_package_watched)
expect(user_paris).to have_a_start_date_alert_notification_for(work_package_accountable)
end end
end end
it 'creates start and finish date alert notifications based on user notification settings' do it 'creates start date alert notifications based on user notification settings' do
user_paris.notification_settings.first.update( user.notification_settings.first.update(
start_date: 1, start_date: 1,
due_date: nil due_date: nil
) )
user_berlin.notification_settings.first.update( work_package = alertable_work_package(assigned_to: user,
start_date: in_1_day,
due_date: in_3_days)
run_job do
expect(user).to have_a_start_date_alert_notification_for(work_package)
expect(user).not_to have_a_due_date_alert_notification_for(work_package)
end
end
it 'creates due date alert notifications based on user notification settings' do
user.notification_settings.first.update(
start_date: nil, start_date: nil,
due_date: 3 due_date: 3
) )
work_package = alertable_work_package(assigned_to: user_paris, work_package = alertable_work_package(assigned_to: user,
responsible: user_berlin,
start_date: in_1_day, start_date: in_1_day,
due_date: in_3_days) due_date: in_3_days)
run_job do run_job do
expect(user_paris).to have_a_start_date_alert_notification_for(work_package) expect(user).not_to have_a_start_date_alert_notification_for(work_package)
expect(user_paris).not_to have_a_due_date_alert_notification_for(work_package) expect(user).to have_a_due_date_alert_notification_for(work_package)
expect(user_berlin).not_to have_a_start_date_alert_notification_for(work_package)
expect(user_berlin).to have_a_due_date_alert_notification_for(work_package)
end end
end end
@ -252,7 +229,7 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: %
work_package = alertable_work_package work_package = alertable_work_package
run_job do run_job do
expect(user_paris).not_to have_a_start_date_alert_notification_for(work_package) expect(user).not_to have_a_start_date_alert_notification_for(work_package)
end end
end end
end end
@ -260,52 +237,52 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: %
context 'when project notification settings are defined for a user' do context 'when project notification settings are defined for a user' do
it 'creates date alert notifications using these settings for work packages of the project' do it 'creates date alert notifications using these settings for work packages of the project' do
# global notification settings # global notification settings
user_paris.notification_settings.first.update( user.notification_settings.first.update(
start_date: 1, start_date: 1,
due_date: nil due_date: nil
) )
# project notifications settings # project notifications settings
user_paris.notification_settings.create( user.notification_settings.create(
project:, project:,
start_date: nil, start_date: nil,
due_date: 7 due_date: 7
) )
silent_work_package = alertable_work_package(assigned_to: user_paris, silent_work_package = alertable_work_package(assigned_to: user,
project:, project:,
start_date: in_1_day, start_date: in_1_day,
due_date: in_1_day) due_date: in_1_day)
noisy_work_package = alertable_work_package(assigned_to: user_paris, noisy_work_package = alertable_work_package(assigned_to: user,
project:, project:,
start_date: in_7_days, start_date: in_7_days,
due_date: in_7_days) due_date: in_7_days)
run_job do run_job do
expect(user_paris).not_to have_a_start_date_alert_notification_for(silent_work_package) expect(user).not_to have_a_start_date_alert_notification_for(silent_work_package)
expect(user_paris).not_to have_a_due_date_alert_notification_for(silent_work_package) expect(user).not_to have_a_due_date_alert_notification_for(silent_work_package)
expect(user_paris).not_to have_a_start_date_alert_notification_for(noisy_work_package) expect(user).not_to have_a_start_date_alert_notification_for(noisy_work_package)
expect(user_paris).to have_a_due_date_alert_notification_for(noisy_work_package) expect(user).to have_a_due_date_alert_notification_for(noisy_work_package)
end end
end end
end end
context 'with existing date alerts' do context 'with existing date alerts' do
it 'marks them as read when new ones are created' do it 'marks them as read when new ones are created' do
work_package = alertable_work_package(assigned_to: user_paris, work_package = alertable_work_package(assigned_to: user,
start_date: in_1_day, start_date: in_1_day,
due_date: in_1_day) due_date: in_1_day)
existing_start_notification = create(:notification, existing_start_notification = create(:notification,
resource: work_package, resource: work_package,
recipient: user_paris, recipient: user,
reason: :date_alert_start_date) reason: :date_alert_start_date)
existing_due_notification = create(:notification, existing_due_notification = create(:notification,
resource: work_package, resource: work_package,
recipient: user_paris, recipient: user,
reason: :date_alert_due_date) reason: :date_alert_due_date)
run_job do run_job do
expect(existing_start_notification.reload).to have_attributes(read_ian: true) expect(existing_start_notification.reload).to have_attributes(read_ian: true)
expect(existing_due_notification.reload).to have_attributes(read_ian: true) expect(existing_due_notification.reload).to have_attributes(read_ian: true)
unread_date_alert_notifications = Notification.where(recipient: user_paris, unread_date_alert_notifications = Notification.where(recipient: user,
read_ian: false, read_ian: false,
resource: work_package) resource: work_package)
expect(unread_date_alert_notifications.pluck(:reason)) expect(unread_date_alert_notifications.pluck(:reason))
@ -315,27 +292,27 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: %
# rubocop:disable RSpec/ExampleLength # rubocop:disable RSpec/ExampleLength
it 'does not mark them as read when if no new notifications are created' do it 'does not mark them as read when if no new notifications are created' do
work_package_start = alertable_work_package(assigned_to: user_paris, work_package_start = alertable_work_package(assigned_to: user,
start_date: in_1_day, start_date: in_1_day,
due_date: nil) due_date: nil)
work_package_due = alertable_work_package(assigned_to: user_paris, work_package_due = alertable_work_package(assigned_to: user,
start_date: nil, start_date: nil,
due_date: in_1_day) due_date: in_1_day)
existing_wp_start_start_notif = create(:notification, existing_wp_start_start_notif = create(:notification,
reason: :date_alert_start_date, reason: :date_alert_start_date,
recipient: user_paris, recipient: user,
resource: work_package_start) resource: work_package_start)
existing_wp_start_due_notif = create(:notification, existing_wp_start_due_notif = create(:notification,
reason: :date_alert_due_date, reason: :date_alert_due_date,
recipient: user_paris, recipient: user,
resource: work_package_start) resource: work_package_start)
existing_wp_due_start_notif = create(:notification, existing_wp_due_start_notif = create(:notification,
reason: :date_alert_start_date, reason: :date_alert_start_date,
recipient: user_paris, recipient: user,
resource: work_package_due) resource: work_package_due)
existing_wp_due_due_notif = create(:notification, existing_wp_due_due_notif = create(:notification,
reason: :date_alert_due_date, reason: :date_alert_due_date,
recipient: user_paris, recipient: user,
resource: work_package_due) resource: work_package_due)
run_job do run_job do
@ -347,45 +324,5 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: %
end end
# rubocop:enable RSpec/ExampleLength # rubocop:enable RSpec/ExampleLength
end end
context 'when scheduled and executed at 01:00 am local time' do
it 'creates a start date alert notification for a user in the same time zone' do
work_package = alertable_work_package
run_job(scheduled_at: '1:00', local_time: '1:00') do
expect(user_paris).to have_a_start_date_alert_notification_for(work_package)
end
end
end
context 'when scheduled and executed at 01:14 am local time' do
it 'creates a start date alert notification for a user in the same time zone' do
work_package = alertable_work_package
run_job(scheduled_at: '1:14', local_time: '1:14') do
expect(user_paris).to have_a_start_date_alert_notification_for(work_package)
end
end
end
context 'when scheduled and executed at 01:15 am local time' do
it 'does not create a start date alert notification for a user in the same time zone' do
work_package = alertable_work_package
run_job(scheduled_at: '1:15', local_time: '1:15') do
expect(user_paris).not_to have_a_start_date_alert_notification_for(work_package)
end
end
end
context 'when scheduled at 01:00 am local time and executed at 01:37 am local time' do
it 'creates a start date alert notification for a user in the same time zone' do
work_package = alertable_work_package
run_job(scheduled_at: '1:00', local_time: '1:37') do
expect(user_paris).to have_a_start_date_alert_notification_for(work_package)
end
end
end
end end
end end

@ -0,0 +1,198 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 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::ScheduleDateAlertsNotificationsJob, type: :job, with_ee: %i[date_alerts] do
include ActiveSupport::Testing::TimeHelpers
shared_let(:project) { create(:project, name: 'main') }
# Paris and Berlin are both UTC+01:00 (CET) or UTC+02:00 (CEST)
shared_let(:timezone_paris) { ActiveSupport::TimeZone['Europe/Paris'] }
# Kathmandu is UTC+05:45 (no DST)
shared_let(:timezone_kathmandu) { ActiveSupport::TimeZone['Asia/Kathmandu'] }
shared_let(:user_paris) do
create(
:user,
firstname: 'Paris',
preferences: { time_zone: timezone_paris.name }
)
end
shared_let(:user_kathmandu) do
create(
:user,
firstname: 'Kathmandu',
preferences: { time_zone: timezone_kathmandu.name }
)
end
let(:schedule_job) do
described_class.ensure_scheduled!
described_class.delayed_job
end
before do
# We need to access the job as stored in the database to get at the run_at time persisted there
allow(ActiveJob::Base)
.to receive(:queue_adapter)
.and_return(ActiveJob::QueueAdapters::DelayedJobAdapter.new)
schedule_job
end
def set_scheduled_time(run_at)
schedule_job.update_column(:run_at, run_at)
end
# Converts "hh:mm" into { hour: h, min: m }
def time_hash(time)
%i[hour min].zip(time.split(':', 2).map(&:to_i)).to_h
end
def timezone_time(time, timezone)
timezone.now.change(time_hash(time))
end
def run_job(scheduled_at: '1:00', local_time: '1:04', timezone: timezone_paris)
set_scheduled_time(timezone_time(scheduled_at, timezone))
travel_to(timezone_time(local_time, timezone)) do
schedule_job.reload.invoke_job
yield if block_given?
end
end
def deserialized_of_job(job)
deserializer_class = Class.new do
include(ActiveJob::Arguments)
end
deserializer = deserializer_class.new
deserializer.deserialize(job.payload_object.job_data).to_h
end
def expect_job(job, klass, *arguments)
job_data = deserialized_of_job(job)
expect(job_data['job_class'])
.to eql klass
expect(job_data['arguments'])
.to match_array arguments
end
shared_examples_for 'job execution creates date alerts creation job' do
let(:timezone) { timezone_paris }
let(:scheduled_at) { '1:00' }
let(:local_time) { '1:04' }
let(:user) { user_paris }
it 'creates the job for the user' do
expect do
run_job(timezone:, scheduled_at:, local_time:) do
expect_job(Delayed::Job.last, "Notifications::CreateDateAlertsNotificationsJob", user)
end
end.to change(Delayed::Job, :count).by 1
end
end
shared_examples_for 'job execution creates no date alerts creation job' do
let(:timezone) { timezone_paris }
let(:scheduled_at) { '1:00' }
let(:local_time) { '1:04' }
it 'creates no job' do
expect do
run_job(timezone:, scheduled_at:, local_time:)
end.not_to change(Delayed::Job, :count)
end
end
describe '#perform' do
context 'for users whose local time is 1:00 am (UTC+1) when the job is executed' do
it_behaves_like 'job execution creates date alerts creation job' do
let(:timezone) { timezone_paris }
let(:scheduled_at) { '1:00' }
let(:local_time) { '1:04' }
let(:user) { user_paris }
end
end
context 'for users whose local time is 1:00 am (UTC+05:45) when the job is executed' do
it_behaves_like 'job execution creates date alerts creation job' do
let(:timezone) { timezone_kathmandu }
let(:scheduled_at) { '1:00' }
let(:local_time) { '1:04' }
let(:user) { user_kathmandu }
end
end
context 'without enterprise token', with_ee: false do
it_behaves_like 'job execution creates no date alerts creation job' do
let(:timezone) { timezone_paris }
let(:scheduled_at) { '1:00' }
let(:local_time) { '1:04' }
end
end
context 'when scheduled and executed at 01:00 am local time' do
it_behaves_like 'job execution creates date alerts creation job' do
let(:timezone) { timezone_paris }
let(:scheduled_at) { '1:00' }
let(:local_time) { '1:00' }
let(:user) { user_paris }
end
end
context 'when scheduled and executed at 01:14 am local time' do
it_behaves_like 'job execution creates date alerts creation job' do
let(:timezone) { timezone_paris }
let(:scheduled_at) { '1:14' }
let(:local_time) { '1:14' }
let(:user) { user_paris }
end
end
context 'when scheduled and executed at 01:15 am local time' do
it_behaves_like 'job execution creates no date alerts creation job' do
let(:timezone) { timezone_paris }
let(:scheduled_at) { '1:15' }
let(:local_time) { '1:15' }
end
end
context 'when scheduled at 01:00 am local time and executed at 01:37 am local time' do
it_behaves_like 'job execution creates date alerts creation job' do
let(:timezone) { timezone_paris }
let(:scheduled_at) { '1:00' }
let(:local_time) { '1:37' }
let(:user) { user_paris }
end
end
end
end
Loading…
Cancel
Save