OpenProject is the leading open source project management software.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
openproject/lib/backlogs_project_patch.rb

221 lines
7.6 KiB

require_dependency 'project'
module Backlogs
class Statistics
def initialize
@errors = {}
@info = {}
end
def merge(stats, prefix = '')
errors.each {|err|
err = "#{prefix}#{err}".intern
stats[err] ||= 0
stats[err] += 1
}
return stats
end
def []=(cat, key, *args)
raise "Unexpected data category #{cat}" unless [:error, :info].include?(cat)
case args.size
when 2
subkey, value = *args
when 1
value = args[0]
subkey = nil
else
raise "Unexpected number of argments"
end
case cat
when :error
if subkey.nil?
raise "Already reported #{key.inspect}" if @errors.include?(key)
@errors[key] = value.nil? ? nil : (!!value)
else
raise "Already reported #{key.inspect}" if @errors.include?(key) && ! @errors[key].is_a?(Hash)
@errors[key] ||= {}
raise "Already errors #{key.inspect}/#{subkey.inspect}" if @errors[key].include?(subkey)
@errors[key][subkey] = value.nil? ? nil : (!!value)
end
when :info
raise "Already added info #{key.inspect}" if @info.include?(key)
@info[key] = value
end
end
def score
scoring = {}
@errors.each_pair{ |k, v|
if v.is_a? Hash
v = v.values.select{|s| !s.nil?}
scoring[k] = v.select{|s| s}.size == 0 if v.size != 0
else
scoring[k] = !v unless v.nil?
end
}
return ((scoring.values.select{|v| v}.size * 10) / scoring.size)
end
def scores(prefix='')
score = {}
@errors.each_pair{|k, v|
if v.is_a? Hash
v.each_pair {|sk, rv|
score["#{prefix}#{k}_#{sk}".intern] = rv if !rv.blank?
}
else
score["#{prefix}#{k}".intern] = v if !v.blank?
end
}
return score
end
def errors(prefix = '')
score = scores(prefix)
return score.keys.select{|k| score[k]}
end
def info(prefix='')
info = {}
@info.each_pair {|k, v|
info["#{prefix}#{k}".intern] = v
}
return info
end
end
module ProjectPatch
def self.included(base) # :nodoc:
base.extend(ClassMethods)
base.send(:include, InstanceMethods)
end
module ClassMethods
end
module InstanceMethods
def active_sprint
return Sprint.find(:first,
:conditions => ["project_id = ? and status = 'open' and ? between sprint_start_date and effective_date", self.id, Time.now])
end
def scrum_statistics
## pretty expensive to compute, so if we're calling this multiple times, return the cached results
return @scrum_statistics if @scrum_statistics
@scrum_statistics = Backlogs::Statistics.new
# magic constant
backlog = Story.product_backlog(self, 10)
active_sprint = self.active_sprint
closed_sprints = Sprint.find(:all,
:conditions => ["project_id = ? and status in ('closed', 'locked') and not(effective_date is null or sprint_start_date is null)", self.id],
:order => "effective_date desc",
:limit => 5)
all_sprints = ([active_sprint] + closed_sprints).compact
@scrum_statistics[:info, :active_sprint] = active_sprint
@scrum_statistics[:info, :closed_sprints] = closed_sprints
@scrum_statistics[:error, :product_backlog, :is_empty] = (self.status == Project::STATUS_ACTIVE && backlog.length == 0)
@scrum_statistics[:error, :product_backlog, :unsized] = backlog.inject(false) {|unsized, story| unsized || story.story_points.blank? }
@scrum_statistics[:error, :sprint, :unsized] = Issue.exists?(["story_points is null and parent_id is null and fixed_version_id in (?) and tracker_id in (?)", all_sprints.collect{|s| s.id}, Story.trackers])
@scrum_statistics[:error, :sprint, :unestimated] = Issue.exists?(["estimated_hours is null and not parent_id is null and fixed_version_id in (?) and tracker_id = ?", all_sprints.collect{|s| s.id}, Task.tracker])
@scrum_statistics[:error, :sprint, :notes_missing] = closed_sprints.inject(false){|missing, sprint| missing || !sprint.has_wiki_page}
@scrum_statistics[:error, :inactive] = (self.status == Project::STATUS_ACTIVE && !(active_sprint && active_sprint.activity))
velocity = nil
begin
points = 0
error = 0
days = 0
closed_sprints.each {|sprint|
bd = sprint.burndown('up')
accepted = (bd.points_accepted || [0])[-1]
committed = (bd.points_committed || [0])[0]
error += (1 - (accepted.to_f / committed.to_f)).abs
points += accepted
days += bd.ideal.size
}
error = (error / closed_sprints.size)
# magic constant
@scrum_statistics[:error, :velocity, :varies] = (error > 0.1)
@scrum_statistics[:error, :velocity, :missing] = false
velocity = (points / closed_sprints.size)
@scrum_statistics[:info, :velocity_divergance] = error * 100
rescue ZeroDivisionError
@scrum_statistics[:error, :velocity, :varies] = nil
@scrum_statistics[:error, :velocity, :missing] = true
@scrum_statistics[:info, :velocity_divergance] = nil
end
@scrum_statistics[:info, :velocity] = velocity
if all_sprints.size != 0 && velocity && velocity != 0
begin
dps = (all_sprints.inject(0){|d, s| d + s.days.size} / all_sprints.size)
@scrum_statistics[:info, :average_days_per_sprint] = dps
@scrum_statistics[:info, :average_days_per_point] = (velocity ? (dps.to_f / velocity) : nil)
rescue ZeroDivisionError
dps = nil
end
else
dps = nil
end
if dps.nil?
@scrum_statistics[:info, :average_days_per_sprint] = nil
@scrum_statistics[:info, :average_days_per_point] = nil
end
sizing_divergance = nil
sizing_is_consistent = false
sprint_ids = all_sprints.collect{|s| "#{s.id}"}.join(',')
story_trackers = Story.trackers.collect{|t| "#{t}"}.join(',')
if sprint_ids != '' && story_trackers != ''
select_stories = "
not (story_points is null or story_points = 0)
and not (estimated_hours is null or estimated_hours = 0)
and fixed_version_id in (#{sprint_ids})
and project_id = #{self.id}
and not parent_id is null
and tracker_id in (#{story_trackers})
"
points_per_hour = Story.find_by_sql("select avg(story_points) / avg(estimated_hours) as points_per_hour from issues where #{select_stories}")[0].points_per_hour
if points_per_hour
points_per_hour = Float(points_per_hour)
stories = Story.find(:all, :conditions => [select_stories])
error = stories.inject(0) {|err, story|
err + (1 - (points_per_hour / (story.story_points / story.estimated_hours)))
}
sizing_divergance = error * 100
# magic constant
sizing_is_consistent = (error < 0.1)
end
end
@scrum_statistics[:info, :sizing_divergance] = sizing_divergance
@scrum_statistics[:error, :sizing_inconsistent] = !sizing_is_consistent
return @scrum_statistics
end
end
end
end
Project.send(:include, Backlogs::ProjectPatch) unless Project.included_modules.include? Backlogs::ProjectPatch