Patch #13400 » 0001-Calculate-done_ratio-based-on-logged-time.patch
| app/models/issue.rb | ||
|---|---|---|
| 52 | 52 |
acts_as_activity_provider :scope => preload(:project, :author, :tracker, :status), |
| 53 | 53 |
:author_key => :author_id |
| 54 | 54 | |
| 55 |
DONE_RATIO_OPTIONS = %w(issue_field issue_status) |
|
| 55 |
DONE_RATIO_OPTIONS = %w(issue_field issue_status logged_time)
|
|
| 56 | 56 | |
| 57 | 57 |
attr_accessor :deleted_attachment_ids |
| 58 | 58 |
attr_reader :current_journal |
| ... | ... | |
| 106 | 106 | |
| 107 | 107 |
before_validation :default_assign, on: :create |
| 108 | 108 |
before_validation :clear_disabled_fields |
| 109 |
before_save :close_duplicates, :update_done_ratio_from_issue_status,
|
|
| 109 |
before_save :close_duplicates, :update_done_ratio, |
|
| 110 | 110 |
:force_updated_on_change, :update_closed_on |
| 111 | 111 |
after_save {|issue| issue.send :after_project_change if !issue.saved_change_to_id? && issue.saved_change_to_project_id?}
|
| 112 | 112 |
after_save :reschedule_following_issues, :update_nested_set_attributes, |
| ... | ... | |
| 687 | 687 |
private :workflow_rule_by_attribute |
| 688 | 688 | |
| 689 | 689 |
def done_ratio |
| 690 |
if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
|
|
| 690 |
if use_status_for_done_ratio?
|
|
| 691 | 691 |
status.default_done_ratio |
| 692 | ||
| 693 |
elsif use_time_for_done_ratio? |
|
| 694 |
ratio = |
|
| 695 |
if done_ratio_derived? && total_estimated_hours.to_f > 0 |
|
| 696 |
(total_spent_hours.to_f / total_estimated_hours.to_f) |
|
| 697 | ||
| 698 |
elsif !done_ratio_derived? && estimated_hours.to_f > 0 |
|
| 699 |
(spent_hours.to_f / estimated_hours.to_f) |
|
| 700 | ||
| 701 |
else |
|
| 702 |
0.0 |
|
| 703 |
end |
|
| 704 | ||
| 705 |
[ratio * 100, 100].min.to_i |
|
| 692 | 706 |
else |
| 693 | 707 |
read_attribute(:done_ratio) |
| 694 | 708 |
end |
| ... | ... | |
| 697 | 711 |
def self.use_status_for_done_ratio? |
| 698 | 712 |
Setting.issue_done_ratio == 'issue_status' |
| 699 | 713 |
end |
| 714 |
def use_status_for_done_ratio? |
|
| 715 |
Issue.use_status_for_done_ratio? && status && status.default_done_ratio |
|
| 716 |
end |
|
| 717 | ||
| 718 |
def self.use_time_for_done_ratio? |
|
| 719 |
Setting.issue_done_ratio == 'logged_time' |
|
| 720 |
end |
|
| 721 |
def use_time_for_done_ratio? |
|
| 722 |
Issue.use_time_for_done_ratio? |
|
| 723 |
end |
|
| 700 | 724 | |
| 701 | 725 |
def self.use_field_for_done_ratio? |
| 702 | 726 |
Setting.issue_done_ratio == 'issue_field' |
| 703 | 727 |
end |
| 728 |
def use_field_for_done_ratio? |
|
| 729 |
!(use_status_for_done_ratio? || use_time_for_done_ratio?) |
|
| 730 |
end |
|
| 704 | 731 | |
| 705 | 732 |
def validate_issue |
| 706 | 733 |
if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date |
| ... | ... | |
| 799 | 826 | |
| 800 | 827 |
# Set the done_ratio using the status if that setting is set. This will keep the done_ratios |
| 801 | 828 |
# even if the user turns off the setting later |
| 802 |
def update_done_ratio_from_issue_status
|
|
| 803 |
if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
|
|
| 804 |
self.done_ratio = status.default_done_ratio
|
|
| 829 |
def update_done_ratio |
|
| 830 |
unless use_field_for_done_ratio?
|
|
| 831 |
self.done_ratio = self.done_ratio
|
|
| 805 | 832 |
end |
| 806 | 833 |
end |
| 807 | 834 | |
| 835 |
def update_done_ratio! |
|
| 836 |
self.init_journal(User.current, "") |
|
| 837 |
self.update_done_ratio |
|
| 838 |
self.save |
|
| 839 |
end |
|
| 840 | ||
| 808 | 841 |
def init_journal(user, notes = "") |
| 809 | 842 |
@current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes) |
| 810 | 843 |
end |
| ... | ... | |
| 1702 | 1735 | |
| 1703 | 1736 |
if p.done_ratio_derived? |
| 1704 | 1737 |
# done ratio = average ratio of children weighted with their total estimated hours |
| 1705 |
unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
|
|
| 1738 |
if p.use_field_for_done_ratio?
|
|
| 1706 | 1739 |
children = p.children.to_a |
| 1707 | 1740 |
if children.any? |
| 1708 | 1741 |
child_with_total_estimated_hours = children.select {|c| c.total_estimated_hours.to_f > 0.0}
|
| app/models/time_entry.rb | ||
|---|---|---|
| 46 | 46 |
validates_length_of :comments, :maximum => 1024, :allow_nil => true |
| 47 | 47 |
validates :spent_on, :date => true |
| 48 | 48 |
before_validation :set_project_if_nil |
| 49 |
after_save :update_done_ratio |
|
| 50 |
after_destroy :update_done_ratio |
|
| 49 | 51 |
validate :validate_time_entry |
| 50 | 52 | |
| 51 | 53 |
scope :visible, lambda {|*args|
|
| ... | ... | |
| 138 | 140 |
errors.add :activity_id, :inclusion if activity_id_changed? && project && !project.activities.include?(activity) |
| 139 | 141 |
end |
| 140 | 142 | |
| 143 |
def update_done_ratio |
|
| 144 |
if issue && Issue.use_time_for_done_ratio? |
|
| 145 |
# Only create a new journal for this update if we don't have any other |
|
| 146 |
# changes pending on the issue. In that case, we will save the issue |
|
| 147 |
# later anyway (which we thus expect here and don't save the issue |
|
| 148 |
# ourselfes) |
|
| 149 |
if issue.changed? |
|
| 150 |
issue.update_done_ratio |
|
| 151 |
else |
|
| 152 |
issue.update_done_ratio! |
|
| 153 |
end |
|
| 154 |
end |
|
| 155 |
end |
|
| 156 | ||
| 141 | 157 |
def hours=(h) |
| 142 | 158 |
write_attribute :hours, (h.is_a?(String) ? (h.to_hours || h) : h) |
| 143 | 159 |
end |
| config/locales/de.yml | ||
|---|---|---|
| 1011 | 1011 |
setting_issue_done_ratio: Berechne den Ticket-Fortschritt mittels |
| 1012 | 1012 |
setting_issue_done_ratio_issue_field: Ticket-Feld % erledigt |
| 1013 | 1013 |
setting_issue_done_ratio_issue_status: Ticket-Status |
| 1014 |
setting_issue_done_ratio_logged_time: geschätzter und gebuchter Zeit |
|
| 1014 | 1015 |
setting_issue_group_assignment: Ticketzuweisung an Gruppen erlauben |
| 1015 | 1016 |
setting_issue_list_default_columns: Standard-Spalten in der Ticket-Auflistung |
| 1016 | 1017 |
setting_issues_export_limit: Max. Anzahl Tickets bei CSV/PDF-Export |
| config/locales/en.yml | ||
|---|---|---|
| 435 | 435 |
setting_issue_done_ratio: Calculate the issue done ratio with |
| 436 | 436 |
setting_issue_done_ratio_issue_field: Use the issue field |
| 437 | 437 |
setting_issue_done_ratio_issue_status: Use the issue status |
| 438 |
setting_issue_done_ratio_logged_time: Use the logged and estimated time |
|
| 438 | 439 |
setting_start_of_week: Start calendars on |
| 439 | 440 |
setting_rest_api_enabled: Enable REST web service |
| 440 | 441 |
setting_cache_formatted_text: Cache formatted text |
| test/unit/issue_subtasking_test.rb | ||
|---|---|---|
| 21 | 21 |
fixtures :projects, :users, :roles, :members, :member_roles, |
| 22 | 22 |
:trackers, :projects_trackers, |
| 23 | 23 |
:issue_statuses, :issue_categories, :enumerations, |
| 24 |
:issues, |
|
| 24 |
:issues, :time_entries,
|
|
| 25 | 25 |
:enabled_modules, |
| 26 | 26 |
:workflows |
| 27 | 27 | |
| ... | ... | |
| 168 | 168 |
end |
| 169 | 169 |
end |
| 170 | 170 | |
| 171 |
def test_parent_done_ratio_via_logged_time_if_set_to_derived |
|
| 172 |
with_settings :parent_issue_done_ratio => 'derived', :issue_done_ratio => 'logged_time' do |
|
| 173 |
parent = Issue.generate!(:estimated_hours => 2) |
|
| 174 |
TimeEntry.generate!(:issue => parent, :hours => 2) |
|
| 175 | ||
| 176 |
child = parent.generate_child!(:estimated_hours => 8) |
|
| 177 |
TimeEntry.generate!(:issue => child, :hours => 2) |
|
| 178 | ||
| 179 |
# Estimated time: 10h, Spent time: 4h => 40 % done |
|
| 180 |
assert_equal 40, parent.reload.done_ratio |
|
| 181 |
end |
|
| 182 |
end |
|
| 183 | ||
| 184 |
def test_parent_done_ratio_via_logged_time_if_set_to_independent |
|
| 185 |
with_settings :parent_issue_done_ratio => 'independent', :issue_done_ratio => 'logged_time' do |
|
| 186 |
parent = Issue.generate!(:estimated_hours => 2) |
|
| 187 |
TimeEntry.generate!(:issue => parent, :hours => 2) |
|
| 188 | ||
| 189 |
child = parent.generate_child!(:estimated_hours => 8) |
|
| 190 |
TimeEntry.generate!(:issue => child, :hours => 2) |
|
| 191 | ||
| 192 |
# Estimated time: 2h, Spent time: 2h => 100 % done |
|
| 193 |
assert_equal 100, parent.reload.done_ratio |
|
| 194 |
end |
|
| 195 |
end |
|
| 196 | ||
| 197 |
def test_parent_done_ratio_via_logged_time_does_not_exceed_100 |
|
| 198 |
with_settings :parent_issue_done_ratio => 'derived', :issue_done_ratio => 'logged_time' do |
|
| 199 |
parent = Issue.generate!(:estimated_hours => 2) |
|
| 200 |
TimeEntry.generate!(:issue => parent, :hours => 2) |
|
| 201 | ||
| 202 |
child = parent.generate_child!(:estimated_hours => 2) |
|
| 203 |
TimeEntry.generate!(:issue => child, :hours => 8) |
|
| 204 | ||
| 205 |
# Estimated time: 4h, Spent time: 10h => 250 % == 100 % done |
|
| 206 |
assert_equal 100, parent.reload.done_ratio |
|
| 207 |
end |
|
| 208 |
end |
|
| 209 | ||
| 171 | 210 |
def test_parent_done_ratio_should_be_weighted_by_estimated_times_if_any |
| 172 | 211 |
with_settings :parent_issue_done_ratio => 'derived' do |
| 173 | 212 |
parent = Issue.generate! |
| test/unit/issue_test.rb | ||
|---|---|---|
| 2730 | 2730 |
end |
| 2731 | 2731 |
end |
| 2732 | 2732 | |
| 2733 |
test "#update_done_ratio_from_issue_status should update done_ratio according to Setting.issue_done_ratio" do
|
|
| 2733 |
test "#update_done_ratio should update done_ratio according to Setting.issue_done_ratio" do |
|
| 2734 | 2734 |
@issue = Issue.find(1) |
| 2735 |
@issue.update!(:estimated_hours => 308.5) |
|
| 2735 | 2736 |
@issue_status = IssueStatus.find(1) |
| 2736 | 2737 |
@issue_status.update!(:default_done_ratio => 50) |
| 2738 | ||
| 2737 | 2739 |
@issue2 = Issue.find(2) |
| 2738 | 2740 |
@issue_status2 = IssueStatus.find(2) |
| 2739 | 2741 |
@issue_status2.update!(:default_done_ratio => 0) |
| 2740 | 2742 | |
| 2741 | 2743 |
with_settings :issue_done_ratio => 'issue_field' do |
| 2742 |
@issue.update_done_ratio_from_issue_status
|
|
| 2743 |
@issue2.update_done_ratio_from_issue_status
|
|
| 2744 |
@issue.update_done_ratio |
|
| 2745 |
@issue2.update_done_ratio |
|
| 2744 | 2746 | |
| 2745 | 2747 |
assert_equal 0, @issue.read_attribute(:done_ratio) |
| 2746 | 2748 |
assert_equal 30, @issue2.read_attribute(:done_ratio) |
| 2747 | 2749 |
end |
| 2748 | 2750 | |
| 2749 | 2751 |
with_settings :issue_done_ratio => 'issue_status' do |
| 2750 |
@issue.update_done_ratio_from_issue_status
|
|
| 2751 |
@issue2.update_done_ratio_from_issue_status
|
|
| 2752 |
@issue.update_done_ratio |
|
| 2753 |
@issue2.update_done_ratio |
|
| 2752 | 2754 | |
| 2753 | 2755 |
assert_equal 50, @issue.read_attribute(:done_ratio) |
| 2754 | 2756 |
assert_equal 0, @issue2.read_attribute(:done_ratio) |
| 2755 | 2757 |
end |
| 2758 | ||
| 2759 |
with_settings :issue_done_ratio => 'logged_time' do |
|
| 2760 |
@issue.update_done_ratio |
|
| 2761 |
@issue2.update_done_ratio |
|
| 2762 | ||
| 2763 |
assert_equal 50, @issue.read_attribute(:done_ratio) |
|
| 2764 |
assert_equal 0, @issue2.read_attribute(:done_ratio) |
|
| 2765 |
end |
|
| 2766 | ||
| 2756 | 2767 |
end |
| 2757 | 2768 | |
| 2758 | 2769 |
test "#by_tracker" do |
| test/unit/time_entry_test.rb | ||
|---|---|---|
| 212 | 212 |
assert_equal ["Comment cannot be blank", "Issue cannot be blank"], entry.errors.full_messages.sort |
| 213 | 213 |
end |
| 214 | 214 |
end |
| 215 | ||
| 216 |
def test_create_updates_issues_done_ratio |
|
| 217 |
with_settings :issue_done_ratio => 'logged_time' do |
|
| 218 |
issue = Issue.generate!(:estimated_hours => 10) |
|
| 219 |
assert_equal 0, issue.done_ratio |
|
| 220 | ||
| 221 |
TimeEntry.generate!(:issue => issue, :hours => 5) |
|
| 222 |
assert_equal 50, issue.reload.done_ratio |
|
| 223 |
end |
|
| 224 |
end |
|
| 225 | ||
| 226 |
def test_update_updates_issues_done_ratio |
|
| 227 |
with_settings :issue_done_ratio => 'logged_time' do |
|
| 228 |
issue = Issue.generate!(:estimated_hours => 10) |
|
| 229 | ||
| 230 |
te = TimeEntry.generate!(:issue => issue, :hours => 5) |
|
| 231 |
assert_equal 50, issue.reload.done_ratio |
|
| 232 | ||
| 233 |
te.update_attribute(:hours, 10) |
|
| 234 |
assert_equal 100, issue.reload.done_ratio |
|
| 235 |
end |
|
| 236 |
end |
|
| 237 | ||
| 238 |
def test_destroy_updates_issues_done_ratio |
|
| 239 |
with_settings :issue_done_ratio => 'logged_time' do |
|
| 240 |
issue = Issue.generate!(:estimated_hours => 10) |
|
| 241 | ||
| 242 |
te = TimeEntry.generate!(:issue => issue, :hours => 5) |
|
| 243 |
assert_equal 50, issue.reload.done_ratio |
|
| 244 | ||
| 245 |
te.destroy |
|
| 246 |
assert_equal 0, issue.reload.done_ratio |
|
| 247 |
end |
|
| 248 |
end |
|
| 215 | 249 |
end |