Bug #2285 » fix-parent-issue-done-by-issue-status.patch
app/models/issue.rb | ||
---|---|---|
if p.done_ratio_derived?
|
||
# done ratio = average ratio of children weighted with their total estimated hours
|
||
unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
|
||
children = p.children.to_a
|
||
if children.any?
|
||
child_with_total_estimated_hours = children.select {|c| c.total_estimated_hours.to_f > 0.0}
|
||
if child_with_total_estimated_hours.any?
|
||
average = Rational(
|
||
child_with_total_estimated_hours.sum(&:total_estimated_hours).to_s,
|
||
child_with_total_estimated_hours.count
|
||
)
|
||
else
|
||
average = Rational(1)
|
||
end
|
||
done = children.sum do |c|
|
||
estimated = Rational(c.total_estimated_hours.to_f.to_s)
|
||
estimated = average unless estimated > 0.0
|
||
ratio = c.closed? ? 100 : (c.done_ratio || 0)
|
||
estimated * ratio
|
||
end
|
||
progress = Rational(done, average * children.count)
|
||
p.done_ratio = progress.floor
|
||
children = p.children.to_a
|
||
if children.any?
|
||
child_with_total_estimated_hours = children.select {|c| c.total_estimated_hours.to_f > 0.0}
|
||
if child_with_total_estimated_hours.any?
|
||
average = Rational(
|
||
child_with_total_estimated_hours.sum(&:total_estimated_hours).to_s,
|
||
child_with_total_estimated_hours.count
|
||
)
|
||
else
|
||
average = Rational(1)
|
||
end
|
||
done = children.sum do |c|
|
||
estimated = Rational(c.total_estimated_hours.to_f.to_s)
|
||
estimated = average unless estimated > 0.0
|
||
ratio = c.closed? ? 100 : (c.done_ratio || 0)
|
||
estimated * ratio
|
||
end
|
||
progress = Rational(done, average * children.count)
|
||
p.done_ratio = progress.floor
|
||
if Issue.use_status_for_done_ratio?
|
||
calculate = Redmine::ParentIssueDone.new(p, progress.floor)
|
||
p.status = calculate.by_status
|
||
end
|
||
end
|
||
end
|
lib/redmine/parent_issue_done.rb | ||
---|---|---|
# frozen_string_literal: true
|
||
# Redmine - project management software
|
||
# Copyright (C) 2006- Jean-Philippe Lang
|
||
#
|
||
# This program is free software; you can redistribute it and/or
|
||
# modify it under the terms of the GNU General Public License
|
||
# as published by the Free Software Foundation; either version 2
|
||
# of the License, or (at your option) any later version.
|
||
#
|
||
# This program is distributed in the hope that it will be useful,
|
||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
# GNU General Public License for more details.
|
||
#
|
||
# You should have received a copy of the GNU General Public License
|
||
# along with this program; if not, write to the Free Software
|
||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||
module Redmine
|
||
class ParentIssueDone
|
||
def initialize(parent_issue, average_done_ratio)
|
||
self.parent = parent_issue
|
||
self.average = average_done_ratio
|
||
end
|
||
def by_status
|
||
return exact_status if exact_status.presence
|
||
parent.status = closest_status
|
||
end
|
||
private
|
||
attr_accessor :parent, :average
|
||
def exact_status
|
||
exact = parent_statuses.select { |status| status.default_done_ratio == average }
|
||
exact.uniq.first
|
||
end
|
||
def closest_status
|
||
# Compare the closest status candidates
|
||
candidates = []
|
||
candidates << parent_statuses[closest_index - 1] if closest_index > 0
|
||
if closest_index < parent_statuses.length
|
||
candidates << parent_statuses[closest_index]
|
||
candidates << parent_statuses[closest_index + 1]
|
||
end
|
||
# Find the closest status
|
||
candidates.min_by { |status| (status.default_done_ratio - average).abs }
|
||
end
|
||
def closest_index
|
||
@closest_index ||= closest_index_search
|
||
end
|
||
def closest_index_search
|
||
return if parent_statuses.empty?
|
||
low = 0
|
||
high = parent_statuses.length - 1
|
||
# Binary search to find the closest index
|
||
while low < high
|
||
mid = (low + high) / 2
|
||
ratio = parent_statuses[mid].default_done_ratio
|
||
if ratio < average
|
||
low = mid + 1
|
||
else
|
||
high = mid
|
||
end
|
||
end
|
||
# Now low is the index of the first element greater than or equal to average
|
||
low
|
||
end
|
||
def parent_statuses
|
||
@parent_statuses ||= trackers_issue_statuses
|
||
end
|
||
delegate :tracker, to: :parent
|
||
def trackers_issue_statuses
|
||
return unless tracker
|
||
# Make sure nil values of default_done_ratio are ignored
|
||
tracker
|
||
.issue_statuses
|
||
.select(&:default_done_ratio)
|
||
.sort_by(&:default_done_ratio)
|
||
end
|
||
end
|
||
end
|
test/unit/issue_subtasking_test.rb | ||
---|---|---|
end
|
||
end
|
||
def test_done_ratio_of_parent_should_reflect_children
|
||
def test_done_ratio_of_parent_should_reflect_children
|
||
root = Issue.generate!
|
||
child1 = root.generate_child!
|
||
child2 = child1.generate_child!
|
||
... | ... | |
assert_equal 0, child2.done_ratio
|
||
with_settings :issue_done_ratio => 'issue_status' do
|
||
ratio = 0
|
||
IssueStatus.all.each do |status|
|
||
status.update_attribute :default_done_ratio, ratio
|
||
ratio += 10
|
||
end
|
||
status = IssueStatus.find(4)
|
||
status.update_attribute :default_done_ratio, 50
|
||
child1.reload
|
||
child1.update_attribute :status, status
|
||
assert_equal 50, child1.done_ratio
|
||
assert_equal 30, child1.done_ratio
|
||
root.reload
|
||
assert_equal 30, root.done_ratio
|
||
assert_equal status.name, root.status.name
|
||
assert_equal 0, child2.done_ratio
|
||
end
|
||
end
|
||
def test_done_ratio_of_parent_should_map_closest_done_ratio_to_children_average
|
||
root = Issue.generate!
|
||
child1 = root.generate_child!
|
||
child2 = root.generate_child!
|
||
child3 = root.generate_child!
|
||
assert_equal 0, root.done_ratio
|
||
assert_equal 0, child1.done_ratio
|
||
assert_equal 0, child2.done_ratio
|
||
with_settings :issue_done_ratio => 'issue_status' do
|
||
ratio = 0
|
||
IssueStatus.all.each do |status|
|
||
status.update_attribute :default_done_ratio, ratio
|
||
ratio += 10
|
||
end
|
||
id = 1
|
||
root.children.each do |child|
|
||
status = IssueStatus.find(id)
|
||
child.reload
|
||
child.update_attribute :status, status
|
||
assert_equal status.default_done_ratio, child.done_ratio
|
||
id += 1
|
||
end
|
||
root.reload
|
||
assert_equal 50, root.done_ratio
|
||
expected_root_status = IssueStatus.find(2)
|
||
assert_equal expected_root_status.default_done_ratio, root.done_ratio
|
||
assert_equal expected_root_status.name, root.status.name
|
||
end
|
||
end
|
||
test/unit/lib/redmine/parent_issue_done_test.rb | ||
---|---|---|
# frozen_string_literal: true
|
||
# Redmine - project management software
|
||
# Copyright (C) 2006- Jean-Philippe Lang
|
||
#
|
||
# This program is free software; you can redistribute it and/or
|
||
# modify it under the terms of the GNU General Public License
|
||
# as published by the Free Software Foundation; either version 2
|
||
# of the License, or (at your option) any later version.
|
||
#
|
||
# This program is distributed in the hope that it will be useful,
|
||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
# GNU General Public License for more details.
|
||
#
|
||
# You should have received a copy of the GNU General Public License
|
||
# along with this program; if not, write to the Free Software
|
||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||
require_relative '../../../test_helper'
|
||
class Redmine::ParentIssueDoneTest < ActiveSupport::TestCase
|
||
fixtures :projects, :users, :roles, :members, :member_roles,
|
||
:trackers, :projects_trackers,
|
||
:issue_statuses, :issue_categories, :enumerations,
|
||
:issues,
|
||
:enabled_modules,
|
||
:workflows
|
||
def setup
|
||
User.current = nil
|
||
end
|
||
def test_should_ignore_nil_default_done_ratio_for_tracker_issue_statuses
|
||
issue = Issue.generate!
|
||
average_done_ratio = 0
|
||
status = IssueStatus.find(4)
|
||
ratio = 60
|
||
status.update_attribute :default_done_ratio, ratio
|
||
parent_issue_done = Redmine::ParentIssueDone.new(issue, average_done_ratio)
|
||
assert_equal 1, parent_issue_done.send(:trackers_issue_statuses).count
|
||
assert_equal ratio, parent_issue_done.send(:trackers_issue_statuses).first.default_done_ratio
|
||
end
|
||
def test_should_sort_tracker_issue_status_by_ascending_default_done_ratio
|
||
issue = Issue.generate!
|
||
average_done_ratio = 0
|
||
ratio = 0
|
||
IssueStatus.all.each do |status|
|
||
status.update_attribute :default_done_ratio, ratio
|
||
ratio += 10
|
||
end
|
||
parent_issue_done = Redmine::ParentIssueDone.new(issue, average_done_ratio)
|
||
sorted_ratios = parent_issue_done.send(:trackers_issue_statuses).map(&:default_done_ratio)
|
||
assert_equal [0, 10, 20, 30, 40, 50], sorted_ratios
|
||
end
|
||
def test_should_early_exit_closest_index_if_parent_statuses_are_empty
|
||
issue = Issue.generate!
|
||
average_done_ratio = 0
|
||
parent_issue_done = Redmine::ParentIssueDone.new(issue, average_done_ratio)
|
||
assert_nil parent_issue_done.send(:closest_index_search)
|
||
end
|
||
def test_should_find_closest_index
|
||
issue = Issue.generate!
|
||
average_done_ratio = 25.34
|
||
ratio = 0
|
||
IssueStatus.all.each do |status|
|
||
status.update_attribute :default_done_ratio, ratio
|
||
ratio += 10
|
||
end
|
||
parent_issue_done = Redmine::ParentIssueDone.new(issue, average_done_ratio)
|
||
index = parent_issue_done.send(:closest_index_search)
|
||
assert_equal 3, index
|
||
end
|
||
def test_should_find_closest_status
|
||
issue = Issue.generate!
|
||
average_done_ratio = 25.34
|
||
ratio = 0
|
||
IssueStatus.all.each do |status|
|
||
status.update_attribute :default_done_ratio, ratio
|
||
ratio += 10
|
||
end
|
||
parent_issue_done = Redmine::ParentIssueDone.new(issue, average_done_ratio)
|
||
closest_status = parent_issue_done.send(:closest_status)
|
||
status = IssueStatus.find(4)
|
||
assert_equal status, closest_status
|
||
end
|
||
def test_should_find_exact_status_if_any
|
||
issue = Issue.generate!
|
||
average_done_ratio = 20
|
||
ratio = 0
|
||
IssueStatus.all.each do |status|
|
||
status.update_attribute :default_done_ratio, ratio
|
||
ratio += 10
|
||
end
|
||
parent_issue_done = Redmine::ParentIssueDone.new(issue, average_done_ratio)
|
||
exact_status = parent_issue_done.send(:exact_status)
|
||
status = IssueStatus.find(3)
|
||
assert_equal status, exact_status
|
||
end
|
||
end
|
- « Previous
- 1
- 2
- Next »