Commit c21eb117 authored by Rémy Coutable's avatar Rémy Coutable

Ensure Insights charts show all periods even if there are no data

Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parent 7ee640ff
......@@ -36,25 +36,31 @@ module InsightsActions
end
def insights_json
issuables = find_issuables(params[:query])
insights = reduce(issuables, params[:chart_type], params[:query])
issuables_finder = finder(params[:query])
issuables = issuables_finder.find
insights = reduce(
issuables: issuables,
chart_type: params[:chart_type],
period: params[:query][:group_by],
period_limit: issuables_finder.period_limit,
labels: params[:query][:collection_labels])
serializer(params[:chart_type]).present(insights)
end
def find_issuables(query)
Gitlab::Insights::Finders::IssuableFinder
.new(insights_entity, current_user, query).find
end
def reduce(issuables, chart_type, query)
def reduce(issuables:, chart_type:, period:, period_limit:, labels: nil)
case chart_type
when 'stacked-bar', 'line'
Gitlab::Insights::Reducers::LabelCountPerPeriodReducer.reduce(issuables, period: query[:group_by], labels: query[:collection_labels])
Gitlab::Insights::Reducers::LabelCountPerPeriodReducer.reduce(issuables, period: period, period_limit: period_limit, labels: labels)
when 'bar'
Gitlab::Insights::Reducers::CountPerPeriodReducer.reduce(issuables, period: query[:group_by])
Gitlab::Insights::Reducers::CountPerPeriodReducer.reduce(issuables, period: period, period_limit: period_limit)
end
end
def finder(query)
Gitlab::Insights::Finders::IssuableFinder
.new(insights_entity, current_user, query)
end
def serializer(chart_type)
case chart_type
when 'stacked-bar'
......
---
title: Ensure Insights charts show all periods even if there are no data
merge_request: 10733
author:
type: fixed
......@@ -36,6 +36,19 @@ module Gitlab
relation
end
def period_limit
@period_limit ||=
if opts.key?(:period_limit)
begin
Integer(opts[:period_limit])
rescue ArgumentError
raise InvalidPeriodLimitError, "Invalid `:period_limit` option: `#{opts[:period_limit]}`. Expected an integer!"
end
else
PERIODS.dig(period, :default)
end
end
private
attr_reader :entity, :current_user, :opts
......@@ -86,19 +99,6 @@ module Gitlab
period
end
end
def period_limit
@period_limit ||=
if opts.key?(:period_limit)
begin
Integer(opts[:period_limit])
rescue ArgumentError
raise InvalidPeriodLimitError, "Invalid `:period_limit` option: `#{opts[:period_limit]}`. Expected an integer!"
end
else
PERIODS.dig(period, :default)
end
end
end
end
end
......
......@@ -6,13 +6,15 @@ module Gitlab
class CountPerPeriodReducer < BaseReducer
InvalidPeriodError = Class.new(BaseReducerError)
InvalidPeriodFieldError = Class.new(BaseReducerError)
InvalidPeriodLimitError = Class.new(BaseReducerError)
VALID_PERIOD = %w[day week month].freeze
VALID_PERIOD_FIELD = %i[created_at].freeze
def initialize(issuables, period:, period_field: :created_at)
def initialize(issuables, period:, period_limit:, period_field: :created_at)
super(issuables)
@period = period.to_s.singularize
@period_limit = period_limit.to_i
@period_field = period_field
validate!
......@@ -25,14 +27,15 @@ module Gitlab
# 'March 2019' => 1
# }
def reduce
issuables_grouped_by_normalized_period.each_with_object({}) do |(period, issuables), hash|
hash[period.strftime(period_format)] = value_for_period(issuables)
(0...period_limit).reverse_each.each_with_object({}) do |period_ago, hash|
period_time = normalized_time(period_ago.public_send(period).ago) # rubocop:disable GitlabSecurity/PublicSend
hash[period_time.strftime(period_format)] = value_for_period(issuables_grouped_by_normalized_period.fetch(period_time, []))
end
end
private
attr_reader :period, :period_field
attr_reader :period, :period_limit, :period_field
def validate!
unless VALID_PERIOD.include?(period)
......@@ -42,6 +45,10 @@ module Gitlab
unless VALID_PERIOD_FIELD.include?(period_field)
raise InvalidPeriodFieldError, "Invalid value for `period_field`: `#{period_field}`. Allowed values are #{VALID_PERIOD_FIELD}!"
end
unless period_limit > 0
raise InvalidPeriodLimitError, "Invalid value for `period_limit`: `#{period_limit}`. Value must be greater than 0!"
end
end
# Returns a hash { period => [array of issuables] }, e.g.
......@@ -51,11 +58,15 @@ module Gitlab
# #<Fri, 01 Mar 2019 00:00:00 UTC +00:00> => [#<Issue id:3 namespace1/project1#3>]
# }
def issuables_grouped_by_normalized_period
issuables.group_by do |issuable|
issuable.public_send(period_field).public_send(period_normalizer) # rubocop:disable GitlabSecurity/PublicSend
@issuables_grouped_by_normalized_period ||= issuables.group_by do |issuable|
normalized_time(issuable.public_send(period_field)) # rubocop:disable GitlabSecurity/PublicSend
end
end
def normalized_time(time)
time.public_send(period_normalizer) # rubocop:disable GitlabSecurity/PublicSend
end
def period_normalizer
:"beginning_of_#{period}"
end
......
......@@ -17,8 +17,8 @@ module Gitlab
# }
# }
class LabelCountPerPeriodReducer < CountPerPeriodReducer
def initialize(issuables, labels:, period:, period_field: :created_at)
super(issuables, period: period, period_field: period_field)
def initialize(issuables, labels:, period:, period_limit:, period_field: :created_at)
super(issuables, period: period, period_limit: period_limit, period_field: period_field)
@labels = labels
end
......
......@@ -7,188 +7,242 @@ RSpec.describe Gitlab::Insights::Finders::IssuableFinder do
Timecop.freeze(Time.utc(2019, 3, 5)) { example.run }
end
def find(entity, opts)
described_class.new(entity, nil, opts).find
let(:base_opts) do
{
state: 'opened',
group_by: 'months'
}
end
it 'raises an error for an invalid :issuable_type option' do
expect { find(nil, issuable_type: 'foo') }.to raise_error(described_class::InvalidIssuableTypeError, "Invalid `:issuable_type` option: `foo`. Allowed values are #{described_class::FINDERS.keys}!")
end
describe '#find' do
def find(entity, opts)
described_class.new(entity, nil, opts).find
end
it 'raises an error for an invalid entity object' do
expect { find(build(:user), issuable_type: 'issue') }.to raise_error(described_class::InvalidEntityError, 'Entity class `User` is not supported. Supported classes are Project and Namespace!')
end
it 'raises an error for an invalid :issuable_type option' do
expect { find(nil, issuable_type: 'foo') }.to raise_error(described_class::InvalidIssuableTypeError, "Invalid `:issuable_type` option: `foo`. Allowed values are #{described_class::FINDERS.keys}!")
end
it 'raises an error for an invalid :group_by option' do
expect { find(nil, issuable_type: 'issue', group_by: 'foo') }.to raise_error(described_class::InvalidGroupByError, "Invalid `:group_by` option: `foo`. Allowed values are #{described_class::PERIODS.keys}!")
end
it 'raises an error for an invalid entity object' do
expect { find(build(:user), issuable_type: 'issue') }.to raise_error(described_class::InvalidEntityError, 'Entity class `User` is not supported. Supported classes are Project and Namespace!')
end
it 'raises an error for an invalid :period_limit option' do
expect { find(build(:user), issuable_type: 'issue', group_by: 'months', period_limit: 'many') }.to raise_error(described_class::InvalidPeriodLimitError, "Invalid `:period_limit` option: `many`. Expected an integer!")
end
it 'raises an error for an invalid :group_by option' do
expect { find(nil, issuable_type: 'issue', group_by: 'foo') }.to raise_error(described_class::InvalidGroupByError, "Invalid `:group_by` option: `foo`. Allowed values are #{described_class::PERIODS.keys}!")
end
shared_examples_for "insights issuable finder" do
let(:label_bug) { create(label_type, label_entity_association_key => entity, name: 'Bug') }
let(:label_manage) { create(label_type, label_entity_association_key => entity, name: 'Manage') }
let(:label_plan) { create(label_type, label_entity_association_key => entity, name: 'Plan') }
let(:label_create) { create(label_type, label_entity_association_key => entity, name: 'Create') }
let(:label_quality) { create(label_type, label_entity_association_key => entity, name: 'Quality') }
let(:extra_issuable_attrs) { [{}, {}, {}, {}, {}, {}] }
let!(:issuable0) { create(:"labeled_#{issuable_type}", :opened, created_at: Time.utc(2018, 2, 1), project_association_key => project, **extra_issuable_attrs[0]) }
let!(:issuable1) { create(:"labeled_#{issuable_type}", :opened, created_at: Time.utc(2018, 2, 1), labels: [label_bug, label_manage], project_association_key => project, **extra_issuable_attrs[1]) }
let!(:issuable2) { create(:"labeled_#{issuable_type}", :opened, created_at: Time.utc(2019, 2, 6), labels: [label_bug, label_plan], project_association_key => project, **extra_issuable_attrs[2]) }
let!(:issuable3) { create(:"labeled_#{issuable_type}", :opened, created_at: Time.utc(2019, 2, 20), labels: [label_bug, label_create], project_association_key => project, **extra_issuable_attrs[3]) }
let!(:issuable4) { create(:"labeled_#{issuable_type}", :opened, created_at: Time.utc(2019, 3, 5), labels: [label_bug, label_quality], project_association_key => project, **extra_issuable_attrs[4]) }
let(:opts) do
{
state: 'opened',
issuable_type: issuable_type,
filter_labels: [label_bug.title],
collection_labels: [label_manage.title, label_plan.title, label_create.title],
group_by: 'months'
}
it 'raises an error for an invalid :period_limit option' do
expect { find(build(:user), issuable_type: 'issue', group_by: 'months', period_limit: 'many') }.to raise_error(described_class::InvalidPeriodLimitError, "Invalid `:period_limit` option: `many`. Expected an integer!")
end
subject { find(entity, opts) }
shared_examples_for "insights issuable finder" do
let(:label_bug) { create(label_type, label_entity_association_key => entity, name: 'Bug') }
let(:label_manage) { create(label_type, label_entity_association_key => entity, name: 'Manage') }
let(:label_plan) { create(label_type, label_entity_association_key => entity, name: 'Plan') }
let(:label_create) { create(label_type, label_entity_association_key => entity, name: 'Create') }
let(:label_quality) { create(label_type, label_entity_association_key => entity, name: 'Quality') }
let(:extra_issuable_attrs) { [{}, {}, {}, {}, {}, {}] }
let!(:issuable0) { create(:"labeled_#{issuable_type}", :opened, created_at: Time.utc(2018, 2, 1), project_association_key => project, **extra_issuable_attrs[0]) }
let!(:issuable1) { create(:"labeled_#{issuable_type}", :opened, created_at: Time.utc(2018, 2, 1), labels: [label_bug, label_manage], project_association_key => project, **extra_issuable_attrs[1]) }
let!(:issuable2) { create(:"labeled_#{issuable_type}", :opened, created_at: Time.utc(2019, 2, 6), labels: [label_bug, label_plan], project_association_key => project, **extra_issuable_attrs[2]) }
let!(:issuable3) { create(:"labeled_#{issuable_type}", :opened, created_at: Time.utc(2019, 2, 20), labels: [label_bug, label_create], project_association_key => project, **extra_issuable_attrs[3]) }
let!(:issuable4) { create(:"labeled_#{issuable_type}", :opened, created_at: Time.utc(2019, 3, 5), labels: [label_bug, label_quality], project_association_key => project, **extra_issuable_attrs[4]) }
let(:opts) do
base_opts.merge(
issuable_type: issuable_type,
filter_labels: [label_bug.title],
collection_labels: [label_manage.title, label_plan.title, label_create.title])
end
it 'avoids N + 1 queries' do
control_queries = ActiveRecord::QueryRecorder.new { subject.map { |issuable| issuable.labels.map(&:title) } }
create(:"labeled_#{issuable_type}", :opened, created_at: Time.utc(2019, 3, 5), labels: [label_bug], project_association_key => project, **extra_issuable_attrs[5])
subject { find(entity, opts) }
expect { find(entity, opts).map { |issuable| issuable.labels.map(&:title) } }.not_to exceed_query_limit(control_queries)
end
it 'avoids N + 1 queries' do
control_queries = ActiveRecord::QueryRecorder.new { subject.map { |issuable| issuable.labels.map(&:title) } }
create(:"labeled_#{issuable_type}", :opened, created_at: Time.utc(2019, 3, 5), labels: [label_bug], project_association_key => project, **extra_issuable_attrs[5])
context ':period_limit option' do
context 'with group_by: "day"' do
before do
opts.merge!(group_by: 'day')
expect { find(entity, opts).map { |issuable| issuable.labels.map(&:title) } }.not_to exceed_query_limit(control_queries)
end
context ':period_limit option' do
context 'with group_by: "day"' do
before do
opts.merge!(group_by: 'day')
end
it 'returns issuable created after 30 days ago' do
expect(subject.to_a).to eq([issuable2, issuable3, issuable4])
end
end
it 'returns issuable created after 30 days ago' do
expect(subject.to_a).to eq([issuable2, issuable3, issuable4])
context 'with group_by: "day", period_limit: 1' do
before do
opts.merge!(group_by: 'day', period_limit: 1)
end
it 'returns issuable created after one day ago' do
expect(subject.to_a).to eq([issuable4])
end
end
end
context 'with group_by: "day", period_limit: 1' do
before do
opts.merge!(group_by: 'day', period_limit: 1)
context 'with group_by: "week"' do
before do
opts.merge!(group_by: 'week')
end
it 'returns issuable created after 4 weeks ago' do
expect(subject.to_a).to eq([issuable2, issuable3, issuable4])
end
end
it 'returns issuable created after one day ago' do
expect(subject.to_a).to eq([issuable4])
context 'with group_by: "week", period_limit: 1' do
before do
opts.merge!(group_by: 'week', period_limit: 1)
end
it 'returns issuable created after one week ago' do
expect(subject.to_a).to eq([issuable4])
end
end
end
context 'with group_by: "week"' do
before do
opts.merge!(group_by: 'week')
context 'with group_by: "month"' do
before do
opts.merge!(group_by: 'month')
end
it 'returns issuable created after 12 months ago' do
expect(subject.to_a).to eq([issuable2, issuable3, issuable4])
end
end
it 'returns issuable created after 4 weeks ago' do
expect(subject.to_a).to eq([issuable2, issuable3, issuable4])
context 'with group_by: "month", period_limit: 1' do
before do
opts.merge!(group_by: 'month', period_limit: 1)
end
it 'returns issuable created after one month ago' do
expect(subject.to_a).to eq([issuable2, issuable3, issuable4])
end
end
end
end
context 'with group_by: "week", period_limit: 1' do
before do
opts.merge!(group_by: 'week', period_limit: 1)
end
shared_examples_for 'group tests' do
let(:entity) { create(:group) }
let(:label_type) { :group_label }
let(:label_entity_association_key) { :group }
it 'returns issuable created after one week ago' do
expect(subject.to_a).to eq([issuable4])
context 'issues' do
include_examples "insights issuable finder" do
let(:issuable_type) { 'issue' }
let(:project_association_key) { :project }
end
end
context 'with group_by: "month"' do
before do
opts.merge!(group_by: 'month')
context 'merge requests' do
include_examples "insights issuable finder" do
let(:issuable_type) { 'merge_request' }
let(:project_association_key) { :source_project }
let(:extra_issuable_attrs) do
[
{ source_branch: "add_images_and_changes" },
{ source_branch: "improve/awesome" },
{ source_branch: "feature_conflict" },
{ source_branch: "markdown" },
{ source_branch: "feature_one" },
{ source_branch: "merged-target" }
]
end
end
end
end
it 'returns issuable created after 12 months ago' do
expect(subject.to_a).to eq([issuable2, issuable3, issuable4])
end
context 'for a group' do
include_examples 'group tests' do
let(:project) { create(:project, :public, group: entity) }
end
end
context 'for a group with subgroups', :nested_groups do
include_examples 'group tests' do
let(:project) { create(:project, :public, group: create(:group, parent: entity)) }
end
end
context 'for a project' do
let(:project) { create(:project, :public) }
let(:entity) { project }
let(:label_type) { :label }
let(:label_entity_association_key) { :project }
context 'with group_by: "month", period_limit: 1' do
before do
opts.merge!(group_by: 'month', period_limit: 1)
context 'issues' do
include_examples "insights issuable finder" do
let(:issuable_type) { 'issue' }
let(:project_association_key) { :project }
end
end
it 'returns issuable created after one month ago' do
expect(subject.to_a).to eq([issuable2, issuable3, issuable4])
context 'merge requests' do
include_examples "insights issuable finder" do
let(:issuable_type) { 'merge_request' }
let(:project_association_key) { :source_project }
let(:extra_issuable_attrs) do
[
{ source_branch: "add_images_and_changes" },
{ source_branch: "improve/awesome" },
{ source_branch: "feature_conflict" },
{ source_branch: "markdown" },
{ source_branch: "feature_one" },
{ source_branch: "merged-target" }
]
end
end
end
end
end
shared_examples_for 'group tests' do
let(:entity) { create(:group) }
let(:label_type) { :group_label }
let(:label_entity_association_key) { :group }
describe '#period_limit' do
subject { described_class.new(create(:project, :public), nil, opts).period_limit }
describe 'default values' do
context 'with group_by: "day"' do
let(:opts) { base_opts.merge!(group_by: 'day') }
context 'issues' do
include_examples "insights issuable finder" do
let(:issuable_type) { 'issue' }
let(:project_association_key) { :project }
it 'returns 30' do
expect(subject).to eq(30)
end
end
end
context 'merge requests' do
include_examples "insights issuable finder" do
let(:issuable_type) { 'merge_request' }
let(:project_association_key) { :source_project }
let(:extra_issuable_attrs) do
[
{ source_branch: "add_images_and_changes" },
{ source_branch: "improve/awesome" },
{ source_branch: "feature_conflict" },
{ source_branch: "markdown" },
{ source_branch: "feature_one" },
{ source_branch: "merged-target" }
]
context 'with group_by: "week"' do
let(:opts) { base_opts.merge!(group_by: 'week') }
it 'returns 4' do
expect(subject).to eq(4)
end
end
end
end
context 'for a group' do
include_examples 'group tests' do
let(:project) { create(:project, :public, group: entity) }
end
end
context 'with group_by: "month"' do
let(:opts) { base_opts.merge!(group_by: 'month') }
context 'for a group with subgroups', :nested_groups do
include_examples 'group tests' do
let(:project) { create(:project, :public, group: create(:group, parent: entity)) }
it 'returns 12' do
expect(subject).to eq(12)
end
end
end
end
context 'for a project' do
let(:project) { create(:project, :public) }
let(:entity) { project }
let(:label_type) { :label }
let(:label_entity_association_key) { :project }
describe 'custom values' do
context 'with period_limit: 42' do
let(:opts) { base_opts.merge!(period_limit: 42) }
context 'issues' do
include_examples "insights issuable finder" do
let(:issuable_type) { 'issue' }
let(:project_association_key) { :project }
it 'returns 42' do
expect(subject).to eq(42)
end
end
end
context 'merge requests' do
include_examples "insights issuable finder" do
let(:issuable_type) { 'merge_request' }
let(:project_association_key) { :source_project }
let(:extra_issuable_attrs) do
[
{ source_branch: "add_images_and_changes" },
{ source_branch: "improve/awesome" },
{ source_branch: "feature_conflict" },
{ source_branch: "markdown" },
{ source_branch: "feature_one" },
{ source_branch: "merged-target" }
]
context 'with an invalid period_limit' do
let(:opts) { base_opts.merge!(period_limit: 'many') }
it 'raises an error' do
expect { subject }.to raise_error(described_class::InvalidPeriodLimitError, "Invalid `:period_limit` option: `many`. Expected an integer!")
end
end
end
......
......@@ -20,7 +20,7 @@ RSpec.describe Gitlab::Insights::Reducers::CountPerLabelReducer do
filter_labels: [label_bug.title],
collection_labels: [label_manage.title, label_plan.title],
group_by: 'month',
period_limit: 2
period_limit: 5
}
end
let(:issuable_relation) { find_issuables(project, opts) }
......
......@@ -9,8 +9,8 @@ RSpec.describe Gitlab::Insights::Reducers::CountPerPeriodReducer do
Gitlab::Insights::Finders::IssuableFinder.new(project, nil, opts).find
end
def reduce(issuable_relation, period, period_field = :created_at)
described_class.reduce(issuable_relation, period: period, period_field: period_field)
def reduce(issuable_relation, period, period_limit = 5, period_field = :created_at)
described_class.reduce(issuable_relation, period: period, period_limit: period_limit, period_field: period_field)
end
let(:opts) do
......@@ -19,7 +19,7 @@ RSpec.describe Gitlab::Insights::Reducers::CountPerPeriodReducer do
issuable_type: 'issue',
filter_labels: [label_bug.title],
group_by: 'month',
period_limit: 3
period_limit: 5
}
end
let(:issuable_relation) { find_issuables(project, opts) }
......@@ -29,8 +29,10 @@ RSpec.describe Gitlab::Insights::Reducers::CountPerPeriodReducer do
let(:expected) do
{
'January 2019' => 1,
'February 2019' => 1,
'March 2019' => 1
'February 2019' => 0,
'March 2019' => 1,
'April 2019' => 1,
'May 2019' => 0
}
end
......@@ -39,7 +41,11 @@ RSpec.describe Gitlab::Insights::Reducers::CountPerPeriodReducer do
end
it 'raises an error for an unknown :period_field option' do
expect { reduce(issuable_relation, 'month', :foo) }.to raise_error(described_class::InvalidPeriodFieldError, "Invalid value for `period_field`: `foo`. Allowed values are #{described_class::VALID_PERIOD_FIELD}!")
expect { reduce(issuable_relation, 'month', 5, :foo) }.to raise_error(described_class::InvalidPeriodFieldError, "Invalid value for `period_field`: `foo`. Allowed values are #{described_class::VALID_PERIOD_FIELD}!")
end
it 'raises an error for an unknown :period_limit option' do
expect { reduce(issuable_relation, 'month', -1) }.to raise_error(described_class::InvalidPeriodLimitError, "Invalid value for `period_limit`: `-1`. Value must be greater than 0!")
end
it 'returns issuables with only the needed fields' do
......
......@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Insights::Reducers::LabelCountPerPeriodReducer do
end
def reduce(issuable_relation, period, labels)
described_class.reduce(issuable_relation, period: period, labels: labels)
described_class.reduce(issuable_relation, period: period, period_limit: 5, labels: labels)
end
let(:opts) do
......@@ -20,7 +20,7 @@ RSpec.describe Gitlab::Insights::Reducers::LabelCountPerPeriodReducer do
filter_labels: [label_bug.title],
collection_labels: [label_manage.title, label_plan.title],
group_by: 'month',
period_limit: 3
period_limit: 5
}
end
let(:issuable_relation) { find_issuables(project, opts) }
......@@ -35,14 +35,24 @@ RSpec.describe Gitlab::Insights::Reducers::LabelCountPerPeriodReducer do
Gitlab::Insights::UNCATEGORIZED => 1
},
'February 2019' => {
label_manage.title => 1,
label_manage.title => 0,
label_plan.title => 0,
Gitlab::Insights::UNCATEGORIZED => 0
},
'March 2019' => {
label_manage.title => 1,
label_plan.title => 0,
Gitlab::Insights::UNCATEGORIZED => 0
},
'April 2019' => {
label_manage.title => 0,
label_plan.title => 1,
Gitlab::Insights::UNCATEGORIZED => 0
},
'May 2019' => {
label_manage.title => 0,
label_plan.title => 0,
Gitlab::Insights::UNCATEGORIZED => 0
}
}
end
......
......@@ -2,7 +2,7 @@
RSpec.shared_context 'Insights reducers context' do
around do |example|
Timecop.freeze(Time.utc(2019, 3, 5)) { example.run }
Timecop.freeze(Time.utc(2019, 5, 5)) { example.run }
end
let(:project) { create(:project, :public) }
......@@ -11,6 +11,6 @@ RSpec.shared_context 'Insights reducers context' do
let(:label_plan) { create(:label, project: project, name: 'Plan') }
let!(:issuable0) { create(:labeled_issue, :opened, created_at: Time.utc(2019, 1, 5), project: project) }
let!(:issuable1) { create(:labeled_issue, :opened, created_at: Time.utc(2019, 1, 5), labels: [label_bug], project: project) }
let!(:issuable2) { create(:labeled_issue, :opened, created_at: Time.utc(2019, 2, 5), labels: [label_bug, label_manage, label_plan], project: project) }
let!(:issuable3) { create(:labeled_issue, :opened, created_at: Time.utc(2019, 3, 5), labels: [label_bug, label_plan], project: project) }
let!(:issuable2) { create(:labeled_issue, :opened, created_at: Time.utc(2019, 3, 5), labels: [label_bug, label_manage, label_plan], project: project) }
let!(:issuable3) { create(:labeled_issue, :opened, created_at: Time.utc(2019, 4, 5), labels: [label_bug, label_plan], project: project) }
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