Commit ef373b8c authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Merge branch 'create-resource-iteration-events-table' into 'master'

Create DB table to track changes of the iterations on issues

See merge request gitlab-org/gitlab!37617
parents b719b6b0 2e8eafd4
# frozen_string_literal: true
class ResourceIterationEvent < ResourceTimeboxEvent
belongs_to :iteration
end
# frozen_string_literal: true
class ResourceMilestoneEvent < ResourceEvent
class ResourceMilestoneEvent < ResourceTimeboxEvent
include IgnorableColumns
include IssueResourceEvent
include MergeRequestResourceEvent
belongs_to :milestone
validate :exactly_one_issuable
scope :include_relations, -> { includes(:user, milestone: [:project, :group]) }
enum action: {
add: 1,
remove: 2
}
# state is used for issue and merge request states.
enum state: Issue.available_states.merge(MergeRequest.available_states)
ignore_columns %i[reference reference_html cached_markdown_version], remove_with: '13.1', remove_after: '2020-06-22'
def self.issuable_attrs
%i(issue merge_request).freeze
end
def milestone_title
milestone&.title
end
......@@ -32,8 +19,4 @@ class ResourceMilestoneEvent < ResourceEvent
def milestone_parent
milestone&.parent
end
def issuable
issue || merge_request
end
end
# frozen_string_literal: true
class ResourceTimeboxEvent < ResourceEvent
self.abstract_class = true
include IssueResourceEvent
include MergeRequestResourceEvent
validate :exactly_one_issuable
enum action: {
add: 1,
remove: 2
}
def self.issuable_attrs
%i(issue merge_request).freeze
end
def issuable
issue || merge_request
end
end
---
title: Add DB table and model to track changes of the iterations on issues
merge_request: 37617
author:
type: added
# frozen_string_literal: true
class CreateResourceIterationEventsTable < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
create_table :resource_iteration_events do |t|
t.bigint :user_id, null: false, index: { name: 'index_resource_iteration_events_on_user_id' }
t.bigint :issue_id, null: true, index: { name: 'index_resource_iteration_events_on_issue_id' }
t.bigint :merge_request_id, null: true, index: { name: 'index_resource_iteration_events_on_merge_request_id' }
t.bigint :iteration_id, index: { name: 'index_resource_iteration_events_on_iteration_id' }
t.datetime_with_timezone :created_at, null: false
t.integer :action, limit: 2, null: false
end
end
end
# frozen_string_literal: true
class AddUsersFkToResourceIterationEventsTable < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_foreign_key :resource_iteration_events, :users, column: :user_id, on_delete: :nullify # rubocop:disable Migration/AddConcurrentForeignKey
end
end
def down
with_lock_retries do
remove_foreign_key :resource_iteration_events, column: :user_id
end
end
end
# frozen_string_literal: true
class AddIssuesFkToResourceIterationEventsTable < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_foreign_key :resource_iteration_events, :issues, column: :issue_id, on_delete: :cascade # rubocop:disable Migration/AddConcurrentForeignKey
end
end
def down
with_lock_retries do
remove_foreign_key :resource_iteration_events, column: :issue_id
end
end
end
# frozen_string_literal: true
class AddMergeRequestsFkToResourceIterationEventsTable < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_foreign_key :resource_iteration_events, :merge_requests, column: :merge_request_id, on_delete: :cascade # rubocop:disable Migration/AddConcurrentForeignKey
end
end
def down
with_lock_retries do
remove_foreign_key :resource_iteration_events, column: :merge_request_id
end
end
end
# frozen_string_literal: true
class AddIterationsFkToResourceIterationEventsTable < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_foreign_key :resource_iteration_events, :sprints, column: :iteration_id, on_delete: :cascade # rubocop:disable Migration/AddConcurrentForeignKey
end
end
def down
with_lock_retries do
remove_foreign_key :resource_iteration_events, column: :iteration_id
end
end
end
5fc97e904844b43a331246c68b1d0a879f683d5b36fab6cd024f417922259864
\ No newline at end of file
7b33a7f96ea02e3d5510cb6a6d75a9c753c1bd24c93d9995f91596d6b1a44592
\ No newline at end of file
543ebc8ab7fad96aeb5a8618bc954babb0eed1d252fe490aa6fab9b8e80ffbf1
\ No newline at end of file
4a6a322cf3f6e1622d3244f6faa99a1befcb8c626a8fea36b09d1910b770b88c
\ No newline at end of file
0307eb0f5082a5da84f5e94084b677d13e03f9cd5011efe3dc7e25645a00082d
\ No newline at end of file
......@@ -15063,6 +15063,25 @@ CREATE SEQUENCE public.requirements_management_test_reports_id_seq
ALTER SEQUENCE public.requirements_management_test_reports_id_seq OWNED BY public.requirements_management_test_reports.id;
CREATE TABLE public.resource_iteration_events (
id bigint NOT NULL,
user_id bigint NOT NULL,
issue_id bigint,
merge_request_id bigint,
iteration_id bigint,
created_at timestamp with time zone NOT NULL,
action smallint NOT NULL
);
CREATE SEQUENCE public.resource_iteration_events_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.resource_iteration_events_id_seq OWNED BY public.resource_iteration_events.id;
CREATE TABLE public.resource_label_events (
id bigint NOT NULL,
action integer NOT NULL,
......@@ -17214,6 +17233,8 @@ ALTER TABLE ONLY public.requirements ALTER COLUMN id SET DEFAULT nextval('public
ALTER TABLE ONLY public.requirements_management_test_reports ALTER COLUMN id SET DEFAULT nextval('public.requirements_management_test_reports_id_seq'::regclass);
ALTER TABLE ONLY public.resource_iteration_events ALTER COLUMN id SET DEFAULT nextval('public.resource_iteration_events_id_seq'::regclass);
ALTER TABLE ONLY public.resource_label_events ALTER COLUMN id SET DEFAULT nextval('public.resource_label_events_id_seq'::regclass);
ALTER TABLE ONLY public.resource_milestone_events ALTER COLUMN id SET DEFAULT nextval('public.resource_milestone_events_id_seq'::regclass);
......@@ -18440,6 +18461,9 @@ ALTER TABLE ONLY public.requirements_management_test_reports
ALTER TABLE ONLY public.requirements
ADD CONSTRAINT requirements_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.resource_iteration_events
ADD CONSTRAINT resource_iteration_events_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.resource_label_events
ADD CONSTRAINT resource_label_events_pkey PRIMARY KEY (id);
......@@ -20526,6 +20550,14 @@ CREATE INDEX index_requirements_on_title_trigram ON public.requirements USING gi
CREATE INDEX index_requirements_on_updated_at ON public.requirements USING btree (updated_at);
CREATE INDEX index_resource_iteration_events_on_issue_id ON public.resource_iteration_events USING btree (issue_id);
CREATE INDEX index_resource_iteration_events_on_iteration_id ON public.resource_iteration_events USING btree (iteration_id);
CREATE INDEX index_resource_iteration_events_on_merge_request_id ON public.resource_iteration_events USING btree (merge_request_id);
CREATE INDEX index_resource_iteration_events_on_user_id ON public.resource_iteration_events USING btree (user_id);
CREATE INDEX index_resource_label_events_issue_id_label_id_action ON public.resource_label_events USING btree (issue_id, label_id, action);
CREATE INDEX index_resource_label_events_on_epic_id ON public.resource_label_events USING btree (epic_id);
......@@ -22275,6 +22307,9 @@ ALTER TABLE ONLY public.security_scans
ALTER TABLE ONLY public.merge_request_diff_files
ADD CONSTRAINT fk_rails_501aa0a391 FOREIGN KEY (merge_request_diff_id) REFERENCES public.merge_request_diffs(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.resource_iteration_events
ADD CONSTRAINT fk_rails_501fa15d69 FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE SET NULL;
ALTER TABLE ONLY public.status_page_settings
ADD CONSTRAINT fk_rails_506e5ba391 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
......@@ -22416,6 +22451,9 @@ ALTER TABLE ONLY public.web_hook_logs
ALTER TABLE ONLY public.jira_imports
ADD CONSTRAINT fk_rails_675d38c03b FOREIGN KEY (label_id) REFERENCES public.labels(id) ON DELETE SET NULL;
ALTER TABLE ONLY public.resource_iteration_events
ADD CONSTRAINT fk_rails_6830c13ac1 FOREIGN KEY (merge_request_id) REFERENCES public.merge_requests(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.geo_hashed_storage_migrated_events
ADD CONSTRAINT fk_rails_687ed7d7c5 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
......@@ -22713,6 +22751,9 @@ ALTER TABLE ONLY public.x509_commit_signatures
ALTER TABLE ONLY public.ci_build_trace_sections
ADD CONSTRAINT fk_rails_ab7c104e26 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.resource_iteration_events
ADD CONSTRAINT fk_rails_abf5d4affa FOREIGN KEY (issue_id) REFERENCES public.issues(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.clusters
ADD CONSTRAINT fk_rails_ac3a663d79 FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE SET NULL;
......@@ -22869,6 +22910,9 @@ ALTER TABLE ONLY public.issue_tracker_data
ALTER TABLE ONLY public.resource_milestone_events
ADD CONSTRAINT fk_rails_cedf8cce4d FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE SET NULL;
ALTER TABLE ONLY public.resource_iteration_events
ADD CONSTRAINT fk_rails_cee126f66c FOREIGN KEY (iteration_id) REFERENCES public.sprints(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.epic_metrics
ADD CONSTRAINT fk_rails_d071904753 FOREIGN KEY (epic_id) REFERENCES public.epics(id) ON DELETE CASCADE;
......
......@@ -165,6 +165,9 @@ RSpec.describe 'Database schema' do
context 'for enums' do
ApplicationRecord.descendants.each do |model|
# skip model if it is an abstract class as it would not have an associated DB table
next if model.abstract_class?
describe model do
let(:ignored_enums) { ignored_limit_enums(model.name) }
let(:enums) { model.defined_enums.keys - ignored_enums }
......
# frozen_string_literal: true
FactoryBot.define do
factory :resource_iteration_event do
issue { merge_request.nil? ? create(:issue) : nil }
merge_request { nil }
iteration
action { :add }
user { issue&.author || merge_request&.author || create(:user) }
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ResourceIterationEvent, type: :model do
it_behaves_like 'a resource event'
it_behaves_like 'a resource event for issues'
it_behaves_like 'a resource event for merge requests'
it_behaves_like 'having unique enum values'
it_behaves_like 'timebox resource event validations'
it_behaves_like 'timebox resource event actions'
describe 'associations' do
it { is_expected.to belong_to(:iteration) }
end
end
......@@ -8,77 +8,14 @@ RSpec.describe ResourceMilestoneEvent, type: :model do
it_behaves_like 'a resource event for merge requests'
it_behaves_like 'having unique enum values'
it_behaves_like 'timebox resource event validations'
it_behaves_like 'timebox resource event states'
it_behaves_like 'timebox resource event actions'
describe 'associations' do
it { is_expected.to belong_to(:milestone) }
end
describe 'validations' do
context 'when issue and merge_request are both nil' do
subject { build(described_class.name.underscore.to_sym, issue: nil, merge_request: nil) }
it { is_expected.not_to be_valid }
end
context 'when issue and merge_request are both set' do
subject { build(described_class.name.underscore.to_sym, issue: build(:issue), merge_request: build(:merge_request)) }
it { is_expected.not_to be_valid }
end
context 'when issue is set' do
subject { create(described_class.name.underscore.to_sym, issue: create(:issue), merge_request: nil) }
it { is_expected.to be_valid }
end
context 'when merge_request is set' do
subject { create(described_class.name.underscore.to_sym, issue: nil, merge_request: create(:merge_request)) }
it { is_expected.to be_valid }
end
end
describe 'states' do
[Issue, MergeRequest].each do |klass|
klass.available_states.each do |state|
it "supports state #{state.first} for #{klass.name.underscore}" do
model = create(klass.name.underscore, state: state[0])
key = model.class.name.underscore
event = build(described_class.name.underscore.to_sym, key => model, state: model.state)
expect(event.state).to eq(state[0])
end
end
end
end
shared_examples 'a milestone action queryable resource event' do |expected_results_for_actions|
[Issue, MergeRequest].each do |klass|
expected_results_for_actions.each do |action, expected_result|
it "is #{expected_result} for action #{action} on #{klass.name.underscore}" do
model = create(klass.name.underscore)
key = model.class.name.underscore
event = build(described_class.name.underscore.to_sym, key => model, action: action)
expect(event.send(query_method)).to eq(expected_result)
end
end
end
end
describe '#added?' do
it_behaves_like 'a milestone action queryable resource event', { add: true, remove: false } do
let(:query_method) { :add? }
end
end
describe '#removed?' do
it_behaves_like 'a milestone action queryable resource event', { add: false, remove: true } do
let(:query_method) { :remove? }
end
end
describe '#milestone_title' do
let(:milestone) { create(:milestone, title: 'v2.3') }
let(:event) { create(:resource_milestone_event, milestone: milestone) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.shared_examples 'timebox resource event validations' do
describe 'validations' do
context 'when issue and merge_request are both nil' do
subject { build(described_class.name.underscore.to_sym, issue: nil, merge_request: nil) }
it { is_expected.not_to be_valid }
end
context 'when issue and merge_request are both set' do
subject { build(described_class.name.underscore.to_sym, issue: build(:issue), merge_request: build(:merge_request)) }
it { is_expected.not_to be_valid }
end
context 'when issue is set' do
subject { create(described_class.name.underscore.to_sym, issue: create(:issue), merge_request: nil) }
it { is_expected.to be_valid }
end
context 'when merge_request is set' do
subject { create(described_class.name.underscore.to_sym, issue: nil, merge_request: create(:merge_request)) }
it { is_expected.to be_valid }
end
end
end
RSpec.shared_examples 'timebox resource event states' do
describe 'states' do
[Issue, MergeRequest].each do |klass|
klass.available_states.each do |state|
it "supports state #{state.first} for #{klass.name.underscore}" do
model = create(klass.name.underscore, state: state[0])
key = model.class.name.underscore
event = build(described_class.name.underscore.to_sym, key => model, state: model.state)
expect(event.state).to eq(state[0])
end
end
end
end
end
RSpec.shared_examples 'queryable timebox action resource event' do |expected_results_for_actions|
[Issue, MergeRequest].each do |klass|
expected_results_for_actions.each do |action, expected_result|
it "is #{expected_result} for action #{action} on #{klass.name.underscore}" do
model = build(klass.name.underscore)
key = model.class.name.underscore
event = build(described_class.name.underscore.to_sym, key => model, action: action)
expect(event.send(query_method)).to eq(expected_result)
end
end
end
end
RSpec.shared_examples 'timebox resource event actions' do
describe '#added?' do
it_behaves_like 'queryable timebox action resource event', { add: true, remove: false } do
let(:query_method) { :add? }
end
end
describe '#removed?' do
it_behaves_like 'queryable timebox action resource event', { add: false, remove: true } do
let(:query_method) { :remove? }
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