parent
1f7bfe7059
commit
d1b101261b
@ -1,14 +0,0 @@ |
||||
class RbStatisticsController < RbApplicationController |
||||
unloadable |
||||
|
||||
skip_before_filter :authorize |
||||
before_filter :authorize_global |
||||
|
||||
def show |
||||
@projects = EnabledModule.find(:all, |
||||
:conditions => ["enabled_modules.name = 'backlogs' and status = ?", Project::STATUS_ACTIVE], |
||||
:include => :project, |
||||
:joins => :project).collect { |mod| mod.project } |
||||
@projects.sort! {|a, b| a.scrum_statistics.score <=> b.scrum_statistics.score} |
||||
end |
||||
end |
@ -1,67 +0,0 @@ |
||||
<%- content_for :header_tags do %> |
||||
<%= stylesheet_link_tag 'rb_default/statistics', 'rb_default/jquery-ui', :plugin => 'redmine_backlogs' %> |
||||
<%= javascript_include_tag_backlogs 'lib/jquery.js', |
||||
'lib/jquery-ui.js' %> |
||||
<%- end %> |
||||
|
||||
<script> |
||||
var $j = jQuery.noConflict(); |
||||
|
||||
$j(document).ready(function(){ |
||||
$j("#projects").accordion({ collapsible: true, header: 'h3'}); |
||||
}); |
||||
</script> |
||||
|
||||
<div id="projects"> |
||||
<% global = {} %> |
||||
<% @projects.each do |project| %> |
||||
<% |
||||
stats = project.scrum_statistics |
||||
scores = stats.scores |
||||
errors = stats.errors('backlogs_') |
||||
info = stats.info |
||||
global = stats.merge(global, 'backlogs_') |
||||
%> |
||||
<h3><a href="#"><span class="score score_<%= stats.score %>"><%= stats.score %></span> <%= project.name %></a> </h3> |
||||
<div> |
||||
<% if !scores[:velocity_missing] %> |
||||
<%= l(:label_sprint_velocity, { :velocity => info[:velocity], :sprints => info[:closed_sprints].length, :days => info[:average_days_per_sprint]}) %><br/> |
||||
<% end %> |
||||
<h4><%= l(:backlogs_product_backlog) %></h4> |
||||
<%= link_to(l(:backlogs_product_backlog), { :controller => 'rb_queries', :action => 'show', :project_id => project }) %><br/> |
||||
<% if info[:active_sprint] || info[:closed_sprints] %> |
||||
<h4><%= l(:backlogs_sprints) %></h4> |
||||
<% if info[:active_sprint] %> |
||||
<%= link_to(info[:active_sprint].name, { |
||||
:controller => 'rb_queries', |
||||
:action => 'show', |
||||
:project_id => project, |
||||
:sprint_id => info[:active_sprint].id }) %> (<%= l(:backlogs_active) %>)<br/> |
||||
<% end %> |
||||
<% if info[:closed_sprints] %> |
||||
<% info[:closed_sprints].each do |sprint| %> |
||||
<%= link_to(sprint.name, { :controller => 'rb_queries', :action => 'show', :project_id => project, :sprint_id => sprint.id }) %><br/> |
||||
<% end %> |
||||
<% end %> |
||||
<% end %> |
||||
|
||||
<% errors.each do |e| %> |
||||
<ul> |
||||
<li><%= l(e) %></li> |
||||
</ul> |
||||
<% end %> |
||||
</div> |
||||
<% end %> |
||||
</div> |
||||
<% if @projects.size > 0 %> |
||||
<h3>Overall</h3> |
||||
<table> |
||||
<% global.each_pair do |e, n| %> |
||||
<tr> |
||||
<td><%= l(e) %></td> |
||||
<td><b><%= (n * 100) / @projects.size %>%</b></td> |
||||
<td><span class="score_<%= 10 - ((n * 10) / @projects.size) %>"> </td> |
||||
</tr> |
||||
<% end %> |
||||
</table> |
||||
<% end %> |
@ -1,42 +0,0 @@ |
||||
Feature: Scrum statistics |
||||
As a product owner |
||||
I want to see scrum statistics |
||||
So that I am able to check the performance of the team |
||||
|
||||
Background: |
||||
Given there is 1 project with: |
||||
| name | ecookbook | |
||||
And I am working in project "ecookbook" |
||||
And the project uses the following modules: |
||||
| backlogs | |
||||
And the backlogs module is initialized |
||||
And there is 1 user with: |
||||
| login | mathias | |
||||
And there is a role "product owner" |
||||
And the role "product owner" may have the following rights: |
||||
| view_scrum_statistics | |
||||
And the user "mathias" is a "product owner" |
||||
And there is 1 user with: |
||||
| login | andre | |
||||
And there is a role "visitor" |
||||
And the role "visitor" has no permissions |
||||
And the user "andre" is a "visitor" |
||||
|
||||
Scenario: View scrum statistics |
||||
Given the scrum statistics are enabled |
||||
And I am logged in as "mathias" |
||||
When I go to the home page |
||||
And I follow "Scrum statistics" |
||||
Then I should be on the scrum statistics page |
||||
|
||||
Scenario: Hide scrum statistics |
||||
Given the scrum statistics are enabled |
||||
And I am logged in as "andre" |
||||
When I go to the home page |
||||
Then I should not see "Scrum statistics" within "#main-menu" |
||||
|
||||
Scenario: Deactivate scrum statistics |
||||
Given the scrum statistics are disabled |
||||
And I am logged in as "mathias" |
||||
When I go to the home page |
||||
Then I should not see "Scrum statistics" within "#main-menu" |
@ -1,221 +0,0 @@ |
||||
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 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 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 |
Loading…
Reference in new issue