Commit 93e9a1e5 authored by Alexandru Croitor's avatar Alexandru Croitor

Refactor burndown chart to accept a list of issues as input

This refactoring should help use Burndown to compute data
for various scenarios, by providing it a list of issues,
start and due dates. We want to reuse this not only for milestone
burndown chart but also for board burndown chart.
parent 0ce66da0
...@@ -3,7 +3,10 @@ ...@@ -3,7 +3,10 @@
module EE module EE
module MilestonesHelper module MilestonesHelper
def burndown_chart(milestone) def burndown_chart(milestone)
Burndown.new(milestone, current_user) if milestone.supports_burndown_charts? if milestone.supports_burndown_charts?
issues = milestone.issues_visible_to_user(current_user)
Burndown.new(issues, milestone.start_date, milestone.due_date)
end
end end
def can_generate_chart?(milestone, burndown) def can_generate_chart?(milestone, burndown)
......
...@@ -3,17 +3,19 @@ ...@@ -3,17 +3,19 @@
class Burndown class Burndown
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
attr_reader :start_date, :due_date, :end_date, :accurate, :milestone, :current_user attr_reader :issues, :start_date, :end_date, :due_date, :accurate
alias_method :accurate?, :accurate alias_method :accurate?, :accurate
def initialize(milestone, current_user) def initialize(issues, start_date, due_date)
@milestone = milestone @start_date = start_date
@current_user = current_user @due_date = due_date
@start_date = @milestone.start_date @end_date = if due_date.blank? || due_date > Date.today
@due_date = @milestone.due_date Date.today
@end_date = @milestone.due_date else
@end_date = Date.today if @end_date.present? && @end_date > Date.today due_date
end
@issues = filter_issues_created_before(@end_date, issues)
@accurate = true @accurate = true
end end
...@@ -36,7 +38,7 @@ class Burndown ...@@ -36,7 +38,7 @@ class Burndown
# If all closed issues have no closed events, mark burndown chart as containing legacy data # If all closed issues have no closed events, mark burndown chart as containing legacy data
def legacy_data? def legacy_data?
strong_memoize(:legacy_data) do strong_memoize(:legacy_data) do
closed_events = milestone_issues.select(&:closed?) closed_events = issues.select(&:closed?)
closed_events.any? && !Event.closed.where(target: closed_events, action: Event::CLOSED).exists? closed_events.any? && !Event.closed.where(target: closed_events, action: Event::CLOSED).exists?
end end
end end
...@@ -44,7 +46,7 @@ class Burndown ...@@ -44,7 +46,7 @@ class Burndown
private private
def burndown_events def burndown_events
milestone_issues issues
.map { |issue| burndown_events_for(issue) } .map { |issue| burndown_events_for(issue) }
.flatten .flatten
end end
...@@ -57,22 +59,12 @@ class Burndown ...@@ -57,22 +59,12 @@ class Burndown
].compact ].compact
end end
def milestone_issues
return [] unless valid?
strong_memoize(:milestone_issues) do
@milestone
.issues_visible_to_user(current_user)
.where('issues.created_at <= ?', end_date.end_of_day)
end
end
def milestone_events_per_issue def milestone_events_per_issue
return [] unless valid? return [] unless valid?
strong_memoize(:milestone_events_per_issue) do strong_memoize(:milestone_events_per_issue) do
Event Event
.where(target: milestone_issues, action: [Event::CLOSED, Event::REOPENED]) .where(target: issues, action: [Event::CLOSED, Event::REOPENED])
.where('created_at <= ?', end_date.end_of_day) .where('created_at <= ?', end_date.end_of_day)
.order(:created_at) .order(:created_at)
.group_by(&:target_id) .group_by(&:target_id)
...@@ -112,10 +104,16 @@ class Burndown ...@@ -112,10 +104,16 @@ class Burndown
# Mark burndown chart as inaccurate # Mark burndown chart as inaccurate
@accurate = false @accurate = false
build_burndown_event(milestone.start_date.beginning_of_day, issue.weight, 'closed') build_burndown_event(start_date.beginning_of_day, issue.weight, 'closed')
end end
def build_burndown_event(created_at, issue_weight, action) def build_burndown_event(created_at, issue_weight, action)
{ created_at: created_at, weight: issue_weight, action: action } { created_at: created_at, weight: issue_weight, action: action }
end end
def filter_issues_created_before(date, issues)
return [] unless valid?
issues.where('issues.created_at <= ?', date.end_of_day)
end
end end
...@@ -11,7 +11,8 @@ module EE ...@@ -11,7 +11,8 @@ module EE
milestone = parent.milestones.find(params[:milestone_id]) milestone = parent.milestones.find(params[:milestone_id])
if milestone.supports_burndown_charts? if milestone.supports_burndown_charts?
present Burndown.new(milestone, current_user).as_json issues = milestone.issues_visible_to_user(current_user)
present Burndown.new(issues, milestone.start_date, milestone.due_date).as_json
else else
render_api_error!("Milestone does not support burndown chart", 405) render_api_error!("Milestone does not support burndown chart", 405)
end end
......
...@@ -18,7 +18,7 @@ describe Burndown do ...@@ -18,7 +18,7 @@ describe Burndown do
end end
end end
subject { described_class.new(milestone, user).as_json } subject { described_class.new(milestone.issues_visible_to_user(user), milestone.start_date, milestone.due_date).as_json }
it 'generates an array of issues with date, issue weight and action' do it 'generates an array of issues with date, issue weight and action' do
expect(subject).to match_array([ expect(subject).to match_array([
...@@ -39,7 +39,7 @@ describe Burndown do ...@@ -39,7 +39,7 @@ describe Burndown do
subject do subject do
project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
described_class.new(milestone, non_member).as_json.each { |event| event[:created_at] = event[:created_at].to_date } described_class.new(milestone.issues_visible_to_user(non_member), milestone.start_date, milestone.due_date).as_json.each { |event| event[:created_at] = event[:created_at].to_date }
end end
it 'does not include confidential issues for users who are not project members' do it 'does not include confidential issues for users who are not project members' do
...@@ -74,15 +74,16 @@ describe Burndown do ...@@ -74,15 +74,16 @@ describe Burndown do
end end
it "sets attribute accurate to true" do it "sets attribute accurate to true" do
burndown = described_class.new(milestone, user) burndown = described_class.new(milestone.issues_visible_to_user(user), milestone.start_date, milestone.due_date)
expect(burndown).to be_accurate expect(burndown).to be_accurate
end end
it "is accurate with no issues" do it "is accurate with no issues" do
burndown = described_class.new(create(:milestone), user) new_milestone = create(:milestone)
burndown = described_class.new(new_milestone.issues_visible_to_user(user), new_milestone.start_date, new_milestone.due_date)
burndown.milestone.project.add_master(user) new_milestone.project.add_master(user)
expect(burndown).to be_accurate expect(burndown).to be_accurate
end end
...@@ -94,7 +95,7 @@ describe Burndown do ...@@ -94,7 +95,7 @@ describe Burndown do
end end
it "sets attribute empty to false" do it "sets attribute empty to false" do
burndown = described_class.new(milestone, user) burndown = described_class.new(milestone.issues_visible_to_user(user), milestone.start_date, milestone.due_date)
expect(burndown).not_to be_empty expect(burndown).not_to be_empty
end end
...@@ -128,7 +129,7 @@ describe Burndown do ...@@ -128,7 +129,7 @@ describe Burndown do
end end
it "sets attribute empty to true" do it "sets attribute empty to true" do
burndown = described_class.new(milestone, user) burndown = described_class.new(milestone.issues_visible_to_user(user), milestone.start_date, milestone.due_date)
expect(burndown).to be_empty expect(burndown).to be_empty
end end
...@@ -137,7 +138,7 @@ describe Burndown do ...@@ -137,7 +138,7 @@ describe Burndown do
context "when one but not all closed issues does not have a closed event" do context "when one but not all closed issues does not have a closed event" do
it "sets attribute accurate to false" do it "sets attribute accurate to false" do
Event.where(target: milestone.issues.closed.first, action: Event::CLOSED).destroy_all # rubocop: disable DestroyAll Event.where(target: milestone.issues.closed.first, action: Event::CLOSED).destroy_all # rubocop: disable DestroyAll
burndown = described_class.new(milestone, user) burndown = described_class.new(milestone.issues_visible_to_user(user), milestone.start_date, milestone.due_date)
aggregate_failures do aggregate_failures do
expect(burndown).not_to be_empty expect(burndown).not_to be_empty
......
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