Commit 1a5c6264 authored by Sean Arnold's avatar Sean Arnold

Add more specs + refine service

- Add with_active_period scope to Oncall rotation
- Use transaction in update
parent 50635e9e
...@@ -50,6 +50,7 @@ module IncidentManagement ...@@ -50,6 +50,7 @@ module IncidentManagement
scope :in_progress, -> { where('starts_at < :time AND (ends_at > :time OR ends_at IS NULL)', time: Time.current) } scope :in_progress, -> { where('starts_at < :time AND (ends_at > :time OR ends_at IS NULL)', time: Time.current) }
scope :except_ids, -> (ids) { where.not(id: ids) } scope :except_ids, -> (ids) { where.not(id: ids) }
scope :with_active_period, -> { where.not(active_period_start: nil) }
scope :with_shift_generation_associations, -> do scope :with_shift_generation_associations, -> do
joins(:active_participants, :schedule) joins(:active_participants, :schedule)
.distinct .distinct
......
...@@ -8,8 +8,7 @@ module IncidentManagement ...@@ -8,8 +8,7 @@ module IncidentManagement
# @param params [Hash] # @param params [Hash]
def initialize(oncall_schedule, user, params) def initialize(oncall_schedule, user, params)
@oncall_schedule = oncall_schedule @oncall_schedule = oncall_schedule
@original_schedule_timezone = oncall_schedule&.timezone @original_schedule_timezone = oncall_schedule.timezone
@oncall_rotations = oncall_schedule&.rotations
@user = user @user = user
@params = params @params = params
@project = oncall_schedule.project @project = oncall_schedule.project
...@@ -19,47 +18,52 @@ module IncidentManagement ...@@ -19,47 +18,52 @@ module IncidentManagement
return error_no_license unless available? return error_no_license unless available?
return error_no_permissions unless allowed? return error_no_permissions unless allowed?
oncall_schedule.update!(params) IncidentManagement::OncallSchedule.transaction do
update_rotation_result = update_rotation_active_periods oncall_schedule.update!(params)
update_rotations!
if update_rotation_result.respond_to?(:error?) && update_rotation_result.error?
return error(update_rotation_result.message)
end end
success(oncall_schedule) success(oncall_schedule)
rescue ActiveRecord::RecordInvalid => e rescue ActiveRecord::RecordInvalid => e
error(e.record.errors.full_messages.to_sentence) error(e.record.errors.full_messages.to_sentence)
rescue StandardError => e
error(e.message)
end end
private private
attr_reader :oncall_schedule, :original_schedule_timezone, :oncall_rotations, :user, :params, :project attr_reader :oncall_schedule, :original_schedule_timezone, :user, :params, :project
def update_rotations!
return unless original_schedule_timezone != oncall_schedule.timezone
update_rotation_active_periods
end
# Converts & updates the active period to the new timezone
# Ex: 8:00 - 17:00 Europe/Berlin becomes 6:00 - 15:00 UTC
def update_rotation_active_periods def update_rotation_active_periods
oncall_schedule.rotations.select(&:has_shift_active_period?).each do |rotation| original_schedule_current_time = Time.current.in_time_zone(original_schedule_timezone)
oncall_schedule.rotations.with_active_period.each do |rotation|
active_period = rotation.active_period.for_date(original_schedule_current_time)
new_start_time, new_end_time = active_period.map { |time| time.in_time_zone(oncall_schedule.timezone).strftime('%H:%M') }
service = IncidentManagement::OncallRotations::EditService.new( service = IncidentManagement::OncallRotations::EditService.new(
rotation, rotation,
user, user,
{ {
active_period_start: new_rotation_active_period(rotation.active_period_start), active_period_start: new_start_time,
active_period_end: new_rotation_active_period(rotation.active_period_end) active_period_end: new_end_time
} }
) )
response = service.execute response = service.execute
break(response) if response.error? raise response.message if response.error?
end end
end end
def new_rotation_active_period(existing_time)
existing_timezone_time = Time.find_zone!(original_schedule_timezone)
.now
.change(hour: existing_time.hour, min: existing_time.min)
existing_timezone_time.in_time_zone(oncall_schedule.timezone).strftime('%H:%M')
end
def error_no_permissions def error_no_permissions
error(_('You have insufficient permissions to update an on-call schedule for this project')) error(_('You have insufficient permissions to update an on-call schedule for this project'))
end end
......
...@@ -107,6 +107,18 @@ RSpec.describe IncidentManagement::OncallRotation do ...@@ -107,6 +107,18 @@ RSpec.describe IncidentManagement::OncallRotation do
it { is_expected.to contain_exactly(rotation_1, rotation_2) } it { is_expected.to contain_exactly(rotation_1, rotation_2) }
end end
describe '.with_active_period' do
subject { described_class.with_active_period }
it { is_expected.to be_empty }
context 'rotation has active period' do
let(:rotation) { create(:incident_management_oncall_rotation, :with_active_period, schedule: schedule) }
it { is_expected.to contain_exactly(rotation) }
end
end
end end
describe '#shift_cycle_duration' do describe '#shift_cycle_duration' do
......
...@@ -9,7 +9,8 @@ RSpec.describe IncidentManagement::OncallSchedules::UpdateService do ...@@ -9,7 +9,8 @@ RSpec.describe IncidentManagement::OncallSchedules::UpdateService do
let_it_be_with_reload(:oncall_schedule) { create(:incident_management_oncall_schedule, :utc, project: project) } let_it_be_with_reload(:oncall_schedule) { create(:incident_management_oncall_schedule, :utc, project: project) }
let(:current_user) { user_with_permissions } let(:current_user) { user_with_permissions }
let(:params) { { name: 'Updated name', description: 'Updated description', timezone: 'America/New_York' } } let(:new_timezone) { 'America/New_York' }
let(:params) { { name: 'Updated name', description: 'Updated description', timezone: new_timezone } }
let(:service) { described_class.new(oncall_schedule, current_user, params) } let(:service) { described_class.new(oncall_schedule, current_user, params) }
before do before do
...@@ -72,19 +73,70 @@ RSpec.describe IncidentManagement::OncallSchedules::UpdateService do ...@@ -72,19 +73,70 @@ RSpec.describe IncidentManagement::OncallSchedules::UpdateService do
context 'schedule has a rotation' do context 'schedule has a rotation' do
# Setting fixed timezone for rotation active period updates # Setting fixed timezone for rotation active period updates
around do |example| around do |example|
freeze_time do travel_to Time.utc(2021, 03, 22, 0, 0)
travel_to Time.utc(2021, 03, 22, 0, 0)
example.run example.run
end
end end
let_it_be_with_reload(:oncall_rotation) { create(:incident_management_oncall_rotation, :with_active_period, schedule: oncall_schedule) } let_it_be_with_reload(:oncall_rotation) { create(:incident_management_oncall_rotation, :with_active_period, schedule: oncall_schedule) }
shared_examples 'updates the rotation active periods' do |new_start_time, new_end_time|
it 'updates the rotation active periods with new timezone' do
initial_start_time = oncall_rotation.reload.attributes_before_type_cast['active_period_start']
initial_end_time = oncall_rotation.attributes_before_type_cast['active_period_end']
expect { execute }.to change { oncall_rotation.reload.attributes_before_type_cast['active_period_start'] }.from(initial_start_time).to("#{new_start_time}:00")
.and change { oncall_rotation.reload.attributes_before_type_cast['active_period_end'] }.from(initial_end_time).to("#{new_end_time}:00")
.and change { oncall_schedule.reload.timezone }.to(new_timezone)
end
end
# This expects the active periods are updated according to the date above (22nd March, 2021 in the new timezone). # This expects the active periods are updated according to the date above (22nd March, 2021 in the new timezone).
it 'updates the rotation active periods with new timezone' do it_behaves_like 'updates the rotation active periods', '04:00', '13:00'
expect { execute }.to change { time_from_time_column(oncall_rotation.reload.active_period_start) }.from('08:00').to('04:00')
.and change { time_from_time_column(oncall_rotation.active_period_end) }.from('17:00').to('13:00') context 'from non-overnight shifts to overnight' do
let(:new_timezone) { 'Pacific/Auckland' }
it_behaves_like 'updates the rotation active periods', '21:00', '06:00'
end
context 'from overnight shifts to non-overnight' do
before do
oncall_rotation.update!(active_period_start: '21:00', active_period_end: '06:00')
end
let(:new_timezone) { 'Pacific/Auckland' }
it_behaves_like 'updates the rotation active periods', '10:00', '19:00'
end
context 'new timezone has non-whole-hour change' do
let(:new_timezone) { 'Asia/Kolkata' }
it_behaves_like 'updates the rotation active periods', '13:30', '22:30'
end
context 'new timezone but same offset' do
let(:new_timezone) { 'Europe/London' }
it 'updates the timezone' do
expect { execute }.to change { oncall_schedule.reload.timezone }.to(new_timezone)
end
it 'does not update the active period times' do
expect { execute }.to not_change { time_from_time_column(oncall_rotation.reload.active_period_start) }
.and not_change { time_from_time_column(oncall_rotation.active_period_end) }
end
end
context 'timezone is not changed' do
before do
params.delete(:timezone)
end
it 'does not update rotations' do
expect { execute }.to not_change { oncall_rotation.updated_at }
end
end end
context 'error updating' do context 'error updating' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment