Commit 8b8b0b43 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'fetch_iterations_cadences_for_group' into 'master'

Implement iteration cadences resolver

See merge request gitlab-org/gitlab!55422
parents d8955d4b d5346524
...@@ -3038,6 +3038,7 @@ Autogenerated return type of GitlabSubscriptionActivate. ...@@ -3038,6 +3038,7 @@ Autogenerated return type of GitlabSubscriptionActivate.
| `id` | [`ID!`](#id) | ID of the namespace. | | `id` | [`ID!`](#id) | ID of the namespace. |
| `isTemporaryStorageIncreaseEnabled` | [`Boolean!`](#boolean) | Status of the temporary storage increase. | | `isTemporaryStorageIncreaseEnabled` | [`Boolean!`](#boolean) | Status of the temporary storage increase. |
| `issues` | [`IssueConnection`](#issueconnection) | Issues for projects in this group. | | `issues` | [`IssueConnection`](#issueconnection) | Issues for projects in this group. |
| `iterationCadences` | [`IterationCadenceConnection`](#iterationcadenceconnection) | Find iteration cadences. |
| `iterations` | [`IterationConnection`](#iterationconnection) | Find iterations. | | `iterations` | [`IterationConnection`](#iterationconnection) | Find iterations. |
| `label` | [`Label`](#label) | A label available on this group. | | `label` | [`Label`](#label) | A label available on this group. |
| `labels` | [`LabelConnection`](#labelconnection) | Labels available on this group. | | `labels` | [`LabelConnection`](#labelconnection) | Labels available on this group. |
...@@ -3557,12 +3558,22 @@ Represents an iteration cadence. ...@@ -3557,12 +3558,22 @@ Represents an iteration cadence.
| ----- | ---- | ----------- | | ----- | ---- | ----------- |
| `active` | [`Boolean`](#boolean) | Whether the iteration cadence is active. | | `active` | [`Boolean`](#boolean) | Whether the iteration cadence is active. |
| `automatic` | [`Boolean`](#boolean) | Whether the iteration cadence should automatically generate future iterations. | | `automatic` | [`Boolean`](#boolean) | Whether the iteration cadence should automatically generate future iterations. |
| `durationInWeeks` | [`Int!`](#int) | Duration in weeks of the iterations within this cadence. | | `durationInWeeks` | [`Int`](#int) | Duration in weeks of the iterations within this cadence. |
| `id` | [`IterationsCadenceID!`](#iterationscadenceid) | Global ID of the iteration cadence. | | `id` | [`IterationsCadenceID!`](#iterationscadenceid) | Global ID of the iteration cadence. |
| `iterationsInAdvance` | [`Int!`](#int) | Future iterations to be created when iteration cadence is set to automatic. | | `iterationsInAdvance` | [`Int`](#int) | Future iterations to be created when iteration cadence is set to automatic. |
| `startDate` | [`Time`](#time) | Timestamp of the iteration cadence start date. | | `startDate` | [`Time`](#time) | Timestamp of the iteration cadence start date. |
| `title` | [`String!`](#string) | Title of the iteration cadence. | | `title` | [`String!`](#string) | Title of the iteration cadence. |
### `IterationCadenceConnection`
The connection type for IterationCadence.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `edges` | [`[IterationCadenceEdge]`](#iterationcadenceedge) | A list of edges. |
| `nodes` | [`[IterationCadence]`](#iterationcadence) | A list of nodes. |
| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
### `IterationCadenceCreatePayload` ### `IterationCadenceCreatePayload`
Autogenerated return type of IterationCadenceCreate. Autogenerated return type of IterationCadenceCreate.
...@@ -3573,6 +3584,15 @@ Autogenerated return type of IterationCadenceCreate. ...@@ -3573,6 +3584,15 @@ Autogenerated return type of IterationCadenceCreate.
| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| `iterationCadence` | [`IterationCadence`](#iterationcadence) | The created iteration cadence. | | `iterationCadence` | [`IterationCadence`](#iterationcadence) | The created iteration cadence. |
### `IterationCadenceEdge`
An edge in a connection.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `cursor` | [`String!`](#string) | A cursor for use in pagination. |
| `node` | [`IterationCadence`](#iterationcadence) | The item at the end of the edge. |
### `IterationConnection` ### `IterationConnection`
The connection type for Iteration. The connection type for Iteration.
...@@ -4688,6 +4708,7 @@ An edge in a connection. ...@@ -4688,6 +4708,7 @@ An edge in a connection.
| `issueStatusCounts` | [`IssueStatusCountsType`](#issuestatuscountstype) | Counts of issues by status for the project. | | `issueStatusCounts` | [`IssueStatusCountsType`](#issuestatuscountstype) | Counts of issues by status for the project. |
| `issues` | [`IssueConnection`](#issueconnection) | Issues of the project. | | `issues` | [`IssueConnection`](#issueconnection) | Issues of the project. |
| `issuesEnabled` | [`Boolean`](#boolean) | Indicates if Issues are enabled for the current user | | `issuesEnabled` | [`Boolean`](#boolean) | Indicates if Issues are enabled for the current user |
| `iterationCadences` | [`IterationCadenceConnection`](#iterationcadenceconnection) | Find iteration cadences. |
| `iterations` | [`IterationConnection`](#iterationconnection) | Find iterations. | | `iterations` | [`IterationConnection`](#iterationconnection) | Find iterations. |
| `jiraImportStatus` | [`String`](#string) | Status of Jira import background job of the project. | | `jiraImportStatus` | [`String`](#string) | Status of Jira import background job of the project. |
| `jiraImports` | [`JiraImportConnection`](#jiraimportconnection) | Jira imports into the project. | | `jiraImports` | [`JiraImportConnection`](#jiraimportconnection) | Jira imports into the project. |
......
# frozen_string_literal: true
# Search for iterations cadences
module Iterations
class CadencesFinder
attr_reader :current_user, :group, :params
def initialize(current_user, group, params = {})
@current_user = current_user
@group = group
@params = params
end
def execute
return Iterations::Cadence.none unless group.iteration_cadences_feature_flag_enabled?
items = Iterations::Cadence.all
items = by_id(items)
items = by_groups(items)
items = by_title(items)
items = by_duration(items)
items = by_automatic(items)
items = by_active(items)
items.ordered_by_title
end
private
def by_groups(items)
items.with_groups(groups)
end
def groups
groups = groups_to_include(group)
groups_user_can_read_cadences(groups).map(&:id)
end
def groups_to_include(group)
groups = [group]
groups += group.ancestors if include_ancestor_groups?
groups
end
def groups_user_can_read_cadences(groups)
# `same_root` should be set only if we are sure that all groups
# in related_groups have the same ancestor root group,
# and here we get the group and its ancestors
# https://gitlab.com/gitlab-org/gitlab/issues/11539
Group.preset_root_ancestor_for(groups)
DeclarativePolicy.user_scope do
groups.select { |group| Ability.allowed?(current_user, :read_iteration_cadence, group) }
end
end
def include_ancestor_groups?
params[:include_ancestor_groups]
end
def by_id(items)
return items if params[:id].blank?
items.id_in(params[:id])
end
def by_title(items)
return items if params[:title].blank?
items.search_title(params[:title])
end
def by_duration(items)
return items if params[:duration_in_weeks].blank?
items.with_duration(params[:duration_in_weeks])
end
def by_automatic(items)
return items if params[:automatic].nil?
items.is_automatic(params[:automatic])
end
def by_active(items)
return items if params[:active].nil?
items.is_active(params[:active])
end
end
end
...@@ -39,6 +39,10 @@ module EE ...@@ -39,6 +39,10 @@ module EE
description: 'Find iterations.', description: 'Find iterations.',
resolver: ::Resolvers::IterationsResolver resolver: ::Resolvers::IterationsResolver
field :iteration_cadences, ::Types::Iterations::CadenceType.connection_type, null: true,
description: 'Find iteration cadences.',
resolver: ::Resolvers::Iterations::CadencesResolver
field :timelogs, ::Types::TimelogType.connection_type, null: false, field :timelogs, ::Types::TimelogType.connection_type, null: false,
description: 'Time logged in issues by group members.', description: 'Time logged in issues by group members.',
extras: [:lookahead], extras: [:lookahead],
......
...@@ -56,6 +56,10 @@ module EE ...@@ -56,6 +56,10 @@ module EE
description: 'Find iterations.', description: 'Find iterations.',
resolver: ::Resolvers::IterationsResolver resolver: ::Resolvers::IterationsResolver
field :iteration_cadences, ::Types::Iterations::CadenceType.connection_type, null: true,
description: 'Find iteration cadences.',
resolver: ::Resolvers::Iterations::CadencesResolver
field :dast_profiles, field :dast_profiles,
::Types::Dast::ProfileType.connection_type, ::Types::Dast::ProfileType.connection_type,
null: true, null: true,
......
# frozen_string_literal: true
module Resolvers
module Iterations
class CadencesResolver < BaseResolver
include Gitlab::Graphql::CopyFieldDescription
include Gitlab::Graphql::Authorize::AuthorizeResource
argument :id, ::Types::GlobalIDType[::Iterations::Cadence], required: false,
description: 'Global ID of the iteration cadence to look up.'
argument :title, GraphQL::STRING_TYPE, required: false,
description: 'Fuzzy search by title.'
argument :duration_in_weeks, GraphQL::INT_TYPE, required: false,
description: copy_field_description(Types::Iterations::CadenceType, :duration_in_weeks)
argument :automatic, GraphQL::BOOLEAN_TYPE, required: false,
description: copy_field_description(Types::Iterations::CadenceType, :automatic)
argument :active, GraphQL::BOOLEAN_TYPE, required: false,
description: copy_field_description(Types::Iterations::CadenceType, :active)
argument :include_ancestor_groups, GraphQL::BOOLEAN_TYPE, required: false,
description: 'Whether to include ancestor groups to search iterations cadences in.'
type ::Types::Iterations::CadenceType.connection_type, null: true
def resolve(**args)
authorize!
cadences = ::Iterations::CadencesFinder.new(context[:current_user], group, args).execute
offset_pagination(cadences)
end
private
def group
@parent ||= object.respond_to?(:sync) ? object.sync : object
case @parent
when Group
@parent
when Project
raise raise_resource_not_available_error!('The project does not have a parent group. Iteration cadences are only supported only at group level.') if @parent.group.blank?
@parent.group
else raise "Unexpected parent type: #{@parent.class}"
end
end
def authorize!
Ability.allowed?(context[:current_user], :read_iteration_cadence, group) || raise_resource_not_available_error!
end
end
end
end
...@@ -14,10 +14,10 @@ module Types ...@@ -14,10 +14,10 @@ module Types
field :title, GraphQL::STRING_TYPE, null: false, field :title, GraphQL::STRING_TYPE, null: false,
description: 'Title of the iteration cadence.' description: 'Title of the iteration cadence.'
field :duration_in_weeks, GraphQL::INT_TYPE, null: false, field :duration_in_weeks, GraphQL::INT_TYPE, null: true,
description: 'Duration in weeks of the iterations within this cadence.' description: 'Duration in weeks of the iterations within this cadence.'
field :iterations_in_advance, GraphQL::INT_TYPE, null: false, field :iterations_in_advance, GraphQL::INT_TYPE, null: true,
description: 'Future iterations to be created when iteration cadence is set to automatic.' description: 'Future iterations to be created when iteration cadence is set to automatic.'
field :start_date, Types::TimeType, null: true, field :start_date, Types::TimeType, null: true,
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
module Iterations module Iterations
class Cadence < ApplicationRecord class Cadence < ApplicationRecord
include Gitlab::SQL::Pattern
self.table_name = 'iterations_cadences' self.table_name = 'iterations_cadences'
belongs_to :group belongs_to :group
...@@ -14,5 +16,15 @@ module Iterations ...@@ -14,5 +16,15 @@ module Iterations
validates :iterations_in_advance, presence: true validates :iterations_in_advance, presence: true
validates :active, inclusion: [true, false] validates :active, inclusion: [true, false]
validates :automatic, inclusion: [true, false] validates :automatic, inclusion: [true, false]
scope :with_groups, -> (group_ids) { where(group_id: group_ids) }
scope :with_duration, -> (duration) { where(duration_in_weeks: duration) }
scope :is_automatic, -> (automatic) { where(automatic: automatic) }
scope :is_active, -> (active) { where(active: active) }
scope :ordered_by_title, -> { order(:title) }
def self.search_title(query)
fuzzy_search(query, [:title])
end
end end
end end
# frozen_string_literal: true
module Iterations
class Cadence < ApplicationRecord
include BulkInsertSafe
self.table_name = 'iterations_cadences'
end
end
Gitlab::Seeder.quiet do
Group.all.each do |group|
cadences = []
1000.times do
random_number = rand(5)
cadence_params = {
title: FFaker::Lorem.sentence(6),
start_date: FFaker::Time.between(1.day.from_now, 2.weeks.from_now),
duration_in_weeks: random_number == 5 ? nil : random_number,
iterations_in_advance: random_number == 5 ? nil : random_number,
active: rand(2),
automatic: rand(2),
group_id: group.id,
created_at: Time.now,
updated_at: Time.now
}
print '.'
cadences << Iterations::Cadence.new(cadence_params)
end
Iterations::Cadence.bulk_insert!(cadences, validate: false)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Iterations::CadencesFinder do
let(:params) { {} }
let_it_be(:group) { create(:group, :private) }
let_it_be(:sub_group) { create(:group, :private, parent: group) }
let_it_be(:project) { create(:project, group: sub_group) }
let_it_be(:user) { create(:user) }
let_it_be(:active_group_iterations_cadence) { create(:iterations_cadence, group: group, active: true, duration_in_weeks: 1, title: 'one week iterations') }
let_it_be(:inactive_group_iterations_cadence) { create(:iterations_cadence, group: group, active: false, duration_in_weeks: 2, title: 'two weeks iterations') }
let_it_be(:automatic_iterations_cadence) { create(:iterations_cadence, group: group, automatic: true, duration_in_weeks: 1, title: 'one week iterations') }
let_it_be(:active_sub_group_iterations_cadence) { create(:iterations_cadence, group: sub_group, active: true, duration_in_weeks: 1, title: 'one week iterations') }
let_it_be(:inactive_sub_group_iterations_cadence) { create(:iterations_cadence, group: sub_group, active: false, duration_in_weeks: 2, title: 'two weeks iterations') }
let_it_be(:non_automatic_sub_group_iterations_cadence) { create(:iterations_cadence, group: sub_group, automatic: false, duration_in_weeks: 1, title: 'one week iterations') }
let_it_be(:current_group) { group }
subject { described_class.new(user, current_group, params).execute }
context 'without permissions' do
context 'groups and projects' do
let(:params) { {} }
it 'returns no iterations cadences for group' do
expect(subject).to be_empty
end
end
end
context 'with permissions' do
before do
group.add_reporter(user)
end
context 'with feature flag disabled' do
before do
stub_feature_flags(iteration_cadences: false)
end
it 'returns no cadences' do
expect(subject).to be_empty
end
end
context 'iterations cadences for group' do
it 'returns iterations cadences' do
expect(subject).to contain_exactly(
active_group_iterations_cadence,
inactive_group_iterations_cadence,
automatic_iterations_cadence
)
end
end
context 'iterations cadences for subgroup' do
let(:current_group) { sub_group }
it 'returns iterations cadences' do
expect(subject).to contain_exactly(
active_sub_group_iterations_cadence,
inactive_sub_group_iterations_cadence,
non_automatic_sub_group_iterations_cadence
)
end
context 'with include ancestor' do
let(:params) { { include_ancestor_groups: true } }
it 'returns ancestor iterations cadences' do
expect(subject).to contain_exactly(
active_group_iterations_cadence,
inactive_group_iterations_cadence,
automatic_iterations_cadence,
active_sub_group_iterations_cadence,
inactive_sub_group_iterations_cadence,
non_automatic_sub_group_iterations_cadence
)
end
end
end
context 'with filters' do
let(:current_group) { sub_group }
let(:params) { { include_ancestor_groups: true } }
it 'filters by title' do
params[:title] = 'one week'
expect(subject).to contain_exactly(
active_group_iterations_cadence,
automatic_iterations_cadence,
active_sub_group_iterations_cadence,
non_automatic_sub_group_iterations_cadence
)
end
it 'filters by ID' do
params[:id] = active_sub_group_iterations_cadence.id
expect(subject).to contain_exactly(active_sub_group_iterations_cadence)
end
it 'filters by active true' do
params[:active] = 'true'
expect(subject).to contain_exactly(
active_group_iterations_cadence,
automatic_iterations_cadence,
active_sub_group_iterations_cadence,
non_automatic_sub_group_iterations_cadence
)
end
it 'filters by active false' do
params[:active] = 'false'
expect(subject).to contain_exactly(
inactive_group_iterations_cadence,
inactive_sub_group_iterations_cadence
)
end
it 'filters by automatic true' do
params[:automatic] = true
expect(subject).to contain_exactly(
active_group_iterations_cadence,
inactive_group_iterations_cadence,
automatic_iterations_cadence,
active_sub_group_iterations_cadence,
inactive_sub_group_iterations_cadence
)
end
it 'filters by automatic false' do
params[:automatic] = false
expect(subject).to contain_exactly(
non_automatic_sub_group_iterations_cadence
)
end
it 'filters by duration_in_weeks false' do
params[:duration_in_weeks] = 2
expect(subject).to contain_exactly(
inactive_group_iterations_cadence,
inactive_sub_group_iterations_cadence
)
end
end
end
end
...@@ -12,6 +12,7 @@ RSpec.describe GitlabSchema.types['Group'] do ...@@ -12,6 +12,7 @@ RSpec.describe GitlabSchema.types['Group'] do
end end
it { expect(described_class).to have_graphql_field(:iterations) } it { expect(described_class).to have_graphql_field(:iterations) }
it { expect(described_class).to have_graphql_field(:iteration_cadences) }
it { expect(described_class).to have_graphql_field(:groupTimelogsEnabled) } it { expect(described_class).to have_graphql_field(:groupTimelogsEnabled) }
it { expect(described_class).to have_graphql_field(:timelogs, complexity: 5) } it { expect(described_class).to have_graphql_field(:timelogs, complexity: 5) }
it { expect(described_class).to have_graphql_field(:vulnerabilities) } it { expect(described_class).to have_graphql_field(:vulnerabilities) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::Iterations::CadencesResolver do
include GraphqlHelpers
describe '#resolve' do
let_it_be(:current_user) { create(:user) }
let_it_be(:group) { create(:group, :private) }
let_it_be(:project) { create(:project, :private, group: group) }
let_it_be(:active_group_iteration_cadence) { create(:iterations_cadence, group: group, active: true, duration_in_weeks: 1, title: 'one week iterations') }
shared_examples 'fetches iteration cadences' do
context 'when user does not have permissions to read iterations cadences' do
it 'raises error' do
expect do
resolve_group_iteration_cadences
end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when user has permissions to read iterations cadences' do
before do
parent.add_developer(current_user)
end
it 'returns iterations cadences from group' do
expect(resolve_group_iteration_cadences).to contain_exactly(active_group_iteration_cadence)
end
context 'when iteration cadences feature is disabled' do
before do
stub_feature_flags(iteration_cadences: false)
end
it 'returns no results' do
expect(resolve_group_iteration_cadences).to be_empty
end
end
end
end
context 'iterations cadences for project' do
let(:parent) { project }
it_behaves_like 'fetches iteration cadences'
context 'when project does not have a parent group' do
let_it_be(:project) { create(:project, :private) }
it 'raises error' do
project.add_developer(current_user)
expect do
resolve_group_iteration_cadences({}, project, { current_user: current_user })
end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
context 'iterations cadences for group' do
let(:parent) { group }
it_behaves_like 'fetches iteration cadences'
end
end
def resolve_group_iteration_cadences(args = {}, obj = parent, context = { current_user: current_user })
resolve(described_class, obj: obj, args: args, ctx: context)
end
end
...@@ -19,7 +19,7 @@ RSpec.describe GitlabSchema.types['Project'] do ...@@ -19,7 +19,7 @@ RSpec.describe GitlabSchema.types['Project'] do
expected_fields = %w[ expected_fields = %w[
vulnerabilities vulnerability_scanners requirement_states_count vulnerabilities vulnerability_scanners requirement_states_count
vulnerability_severities_count packages compliance_frameworks vulnerabilities_count_by_day vulnerability_severities_count packages compliance_frameworks vulnerabilities_count_by_day
security_dashboard_path iterations cluster_agents repository_size_excess actual_repository_size_limit security_dashboard_path iterations iteration_cadences cluster_agents repository_size_excess actual_repository_size_limit
code_coverage_summary api_fuzzing_ci_configuration code_coverage_summary api_fuzzing_ci_configuration
] ]
......
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