backlogs refactoring

pull/6827/head
friflaj 15 years ago
parent 84c21a6955
commit 5368263535
  1. 309
      app/models/sprint.rb
  2. 223
      app/views/backlogs/_burndown.html.erb
  3. 4
      app/views/backlogs/_view_issues_sidebar.html.erb
  4. 9
      app/views/backlogs/burndown.html.erb
  5. 2
      config/locales/en.yml
  6. 2
      config/locales/nl.yml
  7. 14
      lib/backlogs_project_patch.rb

@ -1,5 +1,170 @@
require 'date'
class Burndown
class Series < Array
def initialize(*args)
@units = args.pop
@name = args.pop
@display = true
raise "Unsupported unit '#{@units}'" unless [:points, :hours].include? @units
raise "Name '#{@name}' must be a symbol" unless @name.is_a? Symbol
super(*args)
end
attr_reader :units
attr_reader :name
attr_accessor :display
end
def initialize(sprint, burn_direction = nil)
burn_direction = burn_direction || Setting.plugin_redmine_backlogs[:points_burn_direction]
@days = sprint.days
@sprint_id = sprint.id
# end date for graph
days = @days
daycount = days.size
days = sprint.days(Date.today) if sprint.effective_date > Date.today
_series = ([nil] * days.size)
# load cache
day_index = to_h(days, 0.upto(days.size - 1).collect{|d| d})
starts = sprint.sprint_start_date
BurndownDay.find(:all, :order=>'created_at', :conditions => ["version_id = ?", sprint.id]).each {|data|
day = day_index[data.created_at.to_date]
next if !day
_series[day] = [data.points_committed.to_f, data.points_resolved.to_f, data.points_accepted.to_f, data.remaining_hours.to_f]
}
backlog = nil
# calculate first day if not loaded from cache
if !_series[0]
backlog ||= sprint.stories
_series[0] = [
backlog.inject(0) {|sum, story| sum + story.story_points.to_f },
backlog.select {|s| s.done_ratio == 100 }.inject(0) {|sum, story| sum + story.story_points.to_f },
backlog.select {|s| s.closed? }.inject(0) {|sum, story| sum + story.story_points.to_f },
backlog.inject(0) {|sum, story| sum + story.estimated_hours.to_f }
]
cache(days[0], _series[0])
end
# calculate last day if not loaded from cache
if !_series[-1]
backlog ||= sprint.stories
_series[-1] = [
backlog.inject(0) {|sum, story| sum + story.story_points.to_f },
backlog.select {|s| s.done_ratio == 100 }.inject(0) {|sum, story| sum + story.story_points.to_f },
backlog.select {|s| s.closed? }.inject(0) {|sum, story| sum + story.story_points.to_f },
backlog.select {|s| not s.closed? && s.done_ratio != 100 }.inject(0) {|sum, story| sum + story.remaining_hours.to_f }
]
cache(days[-1], _series[-1])
end
# fill out series
last = nil
_series = _series.enum_for(:each_with_index).collect{|v, i| v.nil? ? last : (last = v; v) }
# make registered series
points_committed, points_resolved, points_accepted, remaining_hours = _series.transpose
make_series :points_committed, :points, points_committed
make_series :points_resolved, :points, points_resolved
make_series :points_accepted, :points, points_accepted
make_series :remaining_hours, :hours, remaining_hours
# calculate burn-up ideal
if daycount == 1 # should never happen
make_series :ideal, :points, [points_committed]
else
make_series :ideal, :points, points_committed.enum_for(:each_with_index).collect{|c, i| c * i * (1.0 / (daycount - 1)) }
end
# burn-down equivalents to the burn-up chart
make_series :points_to_resolve, :points, points_committed.zip(points_resolved).collect{|c, r| c - r}
make_series :points_to_accept, :points, points_committed.zip(points_accepted).collect{|c, a| c - a}
# required burn-rate
make_series :required_burn_rate_points, :points, @points_to_resolve.enum_for(:each_with_index).collect{|p, i| p / (daycount - i) }
make_series :required_burn_rate_hours, :hours, remaining_hours.enum_for(:each_with_index).collect{|r, i| r / (daycount-i) }
# mark series to be displayed if they're not constant-zero, or
# just constant in case of points-committed
@available_series.values.each{|s|
const_val = (s.name == :points_committed ? s[0] : 0)
@available_series[s.name].display = (s.select{|v| v != const_val}.size != 0)
}
# decide whether you want burn-up or down
if burn_direction == 'down'
@ideal.each_with_index{|v, i| @ideal[i] = @points_committed[i] - v}
@points_accepted.display = false
@points_resolved.display = false
@available_series.delete(:points_accepted)
@available_series.delete(:points_resolved)
else
@points_to_accept.display = false
@points_to_resolve.display = false
@available_series.delete(:points_to_accept)
@available_series.delete(:points_to_resolve)
end
@max = {
:points => @available_series.values.select{|s| s.units == :points}.flatten.compact.max,
:hours => @available_series.values.select{|s| s.units == :hours}.flatten.compact.max
}
end
attr_reader :days
attr_reader :sprint_id
attr_reader :max
attr_reader :points_committed
attr_reader :points_resolved
attr_reader :points_accepted
attr_reader :remaining_hours
attr_reader :ideal
attr_reader :points_to_resolve
attr_reader :points_to_accept
attr_reader :required_burn_rate_points
attr_reader :required_burn_rate_hours
def series(select = :active)
return @available_series.values.select{|s| (select == :all) || s.display }.sort{|x,y| "#{x.name}" <=> "#{y.name}"}
end
private
def cache(day, datapoint)
datapoint = {
:points_committed => datapoint[0],
:points_resolved => datapoint[1],
:points_accepted => datapoint[2],
:remaining_hours => datapoint[3],
:created_at => day,
:version_id => @sprint_id
}
bdd = BurndownDay.new datapoint
bdd.save!
end
def make_series(name, units, data)
@available_series ||= {}
s = Burndown::Series.new(data, name, units)
@available_series[name] = s
instance_variable_set("@#{name}", s)
end
def to_h(keys, values)
return Hash[*keys.zip(values).flatten]
end
end
class Sprint < Version
unloadable
@ -75,155 +240,17 @@ class Sprint < Version
def activity
bd = self.burndown('up')
return false if !bd || !bd[:series] || !bd[:series][:remaining_hours]
return false if !bd
# assume a sprint is active if it's only 2 days old
return true if bd[:series][:remaining_hours].size <= 2
return true if bd.remaining_hours.size <= 2
return Issue.exists?(['fixed_version_id = ? and ((updated_on between ? and ?) or (created_on between ? and ?))', self.id, -2.days.from_now, Time.now, -2.days.from_now, Time.now])
end
def burndown(burn_direction = nil)
return nil if not self.has_burndown
end_date = self.effective_date > Date.today ? Date.today : self.effective_date
so_far = self.days(end_date)
cached = {}
BurndownDay.find(:all, :order=>'created_at', :conditions => ["version_id = ?", self.id]).each {|data|
day = data.created_at.to_date
next if day > end_date or day < self.sprint_start_date
cached[day] = {
:points_committed => data.points_committed,
:points_resolved => data.points_resolved,
:points_accepted => data.points_accepted,
:remaining_hours => data.remaining_hours
}
}
backlog = nil
remaining_days = self.days.length
datapoints = []
max_points = 0
max_hours = 0
if remaining_days > 1
ideal_delta = (1.0 / (remaining_days - 1))
else
ideal_delta = 0
end
ideal_factor = 0
so_far.each { |day|
if cached.has_key?(day)
datapoint = cached[day]
else
if day == self.sprint_start_date or day == end_date
backlog = backlog.nil? ? self.stories : backlog
# no stories, nothing to do
break if backlog.length == 0
datapoint = {
:points_committed => backlog.inject(0) {|sum, story| sum + story.story_points.to_f } ,
:points_resolved => backlog.select {|s| s.done_ratio == 100 }.inject(0) {|sum, story| sum + story.story_points.to_f },
:points_accepted => backlog.select {|s| s.closed? }.inject(0) {|sum, story| sum + story.story_points.to_f },
}
# start of sprint
if day == self.sprint_start_date
datapoint[:remaining_hours] = backlog.inject(0) {|sum, story| sum + story.estimated_hours.to_f }
else
datapoint[:remaining_hours] = backlog.select {|s| not s.closed? && s.done_ratio != 100 }.inject(0) {|sum, story| sum + story.remaining_hours.to_f }
end
bdd = BurndownDay.new datapoint.merge(:created_at => day, :updated_at => day, :version_id => self.id)
bdd.save!
else
# we should never get here.
# for some reason the burndown wasn't generated on
# the specified day, return the last known values
# I don't save these because they're a) cheap to
# regenerate, and b) not actual measurements
datapoint = (datapoints.length > 0 ? datapoints[-1].dup : {})
end
end
if datapoint[:points_committed].class == NilClass or datapoint[:points_resolved].class == NilClass
datapoint[:required_burn_rate_points] = nil
else
datapoint[:required_burn_rate_points] = (datapoint[:points_committed] - datapoint[:points_resolved]) / remaining_days
end
max_points = [max_points, datapoint[:points_committed]].compact.max
max_hours = [max_hours, datapoint[:remaining_hours]].compact.max
if datapoint[:remaining_hours].class == NilClass
datapoint[:required_burn_rate_hours] = nil
else
datapoint[:required_burn_rate_hours] = datapoint[:remaining_hours] / remaining_days
end
datapoint[:ideal] = datapoint[:points_committed] * ideal_factor if datapoint[:points_committed]
datapoints << datapoint
remaining_days -= 1
ideal_factor += ideal_delta
}
units = {
:points_committed => :points,
:points_resolved => :points,
:points_accepted => :points,
:points_to_accept => :points,
:points_to_resolve => :points,
:ideal => :points,
:remaining_hours => :hours,
:required_burn_rate_points => :points,
:required_burn_rate_hours => :hours
}
datasets = {}
[:points_committed, :points_resolved, :points_accepted, :ideal, :remaining_hours, :required_burn_rate_points, :required_burn_rate_hours].each {|series|
data = datapoints.collect {|d| d[series]}
if not data.select{|d| d != 0 and not d.class == NilClass }.empty?
datasets[series] = data
end
}
burn_direction ||= Setting.plugin_redmine_backlogs[:points_burn_direction]
if burn_direction == 'down'
if datasets[:points_committed]
if datasets.include? :ideal
if datasets[:points_committed]
datasets[:ideal] = datasets[:ideal].zip(datasets[:points_committed]).collect{|d, c| c - d}
else
datasets.delete(:ideal)
end
end
[[:points_accepted, :points_to_accept], [:points_resolved, :points_to_resolve]].each{|src, tgt|
next if not datasets.include? src
datasets[tgt] = datasets[src].zip(datasets[:points_committed]).collect{|d, c| c - d} if datasets[:points_committed]
}
end
# only show points committed if they're not constant
datasets.delete(:points_committed) if datasets[:points_committed] and datasets[:points_committed].collect{|d| d != datasets[:points_committed][0]}.empty?
datasets.delete(:points_resolved)
datasets.delete(:points_accepted)
end
# clear overlap between accepted/resolved
[[:points_resolved, :points_accepted], [:points_to_resolve, :points_to_accept]].each{|r, a|
datasets.delete(r) if datasets.has_key? r and datasets.has_key? a and datasets[a] == datasets[r]
}
return { :dates => self.days, :series => datasets, :units => units, :max => {:points => max_points, :hours => max_hours} }
return Burndown.new(self, burn_direction)
end
def self.generate_burndown(only_current = true)

@ -1,137 +1,116 @@
<%- dataseries = charts.map{|c| c[:burndown][:series].keys}.flatten.uniq.sort{|a, b| l(a) <=> l(b) } %>
<%- charts = (burndown.is_a?(Array) ? burndown : [burndown]) %>
<%- dataseries = charts[0].series(:all).collect{|series| series.name } %>
<%- content_for :header_tags do %>
<%= stylesheet_link_tag 'jquery.jqplot.min.css', :plugin => 'redmine_backlogs' %>
<%= javascript_include_tag 'jquery-1.4.2.min.js', :plugin => 'redmine_backlogs' %>
<!--[if IE]><%= javascript_include_tag 'jquery.jqplot/excanvas.js', :plugin => 'redmine_backlogs' %><![endif]-->
<%= javascript_include_tag 'jquery.jqplot/jquery.jqplot.min.js', :plugin => 'redmine_backlogs' %>
<%= javascript_include_tag 'jquery.jqplot/plugins/jqplot.highlighter.js', :plugin => 'redmine_backlogs' %>
<%= javascript_include_tag 'jquery.cookies.2.2.0.min.js', :plugin => 'redmine_backlogs' %>
<style type="text/css" media="screen">
.jqplot-axis {
font-size: 0.85em;
}
.jqplot-title {
font-size: 1.1em;
}
.jqplot-y6axis-tick {
padding-right: 0px;
}
</style>
<script type="text/javascript" language="javascript">
$j = jQuery.noConflict();
<%= stylesheet_link_tag 'jquery.jqplot.min.css', :plugin => 'redmine_backlogs' %>
<% charts.each do |chart| %>
chart_<%= chart[:div] %> = null;
<% end %>
<%= javascript_include_tag 'jquery-1.4.2.min.js', :plugin => 'redmine_backlogs' %>
<!--[if IE]><%= javascript_include_tag 'jquery.jqplot/excanvas.js', :plugin => 'redmine_backlogs' %><![endif]-->
<%= javascript_include_tag 'jquery.jqplot/jquery.jqplot.js', :plugin => 'redmine_backlogs' %>
<%= javascript_include_tag 'jquery.jqplot/plugins/jqplot.highlighter.js', :plugin => 'redmine_backlogs' %>
<%= javascript_include_tag 'jquery.cookies.2.2.0.min.js', :plugin => 'redmine_backlogs' %>
<% charts.each do |chart| %>
series_no_<%= chart[:div] %> = {}
<% no = -1
dataseries.each do |series|
next if ! chart[:burndown][:series].include? series
no += 1
%>
series_no_<%= chart[:div] %>['<%= series.to_s %>'] = <%= no %>;
<% end %>
<% end %>
<style type="text/css" media="screen">
.jqplot-axis {
font-size: 0.85em;
}
.jqplot-title {
font-size: 1.1em;
}
.jqplot-y6axis-tick {
padding-right: 0px;
}
</style>
<script type="text/javascript" language="javascript">
// chart objects
burndown_charts = new Object();
// TODO: I have no idea why plain 'jQuery' doesn't work
burndown_charts.jquery = jQuery.noConflict();
<% charts.each do |chart| %>
<%- burndown = chart[:burndown] %>
<%- div = chart[:div] %>
function burndown_<%= div %>() {
<% burndown[:series].keys.each do |series| %>
<%= series.to_s %> = [ <%= burndown[:series][series].collect {|d| "#{d}"}.join(',') %>];
<% end %>
if ($j("#<%= div %>").hasClass('minimal')) {
decoration = false;
} else {
decoration = true;
}
xticks = [ <%= burndown[:dates].enum_with_index.collect {|d,i| "[#{i+1}, '#{escape_javascript(::I18n.t('date.abbr_day_names')[d.wday % 7])}']"}.join(',') %> ];
datasets = [ <%= dataseries.collect{|s| s.to_s}.join(',') %> ];
series = [
<% dataseries.each_with_index do |series, i| %>
{
label: '<%= escape_javascript(l(series)) %>',
yaxis: '<%= burndown[:units][series] == :points ? 'yaxis' : 'y2axis' %>'
}<% if i != dataseries.length - 1 %>,<% end %>
<% end %>
];
// add a little clearance to the chart
max_points = <%= burndown[:max][:points] == 0 ? 1 : 1.01 * burndown[:max][:points] %>;
max_hours = <%= burndown[:max][:hours] == 0 ? 1 : 1.01 * burndown[:max][:hours] %>;
chart_<%= div %> = $j.jqplot('<%= div %>', datasets, {
legend: {show: decoration, location: 'sw'},
grid: {shadow:false},
seriesDefaults: {showMarker: decoration, shadow:false},
axesDefaults: {showTick: decoration},
series: series,
axes:{
xaxis:{min:1, max:<%= burndown[:dates].length %>, tickInterval:1, ticks:xticks},
yaxis:{min:0, max: max_points, tickOptions:{formatString:'%d'}},
y2axis:{min:0, max: max_hours, tickOptions:{formatString:'%d'}}
},
highlighter:{
tooltipAxes: 'y',
formatString: '%s'
}
});
}
<% end %>
<%- charts.each do |chart| %>
burndown_charts[<%= chart.sprint_id%>] = {
chart: null,
dataset_position: {<%= chart.series.enum_for(:each_with_index).collect{|s, i| "#{s.name}: #{i}"}.join(', ') %>},
datasets: [<%= chart.series.collect{|s| "[#{s.join(',')}]"}.join(', ') %>],
series: [<%= chart.series.collect{|s| "{ label: '#{escape_javascript(l(s.name))}', yaxis: 'y#{s.units == :points ? '' : '2'}axis' }"}.join(',') %>],
xticks: [<%= chart.days.each_with_index.collect{|d,i| "[#{i+1}, '#{escape_javascript(::I18n.t('date.abbr_day_names')[d.wday % 7])}']"}.join(',') %>],
function configure(cb)
{
disabled = '|' + $j.cookies.get('burndown_series') + '|';
// add a little clearance to the chart
max_y: <%= chart.max[:points] == 0 ? 1 : 1.01 * chart.max[:points] %>,
max_y2: <%= chart.max[:hours] == 0 ? 1 : 1.01 * chart.max[:hours] %>,
if (!(cb == undefined)) {
_disabled = [];
<% dataseries.each do |series| %>
if (disabled.indexOf('|<%= series.to_s %>|') != -1 && cb.value != '<%= series.to_s %>') {
_disabled.push('<%= series.to_s %>');
}
<% end %>
if (!cb.checked) {
_disabled.push(cb.value);
}
disabled = _disabled.join('|');
$j.cookies.set('burndown_series', disabled, { expiresAt: new Date((new Date()).getFullYear + 100, 1, 1)});
disabled = '|' + disabled + '|';
}
initialize: function() {
decoration = !(burndown_charts.jquery('#<%= "#{div}#{chart.sprint_id}" %>').hasClass('minimal'));
/* enable all */
checkboxes = $j('.series_enabled');
if (checkboxes) {
checkboxes.attr('checked', true);
this.chart = burndown_charts.jquery.jqplot('<%= "#{div}#{chart.sprint_id}" %>', this.datasets, {
legend: {show: decoration, location: 'sw'},
grid: {shadow:false},
seriesDefaults: {showMarker: decoration, shadow:false},
axesDefaults: {showTick: decoration},
series: this.series,
axes:{
xaxis:{min:1, max:<%= chart.days.length %>, tickInterval:1, ticks: this.xticks},
yaxis:{min:0, max: this.max_y, tickOptions:{formatString:'%d'}},
y2axis:{min:0, max: this.max_y2, tickOptions:{formatString:'%d'}}
},
highlighter:{
tooltipAxes: 'y',
formatString: '%s'
}
});
},
<% dataseries.each do |series| %>
checkbox = $j('#<%= series.to_s %>_enabled');
if (checkbox && -1 != disabled.indexOf('|<%= series.to_s %>|')) { checkbox.attr('checked', false); }
<% end %>
reconfigure: function(disabled) {
if (this.chart) {
<%- dataseries.each do |series| %>
p = this.dataset_position['<%= "#{series}" %>'];
if (!(p == undefined)) { this.chart.series[p].show = (disabled.indexOf('|<%= "#{series}" %>|') == -1); };
<%- end %>
this.chart.replot();
}
}
};
<%- end %>
<% charts.each do |chart| %>
if (chart_<%= chart[:div] %>) {
<% dataseries.each do |series| %>
if (!(series_no_<%= chart[:div] %>['<%= series.to_s %>'] == undefined)) {
chart_<%= chart[:div] %>.series[series_no_<%= chart[:div] %>['<%= series.to_s %>']].show = (disabled.indexOf('|<%= series.to_s %>|') == -1);
}
<% end %>
}
function burndown_configure(cb)
{
disabled = '|' + burndown_charts.jquery.cookies.get('burndown_series') + '|';
chart_<%= chart[:div] %>.replot();
<% end %>
if (!(cb == undefined)) {
_disabled = [];
<%- dataseries.each do |series| %>
if (disabled.indexOf('|<%= "#{series}" %>|') != -1 && cb.value != '<%= "#{series}" %>') {
_disabled.push('<%= "#{series}" %>');
}
<%- end %>
if (!cb.checked) {
_disabled.push(cb.value);
}
disabled = _disabled.join('|');
burndown_charts.jquery.cookies.set('burndown_series', disabled, { expiresAt: new Date((new Date()).getFullYear + 100, 1, 1)});
disabled = '|' + disabled + '|';
}
checkboxes = burndown_charts.jquery('.series_enabled');
if (checkboxes) {
/* enable all, then reset */
checkboxes.attr('checked', true);
<%- dataseries.each do |series| %>
checkbox = burndown_charts.jquery('#<%= "#{series}" %>_enabled');
if (checkbox && -1 != disabled.indexOf('|<%= "#{series}" %>|')) { checkbox.attr('checked', false); }
<%- end %>
}
<%- charts.each do |chart| %>
burndown_charts[<%= chart.sprint_id %>].reconfigure(disabled);
<%- end %>
}
$j(document).ready(function(){
<% charts.each do |chart| %>
burndown_<%= chart[:div] %>();
<% end %>
configure();
});
</script>
burndown_charts.jquery(document).ready(function(){
<%- charts.each do |chart| %>
burndown_charts[<%= chart.sprint_id%>].initialize();
<%- end %>
burndown_configure();
});
</script>
<%- end %>

@ -69,7 +69,7 @@
%><br/>
<% end %>
<div id="burndown" class="minimal" style="margin-top:20px; margin-left:20px; width:256px; height:192px;"></div>
<%= render :partial => "backlogs/burndown", :locals => {:charts => [{:div => 'burndown', :burndown => sprint.burndown }]} %>
<div id="burndown_<%= sprint.id %>" class="minimal" style="margin-top:20px; margin-left:20px; width:256px; height:192px;"></div>
<%= render :partial => "backlogs/burndown", :locals => {:div => 'burndown_', :burndown => sprint.burndown } %>
<% end %>
</div>

@ -1,11 +1,10 @@
<%= render :partial => 'burndown', :locals => {:charts => [{:div => 'burndown', :burndown => @burndown }]} %>
<div id="burndown" style="margin-top:20px; margin-left:20px; width:640px; height:480px;"></div>
<%= render :partial => 'burndown', :locals => {:div => 'burndown_', :burndown => @burndown } %>
<div id="burndown_<%= @burndown.sprint_id %>" style="margin-top:20px; margin-left:20px; width:640px; height:480px;"></div>
<%- dataseries = @burndown[:series].keys.sort{|a, b| l(a) <=> l(b) } %>
<fieldset>
<legend><%= l(:label_series) %></legend>
<% dataseries.each do |series| %>
<input class="series_enabled" type="checkbox" onclick="configure(this)" id="<%= series %>_enabled" name="<%= series %>_enabled" value="<%= series %>"><%= l(series) %><br/>
<% @burndown.series(:all).each do |series| %>
<input class="series_enabled" type="checkbox" onclick="burndown_configure(this)" id="<%= series.name %>_enabled" name="<%= series.name %>_enabled" value="<%= series.name %>"><%= l(series.name) %><br/>
<% end %>
</fieldset>

@ -21,8 +21,10 @@ en:
required_burn_rate_points: "required burn rate (points)"
required_burn_rate_hours: "required burn rate (hours)"
points_to_accept: "points not accepted"
points_accepted: "points accepted"
ideal: "ideal"
points_to_resolve: "points not resolved"
points_resolved: "points resolved"
remaining_hours: "remaining hours"
field_velocity_based_estimate: "Velocity based estimate"

@ -32,6 +32,7 @@ nl:
field_backlogs_issue_type: Backlog type
label_points_burn_up: Omhoog
points_to_resolve: points niet opgelost
points_resolved: points opgelost
product_backlog_unsized: De top van de product backlog heeft stories zonder sizes
active_sprint_unestimated_tasks: De lopende sprint heeft taken zonder urenschatting
pessimistic_velocity: Project plant {{pct}}% te pessimistisch
@ -41,6 +42,7 @@ nl:
field_position: Positie
required_burn_rate_points: benodigde burn rate (points)
points_to_accept: points niet geaccepteerd
points_accepted: points geaccepteerd
label_sprint_name: Sprint "{{name}}"
backlogs_product_backlog: Product backlog
remaining_hours: resterende uren

@ -92,15 +92,15 @@ module Backlogs
bd = sprint.burndown('up')
if bd
bd[:series][:points_accepted] ||= [0]
bd[:series][:points_committed] ||= [0]
bd[:series][:remaining_hours] ||= [0]
_accepted = bd.points_accepted || [0]
_committed = bd.points_committed || [0]
_remaining = bd.remaining_hours || [0]
accepted += bd[:series][:points_accepted][-1]
committed += bd[:series][:points_committed][0]
accepted += _accepted[-1]
committed += _committed[0]
if points_per_hour && bd[:series][:remaining_hours][0] > 0
pph = (1.0 * bd[:series][:points_committed]) / bd[:series][:remaining_hours][0]
if points_per_hour && _remaining[0] > 0
pph = (1.0 * _committed[0]) / _remaining[0]
pph_count += 1
pph_diff += (pph - points_per_hour).abs
end

Loading…
Cancel
Save