diff --git a/app/helpers/versions_helper.rb b/app/helpers/versions_helper.rb
index 36b1b511d7..da8aec9b00 100644
--- a/app/helpers/versions_helper.rb
+++ b/app/helpers/versions_helper.rb
@@ -104,4 +104,31 @@ module VersionsHelper
end
end
end
+
+ def issues_burndown_chart_data(version)
+ return nil if version.visible_fixed_issues.empty?
+ chart_start_date = (version.start_date || version.created_on).to_date
+ chart_end_date = [version.due_date, version.visible_fixed_issues.maximum(:due_date), version.visible_fixed_issues.maximum(:updated_on)].compact.max.to_date
+ line_end_date = [User.current.today, chart_end_date].min
+ step_size = ((chart_start_date..chart_end_date).count.to_f / 90).ceil
+ issues = version.visible_fixed_issues
+ return nil if step_size < 1
+ total_closed = chart_start_date.step(line_end_date, step_size).collect{|d| {:t => d.to_s, :y => issues.where("#{Issue.table_name}.closed_on IS NULL OR #{Issue.table_name}.closed_on>=?", d.end_of_day).count}}
+ open = chart_start_date.step(line_end_date, step_size).collect{|d| {:t => d.to_s, :y => issues.where("#{Issue.table_name}.created_on<=?", d.end_of_day).count - issues.open(false).where("#{Issue.table_name}.closed_on<=?", d.end_of_day).count}}
+ chart_data = {
+ :labels => chart_start_date.step(chart_end_date, step_size).collect{|d|d.to_s},
+ :datasets => [
+ {:label => l(:label_ideal), :data => [{:t => chart_start_date.to_s, :y => total_closed.first[:y]}, {:t => chart_end_date.to_s, :y => 0}],
+ :backgroundColor => "rgba(0, 0, 0, 0)",
+ :lineTension => 0, :borderWidth => 2, :borderDash => [5, 2], :spanGaps => true},
+ {:label => l(:label_total_substract_closed), :data => total_closed,
+ :borderColor => "rgba(186, 224, 186, 1)", :backgroundColor => "rgba(0, 0, 0, 0)", :pointBackgroundColor => "rgba(186, 224, 186, 1)",
+ :lineTension => 0, :borderWidth => 2, :borderDash => [5, 2]},
+ {:label => l(:label_open), :data => open,
+ :borderColor => "rgba(186, 224, 186, 1)", :backgroundColor => "rgba(186, 224, 186, 0.1)", :pointBackgroundColor => "rgba(186, 224, 186, 1)",
+ :lineTension => 0, :borderWidth => 2}
+ ]
+ }
+ return chart_data
+ end
end
diff --git a/app/views/versions/show.html.erb b/app/views/versions/show.html.erb
index 0527eae9cc..c72643995a 100644
--- a/app/views/versions/show.html.erb
+++ b/app/views/versions/show.html.erb
@@ -52,6 +52,9 @@
<% end %>
<% end %>
+
+
+
<%= context_menu %>
<% end %>
@@ -59,3 +62,48 @@
<%= call_hook :view_versions_show_bottom, :version => @version %>
<% html_title @version.name %>
+<% if chart_data = issues_burndown_chart_data(@version) %>
+ <%= javascript_tag do %>
+ function renderChart(canvas_id, title, y_label, chartData){
+ new Chart($(canvas_id), {
+ type: 'line',
+ data: chartData,
+ options: {
+ responsive: true,
+ legend: {
+ position: 'right',
+ labels: { boxWidth: 20, fontSize: 10, padding: 10 }
+ },
+ title: { display: true, text: title },
+ tooltips: {
+ callbacks: {
+ title: function(tooltipItem, data) { return '' }
+ }
+ },
+ scales: {
+ xAxes: [{
+ type: "time",
+ time: { unit: "day", displayFormats: { day: 'YYYY-MM-DD' } },
+ gridLines: { borderDash: [6, 4] },
+ ticks: { source: 'labels', autoSkip: true }
+ }],
+ yAxes: [{
+ scaleLabel: { display: true, labelString: y_label },
+ gridLines: { borderDash: [6, 4] },
+ ticks: { min: 0, max: <%= @version.fixed_issues.count %>, stepSize: 1 }
+ }]
+ }
+ }
+ });
+ }
+ $(document).ready(function(){
+ var title = "<%= l(:label_issues_burndown) %>";
+ var y_label = "<%= l(:label_issues_burndown_y_label) %>";
+ var chartData = <%= chart_data.to_json.html_safe %>;
+ renderChart("#version_chart", title, y_label, chartData);
+ });
+ <% end %>
+ <% content_for :header_tags do %>
+ <%= javascript_include_tag "Chart.bundle.min" %>
+ <% end %>
+<% end %>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index dce5bda76b..36a51f5ce6 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -731,6 +731,11 @@ en:
label_total: Total
label_total_plural: Totals
label_total_time: Total time
+ label_issues_burndown: burndown
+ label_issues_burndown_y_label: Number of Issues
+ label_ideal: Ideal
+ label_total_substract_closed: Total - Closed
+ label_open: Open
label_permissions: Permissions
label_current_status: Current status
label_new_statuses_allowed: New statuses allowed
diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css
index 70911e4f6f..00ce4f378d 100644
--- a/public/stylesheets/application.css
+++ b/public/stylesheets/application.css
@@ -630,6 +630,7 @@ div#roadmap .wiki h1 { font-size: 120%; }
div#roadmap .wiki h2 { font-size: 110%; }
div#roadmap h2, div#roadmap h3 {padding-right: 0;}
body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
+body.controller-versions.action-show div#roadmap .version-report-graph { width: 70%; margin: 2em 0 }
div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
div#version-summary fieldset { margin-bottom: 1em; }
diff --git a/public/stylesheets/responsive.css b/public/stylesheets/responsive.css
index a4902af67d..37fed70f74 100644
--- a/public/stylesheets/responsive.css
+++ b/public/stylesheets/responsive.css
@@ -666,6 +666,7 @@
.version-overview table.progress {width:75%;}
div#version-summary {float:none; width:100%; margin-left:0;}
body.controller-versions.action-show div#roadmap .related-issues {width:100%;}
+ body.controller-versions.action-show div#roadmap .version-report-graph {width:100%;}
/* History and Changeset */
div#issue-changesets {
diff --git a/test/helpers/version_helper_test.rb b/test/helpers/version_helper_test.rb
index 0eb895e477..c844bf0fd9 100644
--- a/test/helpers/version_helper_test.rb
+++ b/test/helpers/version_helper_test.rb
@@ -25,6 +25,7 @@ class VersionsHelperTest < Redmine::HelperTest
fixtures :projects, :versions, :enabled_modules,
:users, :members, :roles, :member_roles,
:trackers, :projects_trackers,
+ :enumerations,
:issue_statuses
def test_version_filtered_issues_path_sharing_none
@@ -120,4 +121,44 @@ class VersionsHelperTest < Redmine::HelperTest
# href should contain param tracker_id=2 because for tracker_id 1, user has only readonly permissions on fixed_version_id
assert_select_in link_to_new_issue(version, project), '[href=?]', '/projects/ecookbook/issues/new?back_url=%2Fversions%2F3&issue%5Bfixed_version_id%5D=3&issue%5Btracker_id%5D=2'
end
+
+ def test_issues_burndown_chart_data_should_return_chart_data
+ User.any_instance.stubs(:today).returns(3.days.after.to_date)
+ version = Version.create!(:project => Project.find(1), :name => 'test', :due_date => 5.days.after.to_date)
+ issue = Issue.create!(:project => version.project, :fixed_version => version,
+ :priority => IssuePriority.find_by_name('Normal'),
+ :tracker => version.project.trackers.first, :subject => 'text', :author => User.current,
+ :start_date => 1.days.after.to_date)
+ chart_data = {
+ :labels => (1.days.after.to_date..5.days.after.to_date).map { |d| d.to_s },
+ :datasets => [
+ {:label => l(:label_ideal), :data => [{:t => 1.days.after.to_date.to_s, :y => 1}, {:t => 5.days.after.to_date.to_s, :y => 0}],
+ :backgroundColor => "rgba(0, 0, 0, 0)",
+ :lineTension => 0, :borderWidth => 2, :borderDash => [5, 2], :spanGaps => true},
+ {:label => l(:label_total_substract_closed), :data => [{:t => 1.days.after.to_date.to_s, :y => 1}, {:t => 2.days.after.to_date.to_s, :y => 1}, {:t => 3.days.after.to_date.to_s, :y => 1}],
+ :borderColor => "rgba(186, 224, 186, 1)", :backgroundColor => "rgba(0, 0, 0, 0)", :pointBackgroundColor => "rgba(186, 224, 186, 1)",
+ :lineTension => 0, :borderWidth => 2, :borderDash => [5, 2]},
+ {:label => l(:label_open), :data => [{:t => 1.days.after.to_date.to_s, :y => 1}, {:t => 2.days.after.to_date.to_s, :y => 1}, {:t => 3.days.after.to_date.to_s, :y => 1}],
+ :borderColor => "rgba(186, 224, 186, 1)", :backgroundColor => "rgba(186, 224, 186, 0.1)", :pointBackgroundColor => "rgba(186, 224, 186, 1)",
+ :lineTension => 0, :borderWidth => 2}
+ ]
+ }
+ assert_equal chart_data, issues_burndown_chart_data(version)
+ end
+
+ def test_issues_burndown_chart_data_should_return_nil_when_visible_fixed_issues_empty
+ version = Version.create!(:project => Project.find(1), :name => 'test')
+ version.visible_fixed_issues.destroy_all
+ assert_empty version.visible_fixed_issues
+ assert_nil issues_burndown_chart_data(version)
+ end
+
+ def test_issues_burndown_chart_data_should_return_nil_when_order_of_start_date_and_due_date_is_reversed
+ version = Version.create!(:project => Project.find(1), :name => 'test', :due_date => 10.days.after)
+ issue = Issue.create!(:project => version.project, :fixed_version => version,
+ :priority => IssuePriority.find_by_name('Normal'),
+ :tracker => version.project.trackers.first, :subject => 'text', :author => User.current,
+ :start_date => 11.days.after)
+ assert_nil issues_burndown_chart_data(version)
+ end
end