kanbanworkflowstimelinescrumrubyroadmapproject-planningproject-managementopenprojectangularissue-trackerifcgantt-chartganttbug-trackerboardsbcf
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.
151 lines
6.1 KiB
151 lines
6.1 KiB
require_dependency 'project'
|
|
|
|
module Backlogs
|
|
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
|
|
|
|
stats = {}
|
|
score = []
|
|
|
|
backlog = Story.product_backlog(self)[0,10]
|
|
|
|
if backlog.length == 0
|
|
score << l(:product_backlog_empty)
|
|
elsif backlog.inject(true) {|unsized, story| unsized && story.story_points.nil? }
|
|
score << l(:product_backlog_unsized)
|
|
else
|
|
score << nil
|
|
end
|
|
|
|
active = self.active_sprint
|
|
if active
|
|
stats[:active_sprint] = active
|
|
score <<
|
|
(Issue.exists?(["id <> root_id and estimated_hours is NULL and fixed_version_id =? and tracker_id = ?", active.id, Task.tracker]) ?
|
|
l(:active_sprint_unsized_stories) : nil)
|
|
score << (
|
|
Issue.exists?(["id <> root_id and estimated_hours is NULL and fixed_version_id = ? and tracker_id = ?", active.id, Task.tracker]) ?
|
|
l(:active_sprint_unestimated_tasks) : nil)
|
|
score << (!active.activity ? l(:active_sprint_dormant) : nil)
|
|
end
|
|
|
|
## base sprint stats on the last 5 closed sprints
|
|
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)
|
|
planned_velocity = nil
|
|
if sprints.length == 0
|
|
stats[:sprints] = []
|
|
else
|
|
stats[:sprints] = sprints
|
|
|
|
sprint_ids = sprints.collect{|s| "#{s.id}"}.join(',')
|
|
story_trackers = Story.trackers.collect{|s| "#{s.object_id}"}.join(',')
|
|
|
|
score << (
|
|
Issue.exists?(["id = root_id and story_points is NULL and fixed_version_id in (#{sprint_ids}) and tracker_id in (?)", Story.trackers]) ?
|
|
l(:unsized_stories, {:sprints => sprints.length}) : nil)
|
|
|
|
score << (
|
|
Issue.exists?(["id <> root_id and estimated_hours is NULL and fixed_version_id in (#{sprint_ids}) and tracker_id = ?", Task.tracker]) ?
|
|
l(:unestimated_tasks, {:sprints => sprints.length}) : nil)
|
|
|
|
## average points per hour over the selected sprints
|
|
points_per_hour = nil
|
|
res = Project.connection.execute("
|
|
select avg(story_points), avg(estimated_hours)
|
|
from issues
|
|
where not story_points is null
|
|
and fixed_version_id in (#{sprint_ids})
|
|
and id = root_id
|
|
and tracker_id in (#{story_trackers})
|
|
")
|
|
res.each {|p, h|
|
|
points_per_hour = ((1.0 * p) / h) if h && h != 0
|
|
}
|
|
|
|
accepted = 0
|
|
committed = 0
|
|
days = 0
|
|
pph_count = 0
|
|
pph_diff = 0
|
|
sprints.each {|sprint|
|
|
days += sprint.days.length
|
|
bd = sprint.burndown('up')
|
|
|
|
if bd
|
|
_accepted = bd.points_accepted || [0]
|
|
_committed = bd.points_committed || [0]
|
|
_remaining = bd.remaining_hours || [0]
|
|
|
|
accepted += _accepted[-1]
|
|
committed += _committed[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
|
|
end
|
|
}
|
|
if points_per_hour and pph_count > 0
|
|
pph_variance = (Integer(100 * (pph_diff / pph_count)) - 100).abs
|
|
score << (pph_variance > 10 ? l(:size_accuracy, {:pct => pph_variance}) : nil)
|
|
end
|
|
|
|
last_sprint = sprints[0]
|
|
score << (last_sprint.effective_date < -14.days.from_now.to_date ? l(:project_dormant) : nil) if !active
|
|
score << (!last_sprint.has_wiki_page ? l(:sprint_notes_missing) : nil)
|
|
|
|
stats[:average_days_per_sprint] = days / sprints.length
|
|
stats[:velocity] = accepted / sprints.length
|
|
planned_velocity = committed / sprints.length
|
|
stats[:days_per_point] = (stats[:average_days_per_sprint] * 1.0) / stats[:velocity] if stats[:velocity] > 0
|
|
end
|
|
|
|
stats[:velocity] ||= 0
|
|
score << (stats[:velocity] == 0 ? l(:no_velocity) : nil)
|
|
|
|
if stats[:velocity] > 0
|
|
planned_velocity ||= 0
|
|
mood = Integer((100.0 * planned_velocity) / stats[:velocity]) - 100
|
|
if mood > 10
|
|
score << l(:optimistic_velocity, {:pct => mood})
|
|
elsif mood < -10
|
|
score << l(:pessimistic_velocity, {:pct => mood})
|
|
else
|
|
score << nil
|
|
end
|
|
end
|
|
|
|
stats[:score] = {
|
|
:score => (10 * (score.size - score.compact.size)) / score.size,
|
|
:errors => score.compact
|
|
}
|
|
@scrum_statistics = stats
|
|
return @scrum_statistics
|
|
end
|
|
|
|
end
|
|
end
|
|
end
|
|
|
|
Project.send(:include, Backlogs::ProjectPatch) unless Project.included_modules.include? Backlogs::ProjectPatch
|
|
|