You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
308 lines
10 KiB
Ruby
308 lines
10 KiB
Ruby
# Copyright © Emilio González Montaña
|
|
# Licence: Attribution & no derivatives
|
|
# * Attribution to the plugin web page URL should be done if you want to use it.
|
|
# https://redmine.ociotec.com/projects/redmine-plugin-scrum
|
|
# * No derivatives of this plugin (or partial) are allowed.
|
|
# Take a look to licence.txt file at plugin root folder for further details.
|
|
|
|
class Sprint < ActiveRecord::Base
|
|
|
|
belongs_to :user
|
|
belongs_to :project
|
|
has_many :issues, :dependent => :destroy
|
|
has_many :efforts, :class_name => "SprintEffort", :dependent => :destroy
|
|
scope :sorted, -> { order(fields_for_order_statement) }
|
|
scope :open, -> { where(:status => 'open') }
|
|
|
|
include Redmine::SafeAttributes
|
|
safe_attributes :name, :description, :sprint_start_date, :sprint_end_date, :status, :shared
|
|
|
|
SPRINT_STATUSES = %w(open closed)
|
|
|
|
validates_presence_of :name
|
|
validates_uniqueness_of :name, :scope => [:project_id]
|
|
validates_length_of :name, :maximum => 60
|
|
validates_presence_of :sprint_start_date, :unless => :is_product_backlog?
|
|
validates_presence_of :sprint_end_date, :unless => :is_product_backlog?
|
|
validates_inclusion_of :status, :in => SPRINT_STATUSES
|
|
|
|
def to_s
|
|
name
|
|
end
|
|
|
|
def is_product_backlog?
|
|
self.is_product_backlog
|
|
end
|
|
|
|
def pbis(options = {})
|
|
conditions = {:tracker_id => Scrum::Setting.pbi_tracker_ids,
|
|
:status_id => Scrum::Setting.pbi_status_ids}
|
|
order = "position ASC"
|
|
if options[:position_bellow]
|
|
first_issue = issues.where(conditions).order(order).first
|
|
first_position = first_issue ? first_issue.position : (options[:position_bellow] - 1)
|
|
last_position = options[:position_bellow] - 1
|
|
elsif options[:position_above]
|
|
last_issue = issues.where(conditions).order(order).last
|
|
first_position = options[:position_above] + 1
|
|
last_position = last_issue ? last_issue.position : (options[:position_above] + 1)
|
|
end
|
|
if options[:position_bellow] or options[:position_above]
|
|
if last_position < first_position
|
|
temp = last_position
|
|
last_position = first_position
|
|
first_position = temp
|
|
end
|
|
conditions[:position] = first_position..last_position
|
|
end
|
|
conditions[:project_id] = options[:filter_by_project] if options[:filter_by_project]
|
|
issues.where(conditions).order(order).select{|issue| issue.visible?}
|
|
end
|
|
|
|
def closed_pbis(options = {})
|
|
pbis(options).select {|pbi| pbi.scrum_closed?}
|
|
end
|
|
|
|
def not_closed_pbis(options = {})
|
|
pbis(options).select {|pbi| !pbi.scrum_closed?}
|
|
end
|
|
|
|
def story_points(options = {})
|
|
pbis(options).collect{|pbi| pbi.story_points.to_f}.sum
|
|
end
|
|
|
|
def closed_story_points(options = {})
|
|
pbis(options).collect{|pbi| pbi.closed_story_points}.sum
|
|
end
|
|
|
|
def scheduled_story_points(options = {})
|
|
pbis(options).select{|pbi| pbi.scheduled?}.collect{|pbi| pbi.story_points.to_f}.sum
|
|
end
|
|
|
|
def tasks(options = {})
|
|
modified_options = options.clone
|
|
conditions = {:tracker_id => Scrum::Setting.task_tracker_ids}
|
|
if modified_options[:filter_by_project]
|
|
conditions[:project_id] = modified_options[:filter_by_project]
|
|
modified_options.delete(:filter_by_project)
|
|
end
|
|
conditions.merge!(modified_options)
|
|
issues.where(conditions).select{|issue| issue.visible?}
|
|
end
|
|
|
|
def orphan_tasks
|
|
tasks(:parent_id => nil)
|
|
end
|
|
|
|
def estimated_hours(filter = {})
|
|
sum = 0.0
|
|
tasks(filter).each do |task|
|
|
if task.use_in_burndown?
|
|
pending_effort = task.pending_efforts.where(['date < ?', self.sprint_start_date]).order('date ASC').last
|
|
pending_effort = pending_effort.effort unless pending_effort.nil?
|
|
if (!(pending_effort.nil?))
|
|
sum += pending_effort
|
|
elsif (!((estimated_hours = task.estimated_hours).nil?))
|
|
sum += estimated_hours
|
|
end
|
|
end
|
|
end
|
|
return sum
|
|
end
|
|
|
|
def time_entries
|
|
tasks.collect{|task| task.time_entries}.flatten
|
|
end
|
|
|
|
def time_entries_by_activity
|
|
results = {}
|
|
total = 0.0
|
|
if User.current.allowed_to?(:view_sprint_stats, project)
|
|
time_entries.each do |time_entry|
|
|
if time_entry.activity and time_entry.hours > 0.0 and
|
|
time_entry.spent_on and sprint_start_date and sprint_end_date and
|
|
time_entry.spent_on >= sprint_start_date and time_entry.spent_on <= sprint_end_date
|
|
if !results.key?(time_entry.activity_id)
|
|
results[time_entry.activity_id] = {:activity => time_entry.activity, :total => 0.0}
|
|
end
|
|
results[time_entry.activity_id][:total] += time_entry.hours
|
|
total += time_entry.hours
|
|
end
|
|
end
|
|
results.values.each do |result|
|
|
result[:percentage] = ((result[:total] * 100.0) / total).round
|
|
end
|
|
end
|
|
return results.values, total
|
|
end
|
|
|
|
def time_entries_by_member
|
|
results = {}
|
|
total = 0.0
|
|
if User.current.allowed_to?(:view_sprint_stats_by_member, project)
|
|
time_entries.each do |time_entry|
|
|
if time_entry.activity and time_entry.hours > 0.0 and
|
|
time_entry.spent_on >= sprint_start_date and time_entry.spent_on <= sprint_end_date
|
|
if !results.key?(time_entry.user_id)
|
|
results[time_entry.user_id] = {:member => time_entry.user, :total => 0.0}
|
|
end
|
|
results[time_entry.user_id][:total] += time_entry.hours
|
|
total += time_entry.hours
|
|
end
|
|
end
|
|
results.values.each do |result|
|
|
result[:percentage] = ((result[:total] * 100.0) / total).round
|
|
end
|
|
end
|
|
results = results.values.sort{|a, b| a[:member] <=> b[:member]}
|
|
return results, total
|
|
end
|
|
|
|
def efforts_by_member
|
|
results = {}
|
|
total = 0.0
|
|
if User.current.allowed_to?(:view_sprint_stats_by_member, project)
|
|
efforts.each do |effort|
|
|
if effort.user and effort.effort > 0.0
|
|
if !results.key?(effort.user_id)
|
|
results[effort.user_id] = {:member => effort.user, :total => 0.0}
|
|
end
|
|
results[effort.user_id][:total] += effort.effort
|
|
total += effort.effort
|
|
end
|
|
end
|
|
results.values.each do |result|
|
|
result[:percentage] = ((result[:total] * 100.0) / total).round
|
|
end
|
|
end
|
|
results = results.values.sort{|a, b| a[:member] <=> b[:member]}
|
|
return results, total
|
|
end
|
|
|
|
def efforts_by_member_and_activity
|
|
results = {}
|
|
if User.current.allowed_to?(:view_sprint_stats_by_member, project)
|
|
members = Set.new
|
|
time_entries.each do |time_entry|
|
|
if time_entry.activity and time_entry.hours > 0.0 and
|
|
time_entry.spent_on >= sprint_start_date and time_entry.spent_on <= sprint_end_date
|
|
activity = time_entry.activity.name
|
|
member = time_entry.user.name
|
|
if !results.key?(activity)
|
|
results[activity] = {}
|
|
end
|
|
if !results[activity].key?(member)
|
|
results[activity][member] = 0.0
|
|
end
|
|
results[activity][member] += time_entry.hours
|
|
members << member
|
|
end
|
|
end
|
|
results.values.each do |data|
|
|
members.each do |member|
|
|
data[member] = 0.0 unless data.key?(member)
|
|
end
|
|
end
|
|
end
|
|
return results
|
|
end
|
|
|
|
def sps_by_pbi_category
|
|
return sps_by_pbi_field(:category_id, nil, :category, :name, nil, nil)
|
|
end
|
|
|
|
def sps_by_pbi_type
|
|
return sps_by_pbi_field(:tracker_id, nil, :tracker, :name, nil, nil)
|
|
end
|
|
|
|
def sps_by_pbi_creation_date
|
|
return sps_by_pbi_field(:created_on, :to_date, :created_on, :to_date, self.sprint_start_date,
|
|
l(:label_date_previous_to, :date => self.sprint_start_date))
|
|
end
|
|
|
|
def self.fields_for_order_statement(table = nil)
|
|
table ||= table_name
|
|
["(CASE WHEN #{table}.sprint_end_date IS NULL THEN 1 ELSE 0 END)",
|
|
"#{table}.sprint_end_date",
|
|
"#{table}.name",
|
|
"#{table}.id"]
|
|
end
|
|
|
|
def total_time
|
|
pbis.collect{|pbi| pbi.total_time}.compact.sum
|
|
end
|
|
|
|
def hours_per_story_point
|
|
sps = story_points
|
|
sps > 0 ? (total_time / sps).round(2) : 0.0
|
|
end
|
|
|
|
def closed?
|
|
status == 'closed'
|
|
end
|
|
|
|
def open?
|
|
status == 'open'
|
|
end
|
|
|
|
def get_dependencies
|
|
dependencies = []
|
|
pbis.each do |pbi|
|
|
pbi_dependencies = pbi.get_dependencies
|
|
dependencies << {:pbi => pbi, :dependencies => pbi_dependencies} if pbi_dependencies.count > 0
|
|
end
|
|
return dependencies
|
|
end
|
|
|
|
def completed_sps_by_day(filter = {})
|
|
days = {}
|
|
non_working_days = Setting.non_working_week_days.collect{|day| (day == '7') ? 0 : day.to_i}
|
|
end_date = self.sprint_end_date + 1
|
|
(self.sprint_start_date..end_date).each do |day|
|
|
if (day == end_date) or (!(non_working_days.include?(day.wday)))
|
|
days[day] = self.completed_sps_at_day(day, filter)
|
|
end
|
|
end
|
|
return days
|
|
end
|
|
|
|
def completed_sps_at_day(day, filter = {})
|
|
sps = self.pbis(filter).collect { |pbi| pbi.story_points_for_burdown(day) }.compact.sum
|
|
sps = 0.0 unless sps
|
|
return sps
|
|
end
|
|
|
|
private
|
|
|
|
def sps_by_pbi_field(field_id, subfield_id, field, subfield, field_min, label_min)
|
|
results = {}
|
|
total = 0.0
|
|
if User.current.allowed_to?(:view_sprint_stats, project)
|
|
pbis.each do |pbi|
|
|
pbi_story_points = pbi.story_points
|
|
if pbi_story_points
|
|
pbi_story_points = pbi_story_points.to_f
|
|
if pbi_story_points > 0.0
|
|
field_id_value = pbi.public_send(field_id)
|
|
field_id_value = field_id_value.public_send(subfield_id) unless field_id_value.nil? or subfield_id.nil? or !(field_id_value.respond_to?(subfield_id))
|
|
field_id_value = field_min unless field_min.nil? or (field_id_value > field_min)
|
|
if !results.key?(field_id_value)
|
|
field_value = pbi.public_send(field) unless !(pbi.respond_to?(field))
|
|
field_value = field_value.public_send(subfield) unless field_value.nil? or subfield.nil? or !(field_value.respond_to?(subfield))
|
|
field_value = label_min unless field_min.nil? or label_min.nil? or (field_value >= field_min)
|
|
results[field_id_value] = {field => field_value, :total => 0.0}
|
|
end
|
|
results[field_id_value][:total] += pbi_story_points
|
|
total += pbi_story_points
|
|
end
|
|
end
|
|
end
|
|
results.values.each do |result|
|
|
result[:percentage] = ((result[:total] * 100.0) / total).round
|
|
end
|
|
end
|
|
return results.values, total
|
|
end
|
|
|
|
end
|