Commit 8df4fa8e authored by Etienne Baqué's avatar Etienne Baqué

Merge branch '336611-search-iterations-by-cadence-title' into 'master'

Add ability to search iterations by cadence title

See merge request gitlab-org/gitlab!76231
parents 481527e8 75e3079e
......@@ -125,17 +125,6 @@ module Timebox
fuzzy_search(query, [:title, :description])
end
# Searches for timeboxes with a matching title.
#
# This method uses ILIKE on PostgreSQL
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search_title(query)
fuzzy_search(query, [:title])
end
def filter_by_state(timeboxes, state)
case state
when 'closed' then timeboxes.closed
......
......@@ -52,6 +52,17 @@ class Milestone < ApplicationRecord
state :active
end
# Searches for timeboxes with a matching title.
#
# This method uses ILIKE on PostgreSQL
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def self.search_title(query)
fuzzy_search(query, [:title])
end
def self.min_chars_for_partial_matching
2
end
......
......@@ -11064,12 +11064,15 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="groupiterationsenddate"></a>`endDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.end. |
| <a id="groupiterationsid"></a>`id` | [`ID`](#id) | Global ID of the Iteration to look up. |
| <a id="groupiterationsiid"></a>`iid` | [`ID`](#id) | Internal ID of the Iteration to look up. |
| <a id="groupiterationsin"></a>`in` | [`[IterationSearchableField!]`](#iterationsearchablefield) | Fields in which the fuzzy-search should be performed with the query given in the argument `search`. Defaults to `[title]`. |
| <a id="groupiterationsincludeancestors"></a>`includeAncestors` | [`Boolean`](#boolean) | Whether to include ancestor iterations. Defaults to true. |
| <a id="groupiterationsiterationcadenceids"></a>`iterationCadenceIds` | [`[IterationsCadenceID!]`](#iterationscadenceid) | Global iteration cadence IDs by which to look up the iterations. |
| <a id="groupiterationssearch"></a>`search` | [`String`](#string) | Query used for fuzzy-searching in the fields selected in the argument `in`. |
| <a id="groupiterationssort"></a>`sort` | [`IterationSort`](#iterationsort) | List iterations by sort order. If unspecified, an arbitrary order (subject to change) is used. |
| <a id="groupiterationsstartdate"></a>`startDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.start. |
| <a id="groupiterationsstate"></a>`state` | [`IterationState`](#iterationstate) | Filter iterations by state. |
| <a id="groupiterationstimeframe"></a>`timeframe` | [`Timeframe`](#timeframe) | List items overlapping the given timeframe. |
| <a id="groupiterationstitle"></a>`title` | [`String`](#string) | Fuzzy search by title. |
| <a id="groupiterationstitle"></a>`title` **{warning-solid}** | [`String`](#string) | **Deprecated** in 15.4. The argument will be removed in 15.4. Please use `search` and `in` fields instead. |
##### `Group.label`
......@@ -13762,12 +13765,15 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="projectiterationsenddate"></a>`endDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.end. |
| <a id="projectiterationsid"></a>`id` | [`ID`](#id) | Global ID of the Iteration to look up. |
| <a id="projectiterationsiid"></a>`iid` | [`ID`](#id) | Internal ID of the Iteration to look up. |
| <a id="projectiterationsin"></a>`in` | [`[IterationSearchableField!]`](#iterationsearchablefield) | Fields in which the fuzzy-search should be performed with the query given in the argument `search`. Defaults to `[title]`. |
| <a id="projectiterationsincludeancestors"></a>`includeAncestors` | [`Boolean`](#boolean) | Whether to include ancestor iterations. Defaults to true. |
| <a id="projectiterationsiterationcadenceids"></a>`iterationCadenceIds` | [`[IterationsCadenceID!]`](#iterationscadenceid) | Global iteration cadence IDs by which to look up the iterations. |
| <a id="projectiterationssearch"></a>`search` | [`String`](#string) | Query used for fuzzy-searching in the fields selected in the argument `in`. |
| <a id="projectiterationssort"></a>`sort` | [`IterationSort`](#iterationsort) | List iterations by sort order. If unspecified, an arbitrary order (subject to change) is used. |
| <a id="projectiterationsstartdate"></a>`startDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.start. |
| <a id="projectiterationsstate"></a>`state` | [`IterationState`](#iterationstate) | Filter iterations by state. |
| <a id="projectiterationstimeframe"></a>`timeframe` | [`Timeframe`](#timeframe) | List items overlapping the given timeframe. |
| <a id="projectiterationstitle"></a>`title` | [`String`](#string) | Fuzzy search by title. |
| <a id="projectiterationstitle"></a>`title` **{warning-solid}** | [`String`](#string) | **Deprecated** in 15.4. The argument will be removed in 15.4. Please use `search` and `in` fields instead. |
##### `Project.jobs`
......@@ -17000,6 +17006,23 @@ Issue type.
| <a id="issuetyperequirement"></a>`REQUIREMENT` | Requirement issue type. |
| <a id="issuetypetest_case"></a>`TEST_CASE` | Test Case issue type. |
### `IterationSearchableField`
Fields to perform the search in.
| Value | Description |
| ----- | ----------- |
| <a id="iterationsearchablefieldcadence_title"></a>`CADENCE_TITLE` | Search in cadence_title field. |
| <a id="iterationsearchablefieldtitle"></a>`TITLE` | Search in title field. |
### `IterationSort`
Iteration sort values.
| Value | Description |
| ----- | ----------- |
| <a id="iterationsortcadence_and_due_date_asc"></a>`CADENCE_AND_DUE_DATE_ASC` | Sort by cadence id and due date in ascending order. |
### `IterationState`
State of a GitLab iteration.
......@@ -5,14 +5,18 @@
# params - Hash
# parent - The group in which to look-up iterations.
# include_ancestors - whether to look-up iterations in group ancestors.
# order - Orders by field default due date asc.
# title - Filter by title.
# search - Filter by fuzzy searching the given query in the selected fields.
# in - Array of searchable fields used with search param.
# state - Filters by state.
# sort - Items are sorted by due_date and title with id as a tie breaker if unspecified.
class IterationsFinder
include FinderMethods
include TimeFrameFilter
SEARCHABLE_FIELDS = %i(title cadence_title).freeze
attr_reader :params, :current_user
def initialize(current_user, params = {})
......@@ -30,7 +34,7 @@ class IterationsFinder
items = by_iid(items)
items = by_groups(items)
items = by_title(items)
items = by_search_title(items)
items = by_search(items)
items = by_state(items)
items = by_timeframe(items)
items = by_iteration_cadences(items)
......@@ -72,10 +76,20 @@ class IterationsFinder
items.with_title(params[:title])
end
def by_search_title(items)
return items unless params[:search_title].present?
def by_search(items)
return items unless params[:search].present? && params[:in].present?
query = params[:search]
in_title = params[:in].include?(:title)
in_cadence_title = params[:in].include?(:cadence_title)
items.search_title(params[:search_title])
if in_title && in_cadence_title
items.search_title_or_cadence_title(query)
elsif in_title
items.search_title(query)
elsif in_cadence_title
items.search_cadence_title(query)
end
end
def by_state(items)
......@@ -96,6 +110,8 @@ class IterationsFinder
# rubocop: disable CodeReuse/ActiveRecord
def order(items)
return items.sort_by_cadence_id_and_due_date_asc if params[:sort].present? && params[:sort] == :cadence_and_due_date_asc
items.reorder(:due_date).order(:title, { id: :asc })
end
# rubocop: enable CodeReuse/ActiveRecord
......
......@@ -5,12 +5,23 @@ module Resolvers
include Gitlab::Graphql::Authorize::AuthorizeResource
include TimeFrameArguments
DEFAULT_IN_FIELD = :title
argument :state, Types::IterationStateEnum,
required: false,
description: 'Filter iterations by state.'
argument :title, GraphQL::Types::String,
required: false,
description: 'Fuzzy search by title.'
description: 'Fuzzy search by title.',
deprecated: { reason: 'The argument will be removed in 15.4. Please use `search` and `in` fields instead', milestone: '15.4' }
argument :search, GraphQL::Types::String,
required: false,
description: 'Query used for fuzzy-searching in the fields selected in the argument `in`.'
argument :in, [Types::IterationSearchableFieldEnum],
required: false,
description: "Fields in which the fuzzy-search should be performed with the query given in the argument `search`. Defaults to `[#{DEFAULT_IN_FIELD}]`."
# rubocop:disable Graphql/IDType
argument :id, GraphQL::Types::ID,
......@@ -29,10 +40,15 @@ module Resolvers
required: false,
description: 'Global iteration cadence IDs by which to look up the iterations.'
argument :sort, Types::IterationSortEnum,
required: false,
description: 'List iterations by sort order. If unspecified, an arbitrary order (subject to change) is used.'
type Types::IterationType.connection_type, null: true
def resolve(**args)
validate_timeframe_params!(args)
validate_search_params!(args)
authorize!
......@@ -40,6 +56,8 @@ module Resolvers
args[:iteration_cadence_ids] = parse_iteration_cadence_ids(args[:iteration_cadence_ids])
args[:include_ancestors] = true if args[:include_ancestors].nil? && args[:iid].nil?
handle_search_params!(args)
iterations = IterationsFinder.new(context[:current_user], iterations_finder_params(args)).execute
# Necessary for scopedPath computation in IterationPresenter
......@@ -50,6 +68,23 @@ module Resolvers
private
def validate_search_params!(args)
if args[:title].present? && (args[:search].present? || args[:in].present?)
raise Gitlab::Graphql::Errors::ArgumentError, "'title' is deprecated in favor of 'search'. Please use 'search'."
end
if !args[:search].present? && args[:in].present?
raise Gitlab::Graphql::Errors::ArgumentError, "'search' must be specified when using 'in' argument."
end
end
def handle_search_params!(args)
return unless args[:search] || args[:title]
args[:in] = [DEFAULT_IN_FIELD] if args[:in].nil? || args[:in].empty?
args[:search] = args[:title] if args[:title]
end
def iterations_finder_params(args)
{
parent: parent,
......@@ -58,7 +93,9 @@ module Resolvers
iid: args[:iid],
iteration_cadence_ids: args[:iteration_cadence_ids],
state: args[:state] || 'all',
search_title: args[:title]
search: args[:search],
in: args[:in],
sort: args[:sort]
}.merge(transform_timeframe_parameters(args))
end
......
# frozen_string_literal: true
module Types
class IterationSearchableFieldEnum < BaseEnum
graphql_name 'IterationSearchableField'
description 'Fields to perform the search in'
IterationsFinder::SEARCHABLE_FIELDS.each do |field|
value field.to_s.upcase, value: field, description: "Search in #{field} field."
end
end
end
# frozen_string_literal: true
module Types
class IterationSortEnum < BaseEnum
graphql_name 'IterationSort'
description 'Iteration sort values'
value 'CADENCE_AND_DUE_DATE_ASC', 'Sort by cadence id and due date in ascending order.', value: :cadence_and_due_date_asc
end
end
......@@ -60,6 +60,7 @@ module EE
after_commit :reset, on: [:update, :create], if: :saved_change_to_start_or_due_date?
scope :due_date_order_asc, -> { order(:due_date) }
scope :sort_by_cadence_id_and_due_date_asc, -> { reorder(iterations_cadence_id: :asc).due_date_order_asc.order(id: :asc) }
scope :upcoming, -> { with_state(:upcoming) }
scope :current, -> { with_state(:current) }
scope :closed, -> { with_state(:closed) }
......@@ -147,6 +148,28 @@ module EE
else raise ArgumentError, "Unknown state filter: #{state}"
end
end
def search_title(query)
fuzzy_search(query, [::Resolvers::IterationsResolver::DEFAULT_IN_FIELD], use_minimum_char_limit: contains_digits?(query))
end
def search_cadence_title(query)
cadence_ids = Iterations::Cadence.search_title(query).pluck(:id)
where(iterations_cadence_id: cadence_ids)
end
def search_title_or_cadence_title(query)
union_sql = ::Gitlab::SQL::Union.new([search_title(query), search_cadence_title(query)]).to_sql
::Iteration.from("(#{union_sql}) #{table_name}")
end
private
def contains_digits?(query)
!(query =~ / \d+ /).nil?
end
end
def display_text
......
......@@ -36,8 +36,16 @@ module Iterations
.where("DATE ((COALESCE(iterations_cadences.last_run_date, DATE('01-01-1970')) + iterations_cadences.duration_in_weeks * INTERVAL '1 week')) <= CURRENT_DATE")
end
def self.search_title(query)
fuzzy_search(query, [:title])
class << self
def search_title(query)
fuzzy_search(query, [::Resolvers::IterationsResolver::DEFAULT_IN_FIELD], use_minimum_char_limit: contains_digit?(query))
end
private
def contains_digit?(query)
!(query =~ / \d+ /).nil?
end
end
def next_open_iteration(date)
......
......@@ -23,11 +23,23 @@ module API
end
def iterations_finder_params(parent)
{
finder_params = {
parent: parent,
include_ancestors: params[:include_ancestors],
state: params[:state],
search_title: params[:search]
search: nil,
in: nil
}
finder_params.merge!(search_params) if params[:search]
finder_params
end
def search_params
{
search: params[:search],
in: [::Resolvers::IterationsResolver::DEFAULT_IN_FIELD]
}
end
end
......
......@@ -10,11 +10,11 @@ RSpec.describe IterationsFinder do
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(:iteration_cadence3) { create(:iterations_cadence, group: root, active: true, duration_in_weeks: 3, title: 'three week iterations') }
let_it_be(:closed_iteration) { create(:closed_iteration, :skip_future_date_validation, iterations_cadence: iteration_cadence2, group: iteration_cadence2.group, start_date: 7.days.ago, due_date: 2.days.ago) }
let_it_be(:started_group_iteration) { create(:current_iteration, :skip_future_date_validation, iterations_cadence: iteration_cadence2, group: iteration_cadence2.group, title: 'one test', start_date: 1.day.ago, due_date: Date.today) }
let_it_be(:upcoming_group_iteration) { create(:iteration, iterations_cadence: iteration_cadence1, group: iteration_cadence1.group, start_date: 1.day.from_now, due_date: 3.days.from_now) }
let_it_be(:root_group_iteration) { create(:current_iteration, iterations_cadence: iteration_cadence3, group: iteration_cadence3.group, start_date: 1.day.ago, due_date: 2.days.from_now) }
let_it_be(:root_closed_iteration) { create(:closed_iteration, iterations_cadence: iteration_cadence3, group: iteration_cadence3.group, start_date: 1.week.ago, due_date: 2.days.ago) }
let_it_be(:closed_iteration) { create(:closed_iteration, :skip_future_date_validation, iterations_cadence: iteration_cadence2, start_date: 7.days.ago, due_date: 2.days.ago) }
let_it_be(:started_group_iteration) { create(:current_iteration, :skip_future_date_validation, iterations_cadence: iteration_cadence2, title: 'one test', start_date: 1.day.ago, due_date: Date.today) }
let_it_be(:upcoming_group_iteration) { create(:iteration, iterations_cadence: iteration_cadence1, title: 'Iteration 1', start_date: 1.day.from_now, due_date: 3.days.from_now) }
let_it_be(:root_group_iteration) { create(:current_iteration, iterations_cadence: iteration_cadence3, start_date: 1.day.ago, due_date: 2.days.from_now) }
let_it_be(:root_closed_iteration) { create(:closed_iteration, iterations_cadence: iteration_cadence3, start_date: 1.week.ago, due_date: 2.days.ago) }
let(:parent) { project_1 }
let(:params) { { parent: parent, include_ancestors: true } }
......@@ -132,10 +132,41 @@ RSpec.describe IterationsFinder do
expect(subject.to_a).to contain_exactly(started_group_iteration)
end
it 'filters by search_title' do
params[:search_title] = 'one t'
context "with search params" do
using RSpec::Parameterized::TableSyntax
expect(subject.to_a).to contain_exactly(started_group_iteration)
shared_examples "search returns correct items" do
before do
params.merge!({ search: search, in: fields_to_search })
end
it { is_expected.to contain_exactly(*expected_iterations) }
end
context 'filters by title' do
let(:all_iterations) { [closed_iteration, started_group_iteration, upcoming_group_iteration, root_group_iteration, root_closed_iteration] }
where(:search, :fields_to_search, :expected_iterations) do
'' | [] | lazy { all_iterations }
'iteration' | [] | lazy { all_iterations }
'iteration' | [:title] | lazy { [upcoming_group_iteration] }
'iteration' | [:title] | lazy { [upcoming_group_iteration] }
'iter 1' | [:title] | lazy { [upcoming_group_iteration] }
'iteration 1' | [:title] | lazy { [upcoming_group_iteration] }
'iteration test' | [:title] | lazy { [] }
'one week iter' | [:cadence_title] | lazy { [upcoming_group_iteration] }
'iteration' | [:cadence_title] | lazy { all_iterations }
'two week' | [:cadence_title] | lazy { [closed_iteration, started_group_iteration] }
'iteration test' | [:cadence_title] | lazy { [] }
'one week' | [:title, :cadence_title] | lazy { [upcoming_group_iteration] }
'iteration' | [:title, :cadence_title] | lazy { all_iterations }
'iteration 1' | [:title, :cadence_title] | lazy { [upcoming_group_iteration] }
end
with_them do
it_behaves_like "search returns correct items"
end
end
end
it 'filters by ID' do
......@@ -205,6 +236,22 @@ RSpec.describe IterationsFinder do
end
end
end
context 'sorting' do
it 'sorts by the default order (due_date, title, id asc) when no param is given' do
expect(subject).to eq([closed_iteration, root_closed_iteration, started_group_iteration, root_group_iteration, upcoming_group_iteration])
end
it 'sorts correctly when supported sorting param provided' do
params[:sort] = :cadence_and_due_date_asc
cadence1_iterations = [upcoming_group_iteration]
cadence2_iterations = [closed_iteration, started_group_iteration]
cadence3_iterations = [root_closed_iteration, root_group_iteration]
expect(subject).to eq([*cadence1_iterations, *cadence2_iterations, *cadence3_iterations])
end
end
end
describe '#find_by' do
......
......@@ -15,7 +15,9 @@ RSpec.describe Resolvers::IterationsResolver do
iteration_cadence_ids: nil,
parent: nil,
state: nil,
search_title: nil
search: nil,
in: nil,
sort: nil
}
end
......@@ -50,6 +52,60 @@ RSpec.describe Resolvers::IterationsResolver do
end
context 'with parameters' do
context 'search' do
using RSpec::Parameterized::TableSyntax
let_it_be(:plan_cadence) { create(:iterations_cadence, title: 'plan cadence', group: group) }
let_it_be(:product_cadence) { create(:iterations_cadence, title: 'product management', group: group) }
let_it_be(:plan_iteration1) { create(:iteration, :with_due_date, title: "Iteration 1", iterations_cadence: plan_cadence, start_date: 1.week.ago)}
let_it_be(:plan_iteration2) { create(:iteration, :with_due_date, title: "My iteration", iterations_cadence: plan_cadence, start_date: 2.weeks.ago)}
let_it_be(:product_iteration) { create(:iteration, :with_due_date, iterations_cadence: product_cadence, start_date: 1.week.from_now)}
let(:all_iterations) { group.iterations }
context 'with search and in parameters' do
where(:search, :fields_to_search, :expected_iterations) do
'' | [] | lazy { all_iterations }
'iteration' | nil | lazy { plan_cadence.iterations }
'iteration' | [] | lazy { plan_cadence.iterations }
'iteration' | [:title] | lazy { plan_cadence.iterations }
'iteration' | [:title, :cadence_title] | lazy { plan_cadence.iterations }
'plan' | [] | lazy { [] }
'plan' | [:cadence_title] | lazy { plan_cadence.iterations }
end
with_them do
it "returns correct items" do
expect(resolve_group_iterations({ search: search, in: fields_to_search }).items).to contain_exactly(*expected_iterations)
end
end
end
context "with the deprecated argument 'title' (to be deprecated in 15.4)" do
[
{ search: "foo" },
{ in: [:title] },
{ in: [:cadence_title] }
].each do |params|
it "raises an error when 'title' is used with #{params}" do
expect do
resolve_group_iterations({ title: "foo", **params })
end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, "'title' is deprecated in favor of 'search'. Please use 'search'.")
end
end
it "raises an error when 'in' is specified but 'search' is not" do
expect do
resolve_group_iterations({ in: [:title] })
end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, "'search' must be specified when using 'in' argument.")
end
it "uses 'search' and 'in' arguments to search title" do
expect(resolve_group_iterations({ title: 'iteration' }).items).to contain_exactly(*plan_cadence.iterations)
end
end
end
it 'calls IterationsFinder with correct parameters, using timeframe' do
start_date = now
end_date = start_date + 1.hour
......@@ -58,11 +114,11 @@ RSpec.describe Resolvers::IterationsResolver do
iid = 2
iteration_cadence_ids = ['5']
params = params_list.merge(id: id, iid: iid, iteration_cadence_ids: iteration_cadence_ids, parent: group, include_ancestors: nil, state: 'closed', start_date: start_date, end_date: end_date, search_title: search)
params = params_list.merge(id: id, iid: iid, iteration_cadence_ids: iteration_cadence_ids, parent: group, include_ancestors: nil, state: 'closed', start_date: start_date, end_date: end_date, search: search, in: [:title])
expect(IterationsFinder).to receive(:new).with(current_user, params).and_call_original
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)
resolve_group_iterations(timeframe: { start: start_date, end: end_date }, state: 'closed', search: 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
......@@ -73,11 +129,11 @@ RSpec.describe Resolvers::IterationsResolver do
iid = 2
iteration_cadence_ids = ['5']
params = params_list.merge(id: id, iid: iid, iteration_cadence_ids: iteration_cadence_ids, parent: group, include_ancestors: nil, state: 'closed', start_date: start_date, end_date: end_date, search_title: search)
params = params_list.merge(id: id, iid: iid, iteration_cadence_ids: iteration_cadence_ids, parent: group, include_ancestors: nil, state: 'closed', start_date: start_date, end_date: end_date, search: search, in: [:title])
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)
resolve_group_iterations(start_date: start_date, end_date: end_date, state: 'closed', search: search, id: 'gid://gitlab/Iteration/1', iteration_cadence_ids: ['gid://gitlab/Iterations::Cadence/5'], iid: iid)
end
it 'accepts a raw model id for backward compatibility' do
......
......@@ -538,6 +538,82 @@ RSpec.describe Iteration do
end
end
context 'search and sorting scopes' do
let_it_be(:group1) { create(:group) }
let_it_be(:group2) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group1) }
let_it_be(:plan_cadence) { create(:iterations_cadence, title: 'plan cadence', group: group1) }
let_it_be(:product_cadence) { create(:iterations_cadence, title: 'product management', group: subgroup) }
let_it_be(:cadence) { create(:iterations_cadence, title: 'cadence', group: group2) }
let_it_be(:plan_iteration1) { create(:iteration, :with_due_date, title: "Iteration 1", iterations_cadence: plan_cadence, start_date: 1.week.ago)}
let_it_be(:plan_iteration2) { create(:iteration, :with_due_date, title: "My iteration", iterations_cadence: plan_cadence, start_date: 2.weeks.ago)}
let_it_be(:product_iteration) { create(:iteration, :with_due_date, title: "Iteration 2", iterations_cadence: product_cadence, start_date: 1.week.from_now)}
let_it_be(:cadence_iteration) { create(:iteration, :with_due_date, iterations_cadence: cadence, start_date: Date.today)}
shared_examples "search returns correct records" do
it { is_expected.to contain_exactly(*expected_iterations) }
end
describe '.search_title' do
where(:query, :expected_iterations) do
'iter 1' | lazy { [plan_iteration1] }
'iteration' | lazy { [plan_iteration1, plan_iteration2, product_iteration] }
'iteration 1' | lazy { [plan_iteration1] }
'my iteration 1' | lazy { [] }
end
with_them do
subject { described_class.search_title(query) }
it_behaves_like "search returns correct records"
end
end
describe '.search_cadence_title' do
where(:query, :expected_iterations) do
'plan' | lazy { [plan_iteration1, plan_iteration2] }
'plan cadence' | lazy { [plan_iteration1, plan_iteration2] }
'product cadence' | lazy { [] }
'cadence' | lazy { [plan_iteration1, plan_iteration2, cadence_iteration] }
end
with_them do
subject { described_class.search_cadence_title(query) }
it_behaves_like "search returns correct records"
end
end
describe '.search_title_or_cadence_title' do
where(:query, :expected_iterations) do
# The same test cases used for .search_title
'iter 1' | lazy { [plan_iteration1] }
'iteration' | lazy { [plan_iteration1, plan_iteration2, product_iteration] }
'iteration 1' | lazy { [plan_iteration1] }
'my iteration 1' | lazy { [] }
# The same test cases used for .search_cadence_title
'plan' | lazy { [plan_iteration1, plan_iteration2] }
'plan cadence' | lazy { [plan_iteration1, plan_iteration2] }
'product cadence' | lazy { [] }
'cadence' | lazy { [plan_iteration1, plan_iteration2, cadence_iteration] }
# At least one of cadence title or iteration title should contain all of the terms
'plan iteration' | lazy { [] }
end
with_them do
subject { described_class.search_title_or_cadence_title(query) }
it_behaves_like "search returns correct records"
end
end
describe '.sort_by_cadence_id_and_due_date_asc' do
subject { described_class.all.sort_by_cadence_id_and_due_date_asc }
it { is_expected.to eq([plan_iteration2, plan_iteration1, product_iteration, cadence_iteration]) }
end
end
context 'time scopes' do
let_it_be(:group) { create(:group) }
let_it_be(:iteration_cadence) { create(:iterations_cadence, group: group) }
......
......@@ -11,9 +11,9 @@ RSpec.describe 'getting iterations' do
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(:current_group_iteration) { create(:iteration, :skip_future_date_validation, iterations_cadence: iteration_cadence1, group: iteration_cadence1.group, title: 'one test', start_date: 1.day.ago, due_date: 1.week.from_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(:iteration, :skip_project_validation, iterations_cadence: iteration_cadence1, group: iteration_cadence1.group, start_date: 3.weeks.ago, due_date: 1.week.ago) }
let_it_be(:current_group_iteration) { create(:iteration, :skip_future_date_validation, iterations_cadence: iteration_cadence1, title: 'one test', start_date: 1.day.ago, due_date: 1.week.from_now) }
let_it_be(:upcoming_group_iteration) { create(:iteration, iterations_cadence: iteration_cadence2, start_date: 1.day.from_now, due_date: 2.days.from_now) }
let_it_be(:closed_group_iteration) { create(:iteration, :skip_project_validation, iterations_cadence: iteration_cadence1, start_date: 3.weeks.ago, due_date: 1.week.ago) }
before do
group.add_maintainer(user)
......@@ -47,6 +47,28 @@ RSpec.describe 'getting iterations' do
describe 'query for iterations by cadence' do
context 'with multiple cadences' do
context 'searching by cadence title or iteration title and sorting by cadence and due date ASC' do
using RSpec::Parameterized::TableSyntax
let_it_be(:past_iteration1) { create(:iteration, :with_due_date, iterations_cadence: iteration_cadence2, start_date: 4.weeks.ago) }
let_it_be(:past_iteration2) { create(:iteration, :with_due_date, iterations_cadence: iteration_cadence2, start_date: 2.weeks.ago) }
where(:search, :ordered_expected_iterations) do
'two' | lazy { [past_iteration1, past_iteration2, upcoming_group_iteration] }
'iteration' | lazy { [closed_group_iteration, current_group_iteration, past_iteration1, past_iteration2, upcoming_group_iteration] }
end
with_them do
let(:field_queries) { "search: \"#{search}\", in: [TITLE, CADENCE_TITLE], sort: CADENCE_AND_DUE_DATE_ASC" }
it 'correctly returns ordered items' do
post_graphql(iterations_query(group, field_queries), current_user: user)
expect(actual_iterations).to eq(expected_iterations(ordered_expected_iterations))
end
end
end
it 'returns iterations' do
post_graphql(iteration_cadence_query(group, [iteration_cadence1.to_global_id, iteration_cadence2.to_global_id]), current_user: user)
......@@ -103,11 +125,16 @@ RSpec.describe 'getting iterations' do
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 }
def actual_iterations
graphql_data['group']['iterations']['nodes'].map { |iteration| iteration['id'] }
end
expect(actual_iterations).to contain_exactly(*expected_iterations)
def expected_iterations(iterations)
iterations.map { |iteration| iteration.to_global_id.to_s }
end
def expect_iterations_response(*iterations)
expect(actual_iterations).to contain_exactly(*expected_iterations(iterations))
expect(graphql_errors).to be_nil
end
end
......@@ -12,8 +12,8 @@ RSpec.describe IterationsUpdateStatusWorker do
describe '#perform' do
before do
current_iteration1.update_column(:state_enum, 2)
closed_iteration1.update_column(:state_enum, 1)
current_iteration1.update_column(:state_enum, Iteration::STATE_ENUM_MAP[:current])
closed_iteration1.update_column(:state_enum, Iteration::STATE_ENUM_MAP[:upcoming])
end
it 'schedules an issues roll-over job' 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