Project

General

Profile

Bug #2285 » fix-parent-issue-done-by-issue-status.patch

liaham, 08/05/2024 11:36 AM

View differences:

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
(2-2/2)