Commit 2e2e6f95 authored by Sean Arnold's avatar Sean Arnold Committed by Mayra Cabrera

Add oncall rotation and participant table

- Add migrations
- Add models
- Add specs
parent e01351e1
# frozen_string_literal: true
module Enums
# These color palettes are part of the Pajamas Design System.
# See https://design.gitlab.com/data-visualization/color/#categorical-data
module DataVisualizationPalette
def self.colors
{
blue: 0,
orange: 1,
aqua: 2,
green: 3,
magenta: 4
}
end
def self.weights
{
'50' => 0,
'100' => 1,
'200' => 2,
'300' => 3,
'400' => 4,
'500' => 5,
'600' => 6,
'700' => 7,
'800' => 8,
'900' => 9,
'950' => 10
}
end
end
end
---
title: Add oncall rotations and participants tables
merge_request: 49058
author:
type: added
# frozen_string_literal: true
class CreateIncidentManagementOnCallRotations < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
unless table_exists?(:incident_management_oncall_rotations)
with_lock_retries do
create_table :incident_management_oncall_rotations do |t|
t.timestamps_with_timezone
t.references :oncall_schedule, index: false, null: false, foreign_key: { to_table: :incident_management_oncall_schedules, on_delete: :cascade }
t.integer :length, null: false
t.integer :length_unit, limit: 2, null: false
t.datetime_with_timezone :starts_at, null: false
t.text :name, null: false
t.index %w(oncall_schedule_id id), name: 'index_inc_mgmnt_oncall_rotations_on_oncall_schedule_id_and_id', unique: true, using: :btree
t.index %w(oncall_schedule_id name), name: 'index_inc_mgmnt_oncall_rotations_on_oncall_schedule_id_and_name', unique: true, using: :btree
end
end
end
add_text_limit :incident_management_oncall_rotations, :name, 200
end
def down
with_lock_retries do
drop_table :incident_management_oncall_rotations
end
end
end
# frozen_string_literal: true
class AddIncidentManagementOnCallParticipants < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
PARTICIPANT_ROTATION_INDEX_NAME = 'index_inc_mgmnt_oncall_participants_on_oncall_rotation_id'
PARTICIPANT_USER_INDEX_NAME = 'index_inc_mgmnt_oncall_participants_on_oncall_user_id'
UNIQUE_INDEX_NAME = 'index_inc_mgmnt_oncall_participants_on_user_id_and_rotation_id'
disable_ddl_transaction!
def up
unless table_exists?(:incident_management_oncall_participants)
with_lock_retries do
create_table :incident_management_oncall_participants do |t|
t.references :oncall_rotation, index: false, null: false, foreign_key: { to_table: :incident_management_oncall_rotations, on_delete: :cascade }
t.references :user, index: false, null: false, foreign_key: { on_delete: :cascade }
t.integer :color_palette, limit: 2, null: false
t.integer :color_weight, limit: 2, null: false
t.index :user_id, name: PARTICIPANT_USER_INDEX_NAME
t.index :oncall_rotation_id, name: PARTICIPANT_ROTATION_INDEX_NAME
t.index [:user_id, :oncall_rotation_id], unique: true, name: UNIQUE_INDEX_NAME
end
end
end
end
def down
drop_table :incident_management_oncall_participants
end
end
2929b74d9b9d6e205c0e1fb2aaaffe323394058f6e583c56035a2c83b4d4ff03
\ No newline at end of file
451d7f29392f965467f364c7b119d269551e2dc1485e8cb15ebd14753fdb6e6a
\ No newline at end of file
......@@ -12989,6 +12989,44 @@ CREATE SEQUENCE import_failures_id_seq
ALTER SEQUENCE import_failures_id_seq OWNED BY import_failures.id;
CREATE TABLE incident_management_oncall_participants (
id bigint NOT NULL,
oncall_rotation_id bigint NOT NULL,
user_id bigint NOT NULL,
color_palette smallint NOT NULL,
color_weight smallint NOT NULL
);
CREATE SEQUENCE incident_management_oncall_participants_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE incident_management_oncall_participants_id_seq OWNED BY incident_management_oncall_participants.id;
CREATE TABLE incident_management_oncall_rotations (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
oncall_schedule_id bigint NOT NULL,
length integer NOT NULL,
length_unit smallint NOT NULL,
starts_at timestamp with time zone NOT NULL,
name text NOT NULL,
CONSTRAINT check_5209fb5d02 CHECK ((char_length(name) <= 200))
);
CREATE SEQUENCE incident_management_oncall_rotations_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE incident_management_oncall_rotations_id_seq OWNED BY incident_management_oncall_rotations.id;
CREATE TABLE incident_management_oncall_schedules (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
......@@ -18231,6 +18269,10 @@ ALTER TABLE ONLY import_export_uploads ALTER COLUMN id SET DEFAULT nextval('impo
ALTER TABLE ONLY import_failures ALTER COLUMN id SET DEFAULT nextval('import_failures_id_seq'::regclass);
ALTER TABLE ONLY incident_management_oncall_participants ALTER COLUMN id SET DEFAULT nextval('incident_management_oncall_participants_id_seq'::regclass);
ALTER TABLE ONLY incident_management_oncall_rotations ALTER COLUMN id SET DEFAULT nextval('incident_management_oncall_rotations_id_seq'::regclass);
ALTER TABLE ONLY incident_management_oncall_schedules ALTER COLUMN id SET DEFAULT nextval('incident_management_oncall_schedules_id_seq'::regclass);
ALTER TABLE ONLY index_statuses ALTER COLUMN id SET DEFAULT nextval('index_statuses_id_seq'::regclass);
......@@ -19440,6 +19482,12 @@ ALTER TABLE ONLY import_export_uploads
ALTER TABLE ONLY import_failures
ADD CONSTRAINT import_failures_pkey PRIMARY KEY (id);
ALTER TABLE ONLY incident_management_oncall_participants
ADD CONSTRAINT incident_management_oncall_participants_pkey PRIMARY KEY (id);
ALTER TABLE ONLY incident_management_oncall_rotations
ADD CONSTRAINT incident_management_oncall_rotations_pkey PRIMARY KEY (id);
ALTER TABLE ONLY incident_management_oncall_schedules
ADD CONSTRAINT incident_management_oncall_schedules_pkey PRIMARY KEY (id);
......@@ -21373,6 +21421,16 @@ CREATE INDEX index_import_failures_on_project_id_not_null ON import_failures USI
CREATE INDEX index_imported_projects_on_import_type_creator_id_created_at ON projects USING btree (import_type, creator_id, created_at) WHERE (import_type IS NOT NULL);
CREATE INDEX index_inc_mgmnt_oncall_participants_on_oncall_rotation_id ON incident_management_oncall_participants USING btree (oncall_rotation_id);
CREATE INDEX index_inc_mgmnt_oncall_participants_on_oncall_user_id ON incident_management_oncall_participants USING btree (user_id);
CREATE UNIQUE INDEX index_inc_mgmnt_oncall_participants_on_user_id_and_rotation_id ON incident_management_oncall_participants USING btree (user_id, oncall_rotation_id);
CREATE UNIQUE INDEX index_inc_mgmnt_oncall_rotations_on_oncall_schedule_id_and_id ON incident_management_oncall_rotations USING btree (oncall_schedule_id, id);
CREATE UNIQUE INDEX index_inc_mgmnt_oncall_rotations_on_oncall_schedule_id_and_name ON incident_management_oncall_rotations USING btree (oncall_schedule_id, name);
CREATE INDEX index_incident_management_oncall_schedules_on_project_id ON incident_management_oncall_schedules USING btree (project_id);
CREATE UNIQUE INDEX index_index_statuses_on_project_id ON index_statuses USING btree (project_id);
......@@ -23745,6 +23803,9 @@ ALTER TABLE ONLY namespace_statistics
ALTER TABLE ONLY clusters_applications_elastic_stacks
ADD CONSTRAINT fk_rails_026f219f46 FOREIGN KEY (cluster_id) REFERENCES clusters(id) ON DELETE CASCADE;
ALTER TABLE ONLY incident_management_oncall_participants
ADD CONSTRAINT fk_rails_032b12996a FOREIGN KEY (oncall_rotation_id) REFERENCES incident_management_oncall_rotations(id) ON DELETE CASCADE;
ALTER TABLE ONLY events
ADD CONSTRAINT fk_rails_0434b48643 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
......@@ -23922,6 +23983,9 @@ ALTER TABLE ONLY saml_group_links
ALTER TABLE ONLY group_custom_attributes
ADD CONSTRAINT fk_rails_246e0db83a FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY incident_management_oncall_rotations
ADD CONSTRAINT fk_rails_256e0bc604 FOREIGN KEY (oncall_schedule_id) REFERENCES incident_management_oncall_schedules(id) ON DELETE CASCADE;
ALTER TABLE ONLY analytics_devops_adoption_snapshots
ADD CONSTRAINT fk_rails_25da9a92c0 FOREIGN KEY (segment_id) REFERENCES analytics_devops_adoption_segments(id) ON DELETE CASCADE;
......@@ -24252,6 +24316,9 @@ ALTER TABLE ONLY resource_weight_events
ALTER TABLE ONLY approval_project_rules
ADD CONSTRAINT fk_rails_5fb4dd100b FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY incident_management_oncall_participants
ADD CONSTRAINT fk_rails_5fe86ea341 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY user_highest_roles
ADD CONSTRAINT fk_rails_60f6c325a6 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
......
# frozen_string_literal: true
module IncidentManagement
class OncallParticipant < ApplicationRecord
include BulkInsertSafe
self.table_name = 'incident_management_oncall_participants'
enum color_palette: Enums::DataVisualizationPalette.colors
enum color_weight: Enums::DataVisualizationPalette.weights
belongs_to :rotation, class_name: 'OncallRotation', foreign_key: :oncall_rotation_id
belongs_to :user, class_name: 'User', foreign_key: :user_id
validates :rotation, presence: true
validates :color_palette, presence: true
validates :color_weight, presence: true
validates :user, presence: true, uniqueness: { scope: :oncall_rotation_id }
validate :user_can_read_project, if: :user, on: :create
delegate :project, to: :rotation, allow_nil: true
private
def user_can_read_project
unless user.can?(:read_project, project)
errors.add(:user, 'does not have access to the project')
end
end
end
end
# frozen_string_literal: true
module IncidentManagement
class OncallRotation < ApplicationRecord
self.table_name = 'incident_management_oncall_rotations'
enum length_unit: {
hours: 0,
days: 1,
weeks: 2
}
NAME_LENGTH = 200
belongs_to :schedule, class_name: 'OncallSchedule', inverse_of: 'rotations', foreign_key: 'oncall_schedule_id'
has_many :participants, class_name: 'OncallParticipant', inverse_of: :rotation
has_many :users, through: :participants
validates :name, presence: true, uniqueness: { scope: :oncall_schedule_id }, length: { maximum: NAME_LENGTH }
validates :starts_at, presence: true
validates :length, presence: true
validates :length_unit, presence: true
delegate :project, to: :schedule
end
end
......@@ -11,6 +11,8 @@ module IncidentManagement
DESCRIPTION_LENGTH = 1000
belongs_to :project, inverse_of: :incident_management_oncall_schedules
has_many :rotations, class_name: 'OncallRotation'
has_many :participants, class_name: 'OncallParticipant', through: :rotations
has_internal_id :iid, scope: :project
......
# frozen_string_literal: true
FactoryBot.define do
factory :incident_management_oncall_participant, class: 'IncidentManagement::OncallParticipant' do
association :rotation, factory: :incident_management_oncall_rotation
association :user, factory: :user
color_palette { IncidentManagement::OncallParticipant.color_palettes.first.first }
color_weight { IncidentManagement::OncallParticipant.color_weights.first.first }
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :incident_management_oncall_rotation, class: 'IncidentManagement::OncallRotation' do
association :schedule, factory: :incident_management_oncall_schedule
sequence(:name) { |n| "On-call Rotation ##{n}" }
starts_at { Time.current }
length { 5 }
length_unit { :days }
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::OncallParticipant do
let_it_be(:rotation) { create(:incident_management_oncall_rotation) }
let_it_be(:user) { create(:user) }
subject { build(:incident_management_oncall_participant, rotation: rotation, user: user) }
it { is_expected.to be_valid }
before_all do
rotation.project.add_developer(user)
end
describe '.associations' do
it { is_expected.to belong_to(:rotation) }
it { is_expected.to belong_to(:user) }
end
describe '.validations' do
it { is_expected.to validate_presence_of(:rotation) }
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to validate_presence_of(:color_weight) }
it { is_expected.to validate_presence_of(:color_palette) }
context 'when the participant already exists in the rotation' do
before do
create(:incident_management_oncall_participant, rotation: rotation, user: user)
end
it 'has validation errors' do
expect(subject).to be_invalid
expect(subject.errors.full_messages.to_sentence).to eq('User has already been taken')
end
end
context 'when participant cannot read project' do
let_it_be(:other_user) { create(:user) }
subject { build(:incident_management_oncall_participant, rotation: rotation, user: other_user) }
context 'on creation' do
it 'has validation errors' do
expect(subject).to be_invalid
expect(subject.errors.full_messages.to_sentence).to eq('User does not have access to the project')
end
end
context 'after creation' do
let(:project) { rotation.project }
before do
project.add_developer(other_user)
end
it 'is valid' do
subject.save!
remove_user_from_project(other_user, project)
expect(subject).to be_valid
end
end
end
end
private
def remove_user_from_project(user, project)
Members::DestroyService.new(user).execute(project.project_member(user))
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::OncallRotation do
let_it_be(:schedule) { create(:incident_management_oncall_schedule) }
describe '.associations' do
it { is_expected.to belong_to(:schedule) }
it { is_expected.to have_many(:participants) }
it { is_expected.to have_many(:users).through(:participants) }
end
describe '.validations' do
subject { build(:incident_management_oncall_rotation, schedule: schedule, name: 'Test rotation') }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_length_of(:name).is_at_most(200) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:oncall_schedule_id) }
it { is_expected.to validate_presence_of(:starts_at) }
it { is_expected.to validate_presence_of(:length) }
it { is_expected.to validate_presence_of(:length_unit) }
context 'when the oncall rotation with the same name exists' do
before do
create(:incident_management_oncall_rotation, schedule: schedule, name: 'Test rotation')
end
it 'has validation errors' do
expect(subject).to be_invalid
expect(subject.errors.full_messages.to_sentence).to eq('Name has already been taken')
end
end
end
end
......@@ -7,6 +7,8 @@ RSpec.describe IncidentManagement::OncallSchedule do
describe '.associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:rotations) }
it { is_expected.to have_many(:participants).through(:rotations) }
end
describe '.validations' 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