Commit 7fed848f authored by Alexandru Croitor's avatar Alexandru Croitor

Fetch iterations by cadence id

Allow to fetch iterations by cadence id.

https://gitlab.com/gitlab-org/gitlab/-/issues/323074#note_538374164
parent 29f133ee
...@@ -3695,6 +3695,7 @@ Represents an iteration object. ...@@ -3695,6 +3695,7 @@ Represents an iteration object.
| `dueDate` | [`Time`](#time) | Timestamp of the iteration due date. | | `dueDate` | [`Time`](#time) | Timestamp of the iteration due date. |
| `id` | [`ID!`](#id) | ID of the iteration. | | `id` | [`ID!`](#id) | ID of the iteration. |
| `iid` | [`ID!`](#id) | Internal ID of the iteration. | | `iid` | [`ID!`](#id) | Internal ID of the iteration. |
| `iterationCadence` | [`IterationCadence!`](#iterationcadence) | Cadence of the iteration. |
| `report` | [`TimeboxReport`](#timeboxreport) | Historically accurate report about the timebox. | | `report` | [`TimeboxReport`](#timeboxreport) | Historically accurate report about the timebox. |
| `scopedPath` | [`String`](#string) | Web path of the iteration, scoped to the query parent. Only valid for Project parents. Returns null in other contexts. | | `scopedPath` | [`String`](#string) | Web path of the iteration, scoped to the query parent. Only valid for Project parents. Returns null in other contexts. |
| `scopedUrl` | [`String`](#string) | Web URL of the iteration, scoped to the query parent. Only valid for Project parents. Returns null in other contexts. | | `scopedUrl` | [`String`](#string) | Web URL of the iteration, scoped to the query parent. Only valid for Project parents. Returns null in other contexts. |
......
...@@ -52,6 +52,7 @@ class IterationsFinder ...@@ -52,6 +52,7 @@ class IterationsFinder
items = by_search_title(items) items = by_search_title(items)
items = by_state(items) items = by_state(items)
items = by_timeframe(items) items = by_timeframe(items)
items = by_iteration_cadences(items)
order(items) order(items)
end end
...@@ -88,11 +89,9 @@ class IterationsFinder ...@@ -88,11 +89,9 @@ class IterationsFinder
end end
def by_id(items) def by_id(items)
if params[:id] return items unless params[:id].present?
items.id_in(params[:id]) items.id_in(params[:id])
else
items
end
end end
def by_iid(items) def by_iid(items)
...@@ -100,19 +99,15 @@ class IterationsFinder ...@@ -100,19 +99,15 @@ class IterationsFinder
end end
def by_title(items) def by_title(items)
if params[:title] return items unless params[:title].present?
items.with_title(params[:title]) items.with_title(params[:title])
else
items
end
end end
def by_search_title(items) def by_search_title(items)
if params[:search_title].present? return items unless params[:search_title].present?
items.search_title(params[:search_title]) items.search_title(params[:search_title])
else
items
end
end end
def by_state(items) def by_state(items)
...@@ -121,6 +116,12 @@ class IterationsFinder ...@@ -121,6 +116,12 @@ class IterationsFinder
Iteration.filter_by_state(items, params[:state]) Iteration.filter_by_state(items, params[:state])
end end
def by_iteration_cadences(items)
return items unless params[:iteration_cadence_ids].present?
items.by_iteration_cadence_ids(params[:iteration_cadence_ids])
end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def order(items) def order(items)
order_statement = Gitlab::Database.nulls_last_order('due_date', 'ASC') order_statement = Gitlab::Database.nulls_last_order('due_date', 'ASC')
......
...@@ -25,6 +25,10 @@ module Resolvers ...@@ -25,6 +25,10 @@ module Resolvers
required: false, required: false,
description: 'Whether to include ancestor iterations. Defaults to true.' description: 'Whether to include ancestor iterations. Defaults to true.'
argument :iteration_cadence_ids, [::Types::GlobalIDType[::Iterations::Cadence]],
required: false,
description: 'Global iteration cadence IDs by which to look up the iterations.'
type Types::IterationType.connection_type, null: true type Types::IterationType.connection_type, null: true
def resolve(**args) def resolve(**args)
...@@ -33,6 +37,7 @@ module Resolvers ...@@ -33,6 +37,7 @@ module Resolvers
authorize! authorize!
args[:id] = id_from_args(args) args[:id] = id_from_args(args)
args[:iteration_cadence_ids] = parse_iteration_cadence_ids(args[:iteration_cadence_ids])
args[:include_ancestors] = true if args[:include_ancestors].nil? && args[:iid].nil? args[:include_ancestors] = true if args[:include_ancestors].nil? && args[:iid].nil?
iterations = IterationsFinder.new(context[:current_user], iterations_finder_params(args)).execute iterations = IterationsFinder.new(context[:current_user], iterations_finder_params(args)).execute
...@@ -49,9 +54,10 @@ module Resolvers ...@@ -49,9 +54,10 @@ module Resolvers
IterationsFinder.params_for_parent(parent, include_ancestors: args[:include_ancestors]).merge!( IterationsFinder.params_for_parent(parent, include_ancestors: args[:include_ancestors]).merge!(
id: args[:id], id: args[:id],
iid: args[:iid], iid: args[:iid],
iteration_cadence_ids: args[:iteration_cadence_ids],
state: args[:state] || 'all', state: args[:state] || 'all',
start_date: args[:start_date], start_date: args.dig(:timeframe, :start) || args[:start_date],
end_date: args[:end_date], end_date: args.dig(:timeframe, :end) || args[:end_date],
search_title: args[:title] search_title: args[:title]
) )
end end
...@@ -73,5 +79,11 @@ module Resolvers ...@@ -73,5 +79,11 @@ module Resolvers
rescue Gitlab::Graphql::Errors::ArgumentError rescue Gitlab::Graphql::Errors::ArgumentError
args[:id] args[:id]
end end
def parse_iteration_cadence_ids(iteration_cadence_ids)
return unless iteration_cadence_ids.present?
iteration_cadence_ids.map { |arg| GitlabSchema.parse_gid(arg, expected_type: ::Iterations::Cadence).model_id }
end
end end
end end
...@@ -50,5 +50,12 @@ module Types ...@@ -50,5 +50,12 @@ module Types
field :updated_at, Types::TimeType, null: false, field :updated_at, Types::TimeType, null: false,
description: 'Timestamp of last iteration update.' description: 'Timestamp of last iteration update.'
field :iteration_cadence, Types::Iterations::CadenceType, null: false,
description: 'Cadence of the iteration.'
def iteration_cadence
::Gitlab::Graphql::Loaders::BatchModelLoader.new(::Iterations::Cadence, object.iterations_cadence_id).find
end
end end
end end
...@@ -51,6 +51,7 @@ module EE ...@@ -51,6 +51,7 @@ module EE
scope :upcoming, -> { with_state(:upcoming) } scope :upcoming, -> { with_state(:upcoming) }
scope :started, -> { with_state(:started) } scope :started, -> { with_state(:started) }
scope :closed, -> { with_state(:closed) } scope :closed, -> { with_state(:closed) }
scope :by_iteration_cadence_ids, ->(cadence_ids) { where(iterations_cadence_id: cadence_ids) }
scope :within_timeframe, -> (start_date, end_date) do scope :within_timeframe, -> (start_date, end_date) do
where('start_date <= ?', end_date).where('due_date >= ?', start_date) where('start_date <= ?', end_date).where('due_date >= ?', start_date)
......
---
title: Fetch iterations by cadence id
merge_request: 57572
author:
type: added
...@@ -8,9 +8,11 @@ RSpec.describe IterationsFinder do ...@@ -8,9 +8,11 @@ RSpec.describe IterationsFinder do
let_it_be(:project_1) { create(:project, namespace: group) } let_it_be(:project_1) { create(:project, namespace: group) }
let_it_be(:project_2) { create(:project, namespace: group) } let_it_be(:project_2) { create(:project, namespace: group) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:iteration_cadence1) { create(:iterations_cadence, group: group, active: true, duration_in_weeks: 1, title: 'one week iterations') }
let_it_be(:iteration_cadence2) { create(:iterations_cadence, group: group, active: true, duration_in_weeks: 2, title: 'two week iterations') }
let!(:started_group_iteration) { create(:started_iteration, :skip_future_date_validation, group: group, title: 'one test', start_date: now - 1.day, due_date: now) } let!(:started_group_iteration) { create(:started_iteration, :skip_future_date_validation, iterations_cadence: iteration_cadence2, group: iteration_cadence2.group, title: 'one test', start_date: now - 1.day, due_date: now) }
let!(:upcoming_group_iteration) { create(:iteration, group: group, start_date: 1.day.from_now, due_date: 2.days.from_now) } let!(:upcoming_group_iteration) { create(:iteration, iterations_cadence: iteration_cadence1, group: iteration_cadence1.group, start_date: 1.day.from_now, due_date: 2.days.from_now) }
let!(:iteration_from_project_1) { create(:started_iteration, :skip_project_validation, project: project_1, start_date: 3.days.from_now, due_date: 4.days.from_now) } let!(:iteration_from_project_1) { create(:started_iteration, :skip_project_validation, project: project_1, start_date: 3.days.from_now, due_date: 4.days.from_now) }
let!(:iteration_from_project_2) { create(:started_iteration, :skip_project_validation, project: project_2, start_date: 5.days.from_now, due_date: 6.days.from_now) } let!(:iteration_from_project_2) { create(:started_iteration, :skip_project_validation, project: project_2, start_date: 5.days.from_now, due_date: 6.days.from_now) }
let(:project_ids) { [project_1.id, project_2.id] } let(:project_ids) { [project_1.id, project_2.id] }
...@@ -121,6 +123,18 @@ RSpec.describe IterationsFinder do ...@@ -121,6 +123,18 @@ RSpec.describe IterationsFinder do
expect(subject).to contain_exactly(iteration_from_project_1) expect(subject).to contain_exactly(iteration_from_project_1)
end end
it 'filters by cadence' do
params[:iteration_cadence_ids] = iteration_cadence1.id
expect(subject).to contain_exactly(upcoming_group_iteration)
end
it 'filters by multiple cadences' do
params[:iteration_cadence_ids] = [iteration_cadence1.id, iteration_cadence2.id]
expect(subject).to contain_exactly(started_group_iteration, upcoming_group_iteration)
end
context 'by timeframe' do context 'by timeframe' do
it 'returns iterations with start_date and due_date between timeframe' do it 'returns iterations with start_date and due_date between timeframe' do
params.merge!(start_date: now - 1.day, end_date: 3.days.from_now) params.merge!(start_date: now - 1.day, end_date: 3.days.from_now)
......
...@@ -8,6 +8,19 @@ RSpec.describe Resolvers::IterationsResolver do ...@@ -8,6 +8,19 @@ RSpec.describe Resolvers::IterationsResolver do
describe '#resolve' do describe '#resolve' do
let_it_be(:current_user) { create(:user) } let_it_be(:current_user) { create(:user) }
let(:params_list) do
{
id: nil,
iid: nil,
iteration_cadence_ids: nil,
group_ids: nil,
state: nil,
start_date: nil,
end_date: nil,
search_title: nil
}
end
context 'for group iterations' do context 'for group iterations' do
let_it_be(:now) { Time.now } let_it_be(:now) { Time.now }
let_it_be(:group) { create(:group, :private) } let_it_be(:group) { create(:group, :private) }
...@@ -30,7 +43,7 @@ RSpec.describe Resolvers::IterationsResolver do ...@@ -30,7 +43,7 @@ RSpec.describe Resolvers::IterationsResolver do
context 'without parameters' do context 'without parameters' do
it 'calls IterationsFinder to retrieve all iterations' do it 'calls IterationsFinder to retrieve all iterations' do
params = { id: nil, iid: nil, group_ids: Group.where(id: group.id).select(:id), state: 'all', start_date: nil, end_date: nil, search_title: nil } params = params_list.merge(group_ids: Group.where(id: group.id).select(:id), state: 'all')
expect(IterationsFinder).to receive(:new).with(current_user, params).and_call_original expect(IterationsFinder).to receive(:new).with(current_user, params).and_call_original
...@@ -39,23 +52,40 @@ RSpec.describe Resolvers::IterationsResolver do ...@@ -39,23 +52,40 @@ RSpec.describe Resolvers::IterationsResolver do
end end
context 'with parameters' do context 'with parameters' do
it 'calls IterationsFinder with correct parameters' do it 'calls IterationsFinder with correct parameters, using timeframe' do
start_date = now start_date = now
end_date = start_date + 1.hour end_date = start_date + 1.hour
search = 'wow' search = 'wow'
id = '1' id = '1'
iid = 2 iid = 2
params = { id: id, iid: iid, group_ids: group.id, state: 'closed', start_date: start_date, end_date: end_date, search_title: search } iteration_cadence_ids = ['5']
params = params_list.merge(id: id, iid: iid, iteration_cadence_ids: iteration_cadence_ids, group_ids: group.id, state: 'closed', start_date: start_date, end_date: end_date, search_title: search)
expect(IterationsFinder).to receive(:new).with(current_user, params).and_call_original expect(IterationsFinder).to receive(:new).with(current_user, params).and_call_original
resolve_group_iterations(start_date: start_date, end_date: end_date, state: 'closed', title: search, id: 'gid://gitlab/Iteration/1', iid: iid) resolve_group_iterations(timeframe: { start: start_date, end: end_date }, state: 'closed', title: search, id: 'gid://gitlab/Iteration/1', iteration_cadence_ids: ['gid://gitlab/Iterations::Cadence/5'], iid: iid)
end
it 'calls IterationsFinder with correct parameters, using start and end date' do
start_date = now
end_date = start_date + 1.hour
search = 'wow'
id = '1'
iid = 2
iteration_cadence_ids = ['5']
params = params_list.merge(id: id, iid: iid, iteration_cadence_ids: iteration_cadence_ids, group_ids: group.id, state: 'closed', start_date: start_date, end_date: end_date, search_title: search)
expect(IterationsFinder).to receive(:new).with(current_user, params).and_call_original
resolve_group_iterations(start_date: start_date, end_date: end_date, state: 'closed', title: search, id: 'gid://gitlab/Iteration/1', iteration_cadence_ids: ['gid://gitlab/Iterations::Cadence/5'], iid: iid)
end end
it 'accepts a raw model id for backward compatibility' do it 'accepts a raw model id for backward compatibility' do
id = 1 id = 1
iid = 2 iid = 2
params = { id: id, iid: iid, group_ids: group.id, state: 'all', start_date: nil, end_date: nil, search_title: nil } params = params_list.merge(id: id, iid: iid, group_ids: group.id, state: 'all')
expect(IterationsFinder).to receive(:new).with(current_user, params).and_call_original expect(IterationsFinder).to receive(:new).with(current_user, params).and_call_original
...@@ -67,7 +97,7 @@ RSpec.describe Resolvers::IterationsResolver do ...@@ -67,7 +97,7 @@ RSpec.describe Resolvers::IterationsResolver do
let_it_be(:subgroup) { create(:group, :private, parent: group) } let_it_be(:subgroup) { create(:group, :private, parent: group) }
it 'defaults to include_ancestors' do it 'defaults to include_ancestors' do
params = { id: nil, iid: nil, group_ids: subgroup.self_and_ancestors.select(:id), state: 'all', start_date: nil, end_date: nil, search_title: nil } params = params_list.merge(group_ids: subgroup.self_and_ancestors.select(:id), state: 'all')
expect(IterationsFinder).to receive(:new).with(current_user, params).and_call_original expect(IterationsFinder).to receive(:new).with(current_user, params).and_call_original
...@@ -75,7 +105,7 @@ RSpec.describe Resolvers::IterationsResolver do ...@@ -75,7 +105,7 @@ RSpec.describe Resolvers::IterationsResolver do
end end
it 'does not default to include_ancestors if IID is supplied' do it 'does not default to include_ancestors if IID is supplied' do
params = { id: nil, iid: 1, group_ids: subgroup.id, state: 'all', start_date: nil, end_date: nil, search_title: nil } params = params_list.merge(iid: 1, group_ids: subgroup.id, state: 'all')
expect(IterationsFinder).to receive(:new).with(current_user, params).and_call_original expect(IterationsFinder).to receive(:new).with(current_user, params).and_call_original
...@@ -83,7 +113,7 @@ RSpec.describe Resolvers::IterationsResolver do ...@@ -83,7 +113,7 @@ RSpec.describe Resolvers::IterationsResolver do
end end
it 'accepts include_ancestors false' do it 'accepts include_ancestors false' do
params = { id: nil, iid: nil, group_ids: subgroup.id, state: 'all', start_date: nil, end_date: nil, search_title: nil } params = params_list.merge(group_ids: subgroup.id, state: 'all')
expect(IterationsFinder).to receive(:new).with(current_user, params).and_call_original expect(IterationsFinder).to receive(:new).with(current_user, params).and_call_original
...@@ -92,6 +122,18 @@ RSpec.describe Resolvers::IterationsResolver do ...@@ -92,6 +122,18 @@ RSpec.describe Resolvers::IterationsResolver do
end end
context 'by timeframe' do context 'by timeframe' do
context 'when start_date and end_date are present' do
context 'when start date is after end_date' do
it 'raises error' do
expect do
resolve_group_iterations(timeframe: { start: now, end: now - 2.days })
end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, "start must be before end")
end
end
end
end
context 'by dates' do
context 'when start_date and end_date are present' do context 'when start_date and end_date are present' do
context 'when start date is after end_date' do context 'when start date is after end_date' do
it 'raises error' do it 'raises error' do
......
...@@ -10,7 +10,7 @@ RSpec.describe GitlabSchema.types['Iteration'] do ...@@ -10,7 +10,7 @@ RSpec.describe GitlabSchema.types['Iteration'] do
it 'has the expected fields' do it 'has the expected fields' do
expected_fields = %w[ expected_fields = %w[
id id title description state web_path web_url scoped_path scoped_url id id title description state web_path web_url scoped_path scoped_url
due_date start_date created_at updated_at report due_date start_date created_at updated_at report iteration_cadence
] ]
expect(described_class).to have_graphql_fields(*expected_fields).at_least expect(described_class).to have_graphql_fields(*expected_fields).at_least
......
...@@ -432,6 +432,26 @@ RSpec.describe Iteration do ...@@ -432,6 +432,26 @@ RSpec.describe Iteration do
end end
end end
describe '.by_iteration_cadence_ids' do
let_it_be(:iterations_cadence1) { create(:iterations_cadence, group: group, start_date: 10.days.ago) }
let_it_be(:iterations_cadence2) { create(:iterations_cadence, group: group, start_date: 10.days.ago) }
let_it_be(:closed_iteration) { create(:iteration, :closed, :skip_future_date_validation, iterations_cadence: iterations_cadence1, group: group, start_date: 8.days.ago, due_date: 2.days.ago) }
let_it_be(:started_iteration) { create(:iteration, :started, :skip_future_date_validation, iterations_cadence: iterations_cadence2, group: group, start_date: 1.day.ago, due_date: 6.days.from_now) }
let_it_be(:upcoming_iteration) { create(:iteration, :upcoming, iterations_cadence: iterations_cadence2, group: group, start_date: 1.week.from_now, due_date: 2.weeks.from_now) }
it 'returns iterations by cadence' do
iterations = described_class.by_iteration_cadence_ids(iterations_cadence1)
expect(iterations).to match_array([closed_iteration])
end
it 'returns iterations by multiple cadences' do
iterations = described_class.by_iteration_cadence_ids([iterations_cadence1, iterations_cadence2])
expect(iterations).to match_array([closed_iteration, started_iteration, upcoming_iteration])
end
end
it_behaves_like 'a timebox', :iteration do it_behaves_like 'a timebox', :iteration do
let(:timebox_args) { [:skip_project_validation] } let(:timebox_args) { [:skip_project_validation] }
let(:timebox_table_name) { described_class.table_name.to_sym } let(:timebox_table_name) { described_class.table_name.to_sym }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'getting iterations' do
include GraphqlHelpers
let_it_be(:now) { Time.now }
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:iteration_cadence1) { create(:iterations_cadence, group: group, active: true, duration_in_weeks: 1, title: 'one week iterations') }
let_it_be(:iteration_cadence2) { create(:iterations_cadence, group: group, active: true, duration_in_weeks: 2, title: 'two week iterations') }
let_it_be(:started_group_iteration) { create(:started_iteration, :skip_future_date_validation, iterations_cadence: iteration_cadence1, group: iteration_cadence1.group, title: 'one test', start_date: now - 1.day, due_date: now) }
let_it_be(:upcoming_group_iteration) { create(:iteration, iterations_cadence: iteration_cadence2, group: iteration_cadence2.group, start_date: 1.day.from_now, due_date: 2.days.from_now) }
let_it_be(:closed_group_iteration) { create(:closed_iteration, :skip_project_validation, iterations_cadence: iteration_cadence1, group: iteration_cadence1.group, start_date: 3.days.from_now, due_date: 4.days.from_now) }
before do
group.add_maintainer(user)
end
describe 'query for iterations by timeframe' do
context 'without start date' do
it 'returns error' do
post_graphql(iterations_query(group, "timeframe: { end: \"#{3.days.ago.to_date}\" }"), current_user: user)
expect(graphql_errors).to include(a_hash_including('message' => "Argument 'start' on InputObject 'Timeframe' is required. Expected type Date!"))
end
end
context 'without end date' do
it 'returns error' do
post_graphql(iterations_query(group, "timeframe: { start: \"#{3.days.ago.to_date}\" }"), current_user: user)
expect(graphql_errors).to include(a_hash_including('message' => "Argument 'end' on InputObject 'Timeframe' is required. Expected type Date!"))
end
end
context 'with start and end date' do
it 'does not have errors' do
post_graphql(iterations_query(group, "timeframe: { start: \"#{3.days.ago.to_date}\", end: \"#{3.days.from_now.to_date}\" }"), current_user: user)
expect(graphql_errors).to be_nil
end
end
end
describe 'query for iterations by cadence' do
context 'with multiple cadences' do
it 'returns iterations' do
post_graphql(iteration_cadence_query(group, [iteration_cadence1.to_global_id, iteration_cadence2.to_global_id]), current_user: user)
expect_iterations_response(started_group_iteration, closed_group_iteration, upcoming_group_iteration)
end
end
end
def iteration_cadence_query(group, cadence_ids)
cadence_ids_param = "[\"#{cadence_ids.join('","')}\"]"
field_queries = "iterationCadenceIds: #{cadence_ids_param}"
iterations_query(group, field_queries)
end
def iterations_query(group, field_queries)
<<~QUERY
query {
group(fullPath: "#{group.full_path}") {
id,
iterations(#{field_queries}) {
nodes {
id
}
}
}
}
QUERY
end
def expect_iterations_response(*iterations)
actual_iterations = graphql_data['group']['iterations']['nodes'].map { |iteration| iteration['id'] }
expected_iterations = iterations.map { |iteration| iteration.to_global_id.to_s }
expect(actual_iterations).to contain_exactly(*expected_iterations)
expect(graphql_errors).to be_nil
end
end
...@@ -77,7 +77,9 @@ RSpec.describe 'Creating an Iteration' do ...@@ -77,7 +77,9 @@ RSpec.describe 'Creating an Iteration' do
end end
end end
context 'when a project_path is given' do # Skipping creation of project level iterations.
# Pending https://gitlab.com/gitlab-org/gitlab/-/issues/299864
xcontext 'when a project_path is given' do
let_it_be(:project) { create(:project, namespace: group) } let_it_be(:project) { create(:project, namespace: group) }
let(:params) { { project_path: project.full_path } } let(:params) { { project_path: project.full_path } }
......
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