Commit 11e5fcac authored by Adam Hegyi's avatar Adam Hegyi

Merge branch '262395-models-for-devops-adoption-segments' into 'master'

Models for Devops Adoption segment configuration

See merge request gitlab-org/gitlab!45748
parents 083d53ca 1f439cae
# frozen_string_literal: true
module Analytics::DevopsAdoption
def self.table_name_prefix
'analytics_devops_adoption_'
end
end
# frozen_string_literal: true
class Analytics::DevopsAdoption::Segment < ApplicationRecord
ALLOWED_SEGMENT_COUNT = 20
has_many :segment_selections
validates :name, presence: true, uniqueness: true, length: { maximum: 255 }
validate :validate_segment_count
private
def validate_segment_count
if self.class.count >= ALLOWED_SEGMENT_COUNT
errors.add(:name, s_('DevopsAdoptionSegment|The maximum number of segments has been reached'))
end
end
end
# frozen_string_literal: true
class Analytics::DevopsAdoption::SegmentSelection < ApplicationRecord
ALLOWED_SELECTIONS_PER_SEGMENT = 20
belongs_to :segment
belongs_to :project
belongs_to :group
validates :segment, presence: true
validates :project, presence: { unless: :group }
validates :project_id, uniqueness: { scope: :segment_id, if: :project }
validates :group, presence: { unless: :project }
validates :group_id, uniqueness: { scope: :segment_id, if: :group }
validate :exclusive_project_or_group
validate :validate_selection_count
private
def exclusive_project_or_group
if project.present? && group.present?
errors.add(:group, s_('DevopsAdoptionSegmentSelection|The selection cannot be configured for a project and for a group at the same time'))
end
end
def validate_selection_count
return unless segment
selection_count_for_segment = self.class.where(segment: segment).count
if selection_count_for_segment >= ALLOWED_SELECTIONS_PER_SEGMENT
errors.add(:segment, s_('DevopsAdoptionSegmentSelection|The maximum number of selections has been reached'))
end
end
end
---
title: Add `analytics_devops_adoption_segment_selections` and `analytics_devops_adoption_segments` database tables.
merge_request: 45748
author:
type: other
# frozen_string_literal: true
class CreateAnalyticsDevopsAdoptionSegments < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
create_table :analytics_devops_adoption_segments, if_not_exists: true do |t|
t.text :name, null: false, index: { unique: true }
t.datetime_with_timezone :last_recorded_at
t.timestamps_with_timezone
end
add_text_limit :analytics_devops_adoption_segments, :name, 255
end
def down
drop_table :analytics_devops_adoption_segments
end
end
# frozen_string_literal: true
class CreateAnalyticsDevopsAdoptionSegmentSelections < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
create_table :analytics_devops_adoption_segment_selections do |t|
t.references :segment, index: { name: 'index_on_segment_selections_segment_id' }, null: false, foreign_key: { to_table: :analytics_devops_adoption_segments, on_delete: :cascade }
t.bigint :group_id
t.bigint :project_id
t.index [:group_id, :segment_id], unique: true, name: 'index_on_segment_selections_group_id_segment_id'
t.index [:project_id, :segment_id], unique: true, name: 'index_on_segment_selections_project_id_segment_id'
t.timestamps_with_timezone
end
end
end
# frozen_string_literal: true
class AddForeignKeyProjectIdToSegmentSelection < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_foreign_key(:analytics_devops_adoption_segment_selections, :projects, column: :project_id, on_delete: :cascade)
end
def down
with_lock_retries do
remove_foreign_key :analytics_devops_adoption_segment_selections, column: :project_id
end
end
end
# frozen_string_literal: true
class AddForeignKeyGroupIdToSegmentSelection < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_foreign_key(:analytics_devops_adoption_segment_selections, :namespaces, column: :group_id, on_delete: :cascade)
end
def down
with_lock_retries do
remove_foreign_key :analytics_devops_adoption_segment_selections, column: :group_id
end
end
end
# frozen_string_literal: true
class AddCheckConstraintToSegmentSelection < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
CONSTRAINT_NAME = 'segment_selection_project_id_or_group_id_required'
def up
add_check_constraint :analytics_devops_adoption_segment_selections, '(project_id != NULL AND group_id IS NULL) OR (group_id != NULL AND project_id IS NULL)', CONSTRAINT_NAME
end
def down
remove_check_constraint :analytics_devops_adoption_segment_selections, CONSTRAINT_NAME
end
end
552f8a042bdecb7511d63e129438faff3fde8e1086cc88f7a79269b2b7098a65
\ No newline at end of file
90936786a9c2a9d035d13be87021b2f8278342cd11992d58753ca5a5413ed9d7
\ No newline at end of file
d7683f7a2db6aa27606756c158fa029c932230713b165917bc05a491f10e7adf
\ No newline at end of file
9b90dd436cc1c315eacc60acbc5f6b3b94029b82b4fcb27a41abc4941ad9a4ad
\ No newline at end of file
d924e4ad9ff67d01d660db3b3a1b9e5d74cecb1a127d598d1d1193cbfa499030
\ No newline at end of file
......@@ -8953,6 +8953,42 @@ CREATE SEQUENCE analytics_cycle_analytics_project_stages_id_seq
ALTER SEQUENCE analytics_cycle_analytics_project_stages_id_seq OWNED BY analytics_cycle_analytics_project_stages.id;
CREATE TABLE analytics_devops_adoption_segment_selections (
id bigint NOT NULL,
segment_id bigint NOT NULL,
group_id bigint,
project_id bigint,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
CONSTRAINT segment_selection_project_id_or_group_id_required CHECK ((((project_id <> NULL::bigint) AND (group_id IS NULL)) OR ((group_id <> NULL::bigint) AND (project_id IS NULL))))
);
CREATE SEQUENCE analytics_devops_adoption_segment_selections_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE analytics_devops_adoption_segment_selections_id_seq OWNED BY analytics_devops_adoption_segment_selections.id;
CREATE TABLE analytics_devops_adoption_segments (
id bigint NOT NULL,
name text NOT NULL,
last_recorded_at timestamp with time zone,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL
);
CREATE SEQUENCE analytics_devops_adoption_segments_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE analytics_devops_adoption_segments_id_seq OWNED BY analytics_devops_adoption_segments.id;
CREATE TABLE analytics_instance_statistics_measurements (
id bigint NOT NULL,
count bigint NOT NULL,
......@@ -17471,6 +17507,10 @@ ALTER TABLE ONLY analytics_cycle_analytics_group_value_streams ALTER COLUMN id S
ALTER TABLE ONLY analytics_cycle_analytics_project_stages ALTER COLUMN id SET DEFAULT nextval('analytics_cycle_analytics_project_stages_id_seq'::regclass);
ALTER TABLE ONLY analytics_devops_adoption_segment_selections ALTER COLUMN id SET DEFAULT nextval('analytics_devops_adoption_segment_selections_id_seq'::regclass);
ALTER TABLE ONLY analytics_devops_adoption_segments ALTER COLUMN id SET DEFAULT nextval('analytics_devops_adoption_segments_id_seq'::regclass);
ALTER TABLE ONLY analytics_instance_statistics_measurements ALTER COLUMN id SET DEFAULT nextval('analytics_instance_statistics_measurements_id_seq'::regclass);
ALTER TABLE ONLY appearances ALTER COLUMN id SET DEFAULT nextval('appearances_id_seq'::regclass);
......@@ -18422,6 +18462,12 @@ ALTER TABLE ONLY analytics_cycle_analytics_group_value_streams
ALTER TABLE ONLY analytics_cycle_analytics_project_stages
ADD CONSTRAINT analytics_cycle_analytics_project_stages_pkey PRIMARY KEY (id);
ALTER TABLE ONLY analytics_devops_adoption_segment_selections
ADD CONSTRAINT analytics_devops_adoption_segment_selections_pkey PRIMARY KEY (id);
ALTER TABLE ONLY analytics_devops_adoption_segments
ADD CONSTRAINT analytics_devops_adoption_segments_pkey PRIMARY KEY (id);
ALTER TABLE ONLY analytics_instance_statistics_measurements
ADD CONSTRAINT analytics_instance_statistics_measurements_pkey PRIMARY KEY (id);
......@@ -19960,6 +20006,8 @@ CREATE INDEX index_analytics_ca_project_stages_on_start_event_label_id ON analyt
CREATE INDEX index_analytics_cycle_analytics_group_stages_custom_only ON analytics_cycle_analytics_group_stages USING btree (id) WHERE (custom = true);
CREATE UNIQUE INDEX index_analytics_devops_adoption_segments_on_name ON analytics_devops_adoption_segments USING btree (name);
CREATE INDEX index_application_settings_on_custom_project_templates_group_id ON application_settings USING btree (custom_project_templates_group_id);
CREATE INDEX index_application_settings_on_file_template_project_id ON application_settings USING btree (file_template_project_id);
......@@ -21202,6 +21250,12 @@ CREATE UNIQUE INDEX index_on_instance_statistics_recorded_at_and_identifier ON a
CREATE INDEX index_on_label_links_all_columns ON label_links USING btree (target_id, label_id, target_type);
CREATE UNIQUE INDEX index_on_segment_selections_group_id_segment_id ON analytics_devops_adoption_segment_selections USING btree (group_id, segment_id);
CREATE UNIQUE INDEX index_on_segment_selections_project_id_segment_id ON analytics_devops_adoption_segment_selections USING btree (project_id, segment_id);
CREATE INDEX index_on_segment_selections_segment_id ON analytics_devops_adoption_segment_selections USING btree (segment_id);
CREATE INDEX index_on_users_name_lower ON users USING btree (lower((name)::text));
CREATE INDEX index_open_project_tracker_data_on_service_id ON open_project_tracker_data USING btree (service_id);
......@@ -22980,6 +23034,9 @@ ALTER TABLE ONLY project_group_links
ALTER TABLE ONLY epics
ADD CONSTRAINT fk_dccd3f98fc FOREIGN KEY (assignee_id) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE ONLY analytics_devops_adoption_segment_selections
ADD CONSTRAINT fk_ded7fe0344 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY issues
ADD CONSTRAINT fk_df75a7c8b8 FOREIGN KEY (promoted_to_epic_id) REFERENCES epics(id) ON DELETE SET NULL;
......@@ -23043,6 +23100,9 @@ ALTER TABLE ONLY clusters
ALTER TABLE ONLY epics
ADD CONSTRAINT fk_f081aa4489 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY analytics_devops_adoption_segment_selections
ADD CONSTRAINT fk_f1472b95f3 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY boards
ADD CONSTRAINT fk_f15266b5f9 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
......@@ -23112,6 +23172,9 @@ ALTER TABLE ONLY ip_restrictions
ALTER TABLE ONLY terraform_state_versions
ADD CONSTRAINT fk_rails_04f176e239 FOREIGN KEY (terraform_state_id) REFERENCES terraform_states(id) ON DELETE CASCADE;
ALTER TABLE ONLY analytics_devops_adoption_segment_selections
ADD CONSTRAINT fk_rails_053f00a9da FOREIGN KEY (segment_id) REFERENCES analytics_devops_adoption_segments(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_build_report_results
ADD CONSTRAINT fk_rails_056d298d48 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
......
......@@ -9283,6 +9283,15 @@ msgstr ""
msgid "DevOps Score"
msgstr ""
msgid "DevopsAdoptionSegmentSelection|The maximum number of selections has been reached"
msgstr ""
msgid "DevopsAdoptionSegmentSelection|The selection cannot be configured for a project and for a group at the same time"
msgstr ""
msgid "DevopsAdoptionSegment|The maximum number of segments has been reached"
msgstr ""
msgid "DevopsAdoption|Add a segment to get started"
msgstr ""
......
# frozen_string_literal: true
FactoryBot.define do
factory :devops_adoption_segment_selection, class: 'Analytics::DevopsAdoption::SegmentSelection' do
association :segment, factory: :devops_adoption_segment
project
trait :project do
group { nil }
project
end
trait :group do
project { nil }
group
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :devops_adoption_segment, class: 'Analytics::DevopsAdoption::Segment' do
sequence(:name) { |n| "Segment #{n}" }
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Analytics::DevopsAdoption::SegmentSelection, type: :model do
subject { build(:devops_adoption_segment_selection, :project) }
describe 'validation' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project) }
it { is_expected.to validate_presence_of(:segment) }
context do
subject { create(:devops_adoption_segment_selection, :project, project: project) }
it { is_expected.to validate_uniqueness_of(:project_id).scoped_to(:segment_id) }
end
context do
subject { create(:devops_adoption_segment_selection, :group, group: group) }
it { is_expected.to validate_uniqueness_of(:group_id).scoped_to(:segment_id) }
end
it 'project is required' do
selection = build(:devops_adoption_segment_selection, project: nil, group: nil)
selection.validate
expect(selection.errors).to have_key(:project)
end
it 'project is not required when a group is given' do
selection = build(:devops_adoption_segment_selection, :group, group: group)
expect(selection).to be_valid
end
it 'does not allow group to be set when project is present' do
selection = build(:devops_adoption_segment_selection)
selection.group = group
selection.project = project
selection.validate
expect(selection.errors[:group]).to eq([s_('DevopsAdoptionSegmentSelection|The selection cannot be configured for a project and for a group at the same time')])
end
context 'limit the number of segment selections' do
let_it_be(:segment) { create(:devops_adoption_segment) }
subject { build(:devops_adoption_segment_selection, segment: segment, project: project) }
before do
create(:devops_adoption_segment_selection, :project, segment: segment)
stub_const("#{described_class}::ALLOWED_SELECTIONS_PER_SEGMENT", 1)
end
it 'shows validation error' do
subject.validate
expect(subject.errors[:segment]).to eq([s_('DevopsAdoptionSegment|The maximum number of selections has been reached')])
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Analytics::DevopsAdoption::Segment, type: :model do
subject { build(:devops_adoption_segment) }
describe 'validation' do
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name) }
it { is_expected.to validate_length_of(:name).is_at_most(255) }
context 'limit the number of segments' do
subject { build(:devops_adoption_segment) }
before do
create_list(:devops_adoption_segment, 2)
stub_const("#{described_class}::ALLOWED_SEGMENT_COUNT", 2)
end
it 'shows validation error' do
subject.validate
expect(subject.errors[:name]).to eq([s_('DevopsAdoptionSegment|The maximum number of segments has been reached')])
end
end
end
end
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