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 |