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