##// END OF EJS Templates
Adds settings to control start/due dates and priority on parent tasks (#5490)....
Jean-Philippe Lang -
r13887:0ad09e3aef86
parent child
Show More
@@ -0,0 +1,146
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.expand_path('../../test_helper', __FILE__)
19
20 class IssueSubtaskingTest < ActiveSupport::TestCase
21 fixtures :projects, :users, :roles, :members, :member_roles,
22 :trackers, :projects_trackers,
23 :issue_statuses, :issue_categories, :enumerations,
24 :issues
25
26 def test_leaf_planning_fields_should_be_editable
27 issue = Issue.generate!
28 user = User.find(1)
29 %w(priority_id done_ratio start_date due_date estimated_hours).each do |attribute|
30 assert issue.safe_attribute?(attribute, user)
31 end
32 end
33
34 def test_parent_dates_should_be_read_only_with_parent_issue_dates_set_to_derived
35 with_settings :parent_issue_dates => 'derived' do
36 issue = Issue.generate_with_child!
37 user = User.find(1)
38 %w(start_date due_date).each do |attribute|
39 assert !issue.safe_attribute?(attribute, user)
40 end
41 end
42 end
43
44 def test_parent_dates_should_be_lowest_start_and_highest_due_dates_with_parent_issue_dates_set_to_derived
45 with_settings :parent_issue_dates => 'derived' do
46 parent = Issue.generate!
47 parent.generate_child!(:start_date => '2010-01-25', :due_date => '2010-02-15')
48 parent.generate_child!( :due_date => '2010-02-13')
49 parent.generate_child!(:start_date => '2010-02-01', :due_date => '2010-02-22')
50 parent.reload
51 assert_equal Date.parse('2010-01-25'), parent.start_date
52 assert_equal Date.parse('2010-02-22'), parent.due_date
53 end
54 end
55
56 def test_reschuling_a_parent_should_reschedule_subtasks_with_parent_issue_dates_set_to_derived
57 with_settings :parent_issue_dates => 'derived' do
58 parent = Issue.generate!
59 c1 = parent.generate_child!(:start_date => '2010-05-12', :due_date => '2010-05-18')
60 c2 = parent.generate_child!(:start_date => '2010-06-03', :due_date => '2010-06-10')
61 parent.reload.reschedule_on!(Date.parse('2010-06-02'))
62 c1.reload
63 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-08')], [c1.start_date, c1.due_date]
64 c2.reload
65 assert_equal [Date.parse('2010-06-03'), Date.parse('2010-06-10')], [c2.start_date, c2.due_date] # no change
66 parent.reload
67 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-10')], [parent.start_date, parent.due_date]
68 end
69 end
70
71 def test_parent_priority_should_be_read_only_with_parent_issue_priority_set_to_derived
72 with_settings :parent_issue_priority => 'derived' do
73 issue = Issue.generate_with_child!
74 user = User.find(1)
75 assert !issue.safe_attribute?('priority_id', user)
76 end
77 end
78
79 def test_parent_priority_should_be_the_highest_child_priority
80 with_settings :parent_issue_priority => 'derived' do
81 parent = Issue.generate!(:priority => IssuePriority.find_by_name('Normal'))
82 # Create children
83 child1 = parent.generate_child!(:priority => IssuePriority.find_by_name('High'))
84 assert_equal 'High', parent.reload.priority.name
85 child2 = child1.generate_child!(:priority => IssuePriority.find_by_name('Immediate'))
86 assert_equal 'Immediate', child1.reload.priority.name
87 assert_equal 'Immediate', parent.reload.priority.name
88 child3 = parent.generate_child!(:priority => IssuePriority.find_by_name('Low'))
89 assert_equal 'Immediate', parent.reload.priority.name
90 # Destroy a child
91 child1.destroy
92 assert_equal 'Low', parent.reload.priority.name
93 # Update a child
94 child3.reload.priority = IssuePriority.find_by_name('Normal')
95 child3.save!
96 assert_equal 'Normal', parent.reload.priority.name
97 end
98 end
99
100 def test_parent_dates_should_be_editable_with_parent_issue_dates_set_to_independent
101 with_settings :parent_issue_dates => 'independent' do
102 issue = Issue.generate_with_child!
103 user = User.find(1)
104 %w(start_date due_date).each do |attribute|
105 assert issue.safe_attribute?(attribute, user)
106 end
107 end
108 end
109
110 def test_parent_dates_should_not_be_updated_with_parent_issue_dates_set_to_independent
111 with_settings :parent_issue_dates => 'independent' do
112 parent = Issue.generate!(:start_date => '2015-07-01', :due_date => '2015-08-01')
113 parent.generate_child!(:start_date => '2015-06-01', :due_date => '2015-09-01')
114 parent.reload
115 assert_equal Date.parse('2015-07-01'), parent.start_date
116 assert_equal Date.parse('2015-08-01'), parent.due_date
117 end
118 end
119
120 def test_reschuling_a_parent_should_not_reschedule_subtasks_with_parent_issue_dates_set_to_independent
121 with_settings :parent_issue_dates => 'independent' do
122 parent = Issue.generate!(:start_date => '2010-05-01', :due_date => '2010-05-20')
123 c1 = parent.generate_child!(:start_date => '2010-05-12', :due_date => '2010-05-18')
124 parent.reload.reschedule_on!(Date.parse('2010-06-01'))
125 assert_equal Date.parse('2010-06-01'), parent.reload.start_date
126 c1.reload
127 assert_equal [Date.parse('2010-05-12'), Date.parse('2010-05-18')], [c1.start_date, c1.due_date]
128 end
129 end
130
131 def test_parent_priority_should_be_editable_with_parent_issue_priority_set_to_independent
132 with_settings :parent_issue_priority => 'independent' do
133 issue = Issue.generate_with_child!
134 user = User.find(1)
135 assert issue.safe_attribute?('priority_id', user)
136 end
137 end
138
139 def test_parent_priority_should_not_be_updated_with_parent_issue_priority_set_to_independent
140 with_settings :parent_issue_priority => 'independent' do
141 parent = Issue.generate!(:priority => IssuePriority.find_by_name('Normal'))
142 child1 = parent.generate_child!(:priority => IssuePriority.find_by_name('High'))
143 assert_equal 'Normal', parent.reload.priority.name
144 end
145 end
146 end
@@ -79,7 +79,12 module SettingsHelper
79 79
80 80 def setting_label(setting, options={})
81 81 label = options.delete(:label)
82 label != false ? label_tag("settings_#{setting}", l(label || "setting_#{setting}"), options[:label_options]).html_safe : ''
82 if label == false
83 ''
84 else
85 text = label.is_a?(String) ? label : l(label || "setting_#{setting}")
86 label_tag("settings_#{setting}", text, options[:label_options])
87 end
83 88 end
84 89
85 90 # Renders a notification field for a Redmine::Notifiable option
@@ -126,6 +131,24 module SettingsHelper
126 131 options.map {|label, value| [l(label), value.to_s]}
127 132 end
128 133
134 def parent_issue_dates_options
135 options = [
136 [:label_parent_task_attributes_derived, 'derived'],
137 [:label_parent_task_attributes_independent, 'independent']
138 ]
139
140 options.map {|label, value| [l(label), value.to_s]}
141 end
142
143 def parent_issue_priority_options
144 options = [
145 [:label_parent_task_attributes_derived, 'derived'],
146 [:label_parent_task_attributes_independent, 'independent']
147 ]
148
149 options.map {|label, value| [l(label), value.to_s]}
150 end
151
129 152 # Returns the options for the date_format setting
130 153 def date_format_setting_options(locale)
131 154 Setting::DATE_FORMATS.map do |f|
@@ -426,6 +426,15 class Issue < ActiveRecord::Base
426 426 # Make sure that project_id can always be set for new issues
427 427 names |= %w(project_id)
428 428 end
429 if dates_derived?
430 names -= %w(start_date due_date)
431 end
432 if priority_derived?
433 names -= %w(priority_id)
434 end
435 unless leaf?
436 names -= %w(done_ratio estimated_hours)
437 end
429 438 names
430 439 end
431 440
@@ -463,10 +472,6 class Issue < ActiveRecord::Base
463 472 attrs = delete_unsafe_attributes(attrs, user)
464 473 return if attrs.empty?
465 474
466 unless leaf?
467 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
468 end
469
470 475 if attrs['parent_issue_id'].present?
471 476 s = attrs['parent_issue_id'].to_s
472 477 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
@@ -1094,11 +1099,15 class Issue < ActiveRecord::Base
1094 1099 end
1095 1100
1096 1101 def soonest_start(reload=false)
1097 @soonest_start = nil if reload
1098 @soonest_start ||= (
1099 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
1100 [(@parent_issue || parent).try(:soonest_start)]
1101 ).compact.max
1102 if @soonest_start.nil? || reload
1103 dates = relations_to(reload).collect{|relation| relation.successor_soonest_start}
1104 p = @parent_issue || parent
1105 if p && Setting.parent_issue_dates == 'derived'
1106 dates << p.soonest_start
1107 end
1108 @soonest_start = dates.compact.max
1109 end
1110 @soonest_start
1102 1111 end
1103 1112
1104 1113 # Sets start_date on the given date or the next working day
@@ -1114,7 +1123,7 class Issue < ActiveRecord::Base
1114 1123 # If the issue is a parent task, this is done by rescheduling its subtasks.
1115 1124 def reschedule_on!(date)
1116 1125 return if date.nil?
1117 if leaf?
1126 if leaf? || !dates_derived?
1118 1127 if start_date.nil? || start_date != date
1119 1128 if start_date && start_date > date
1120 1129 # Issue can not be moved earlier than its soonest start date
@@ -1144,6 +1153,14 class Issue < ActiveRecord::Base
1144 1153 end
1145 1154 end
1146 1155
1156 def dates_derived?
1157 !leaf? && Setting.parent_issue_dates == 'derived'
1158 end
1159
1160 def priority_derived?
1161 !leaf? && Setting.parent_issue_priority == 'derived'
1162 end
1163
1147 1164 def <=>(issue)
1148 1165 if issue.nil?
1149 1166 -1
@@ -1430,16 +1447,20 class Issue < ActiveRecord::Base
1430 1447
1431 1448 def recalculate_attributes_for(issue_id)
1432 1449 if issue_id && p = Issue.find_by_id(issue_id)
1433 # priority = highest priority of children
1434 if priority_position = p.children.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1435 p.priority = IssuePriority.find_by_position(priority_position)
1450 if p.priority_derived?
1451 # priority = highest priority of children
1452 if priority_position = p.children.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1453 p.priority = IssuePriority.find_by_position(priority_position)
1454 end
1436 1455 end
1437 1456
1438 # start/due dates = lowest/highest dates of children
1439 p.start_date = p.children.minimum(:start_date)
1440 p.due_date = p.children.maximum(:due_date)
1441 if p.start_date && p.due_date && p.due_date < p.start_date
1442 p.start_date, p.due_date = p.due_date, p.start_date
1457 if p.dates_derived?
1458 # start/due dates = lowest/highest dates of children
1459 p.start_date = p.children.minimum(:start_date)
1460 p.due_date = p.children.maximum(:due_date)
1461 if p.start_date && p.due_date && p.due_date < p.start_date
1462 p.start_date, p.due_date = p.due_date, p.start_date
1463 end
1443 1464 end
1444 1465
1445 1466 # done ratio = weighted average ratio of leaves
@@ -48,25 +48,23
48 48
49 49 <% if @issue.safe_attribute? 'start_date' %>
50 50 <p id="start_date_area">
51 <%= f.text_field(:start_date, :size => 10, :disabled => !@issue.leaf?,
52 :required => @issue.required_attribute?('start_date')) %>
51 <%= f.text_field(:start_date, :size => 10, :required => @issue.required_attribute?('start_date')) %>
53 52 <%= calendar_for('issue_start_date') if @issue.leaf? %>
54 53 </p>
55 54 <% end %>
56 55
57 56 <% if @issue.safe_attribute? 'due_date' %>
58 57 <p id="due_date_area">
59 <%= f.text_field(:due_date, :size => 10, :disabled => !@issue.leaf?,
60 :required => @issue.required_attribute?('due_date')) %>
58 <%= f.text_field(:due_date, :size => 10, :required => @issue.required_attribute?('due_date')) %>
61 59 <%= calendar_for('issue_due_date') if @issue.leaf? %>
62 60 </p>
63 61 <% end %>
64 62
65 63 <% if @issue.safe_attribute? 'estimated_hours' %>
66 <p><%= f.text_field :estimated_hours, :size => 3, :disabled => !@issue.leaf?, :required => @issue.required_attribute?('estimated_hours') %> <%= l(:field_hours) %></p>
64 <p><%= f.text_field :estimated_hours, :size => 3, :required => @issue.required_attribute?('estimated_hours') %> <%= l(:field_hours) %></p>
67 65 <% end %>
68 66
69 <% if @issue.safe_attribute?('done_ratio') && @issue.leaf? && Issue.use_field_for_done_ratio? %>
67 <% if @issue.safe_attribute?('done_ratio') && Issue.use_field_for_done_ratio? %>
70 68 <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }), :required => @issue.required_attribute?('done_ratio') %></p>
71 69 <% end %>
72 70 </div>
@@ -23,6 +23,15
23 23 </div>
24 24
25 25 <fieldset class="box">
26 <legend><%= l(:label_parent_task_attributes) %></legend>
27 <div class="tabular settings">
28 <p><%= setting_select :parent_issue_dates, parent_issue_dates_options, :label => "#{l(:field_start_date)} / #{l(:field_due_date)}" %></p>
29
30 <p><%= setting_select :parent_issue_priority, parent_issue_priority_options, :label => :field_priority %></p>
31 </div>
32 </fieldset>
33
34 <fieldset class="box">
26 35 <legend><%= l(:setting_issue_list_default_columns) %></legend>
27 36 <%= render_query_columns_selection(
28 37 IssueQuery.new(:column_names => Setting.issue_list_default_columns),
@@ -941,6 +941,9 en:
941 941 label_enable_notifications: Enable notifications
942 942 label_disable_notifications: Disable notifications
943 943 label_blank_value: blank
944 label_parent_task_attributes: Parent tasks attributes
945 label_parent_task_attributes_derived: Calculated from subtasks
946 label_parent_task_attributes_independent: Independent of subtasks
944 947
945 948 button_login: Login
946 949 button_submit: Submit
@@ -961,6 +961,7 fr:
961 961 label_enable_notifications: Activer les notifications
962 962 label_disable_notifications: DΓ©sactiver les notifications
963 963 label_blank_value: non renseignΓ©
964 label_parent_task_attributes: Attributs des tΓ’ches parentes
964 965
965 966 button_login: Connexion
966 967 button_submit: Soumettre
@@ -146,6 +146,10 cross_project_issue_relations:
146 146 # Enables subtasks to be in other projects
147 147 cross_project_subtasks:
148 148 default: 'tree'
149 parent_issue_dates:
150 default: 'derived'
151 parent_issue_priority:
152 default: 'derived'
149 153 link_copied_issue:
150 154 default: 'ask'
151 155 issue_group_assignment:
@@ -105,6 +105,12 module ObjectHelpers
105 105 issue.reload
106 106 end
107 107
108 def Issue.generate_with_child!(attributes={})
109 issue = Issue.generate!(attributes)
110 Issue.generate!(:parent_issue_id => issue.id)
111 issue.reload
112 end
113
108 114 def Journal.generate!(attributes={})
109 115 journal = Journal.new(attributes)
110 116 journal.user ||= User.first
@@ -287,35 +287,6 class IssueNestedSetTest < ActiveSupport::TestCase
287 287 end
288 288 end
289 289
290 def test_parent_priority_should_be_the_highest_child_priority
291 parent = Issue.generate!(:priority => IssuePriority.find_by_name('Normal'))
292 # Create children
293 child1 = parent.generate_child!(:priority => IssuePriority.find_by_name('High'))
294 assert_equal 'High', parent.reload.priority.name
295 child2 = child1.generate_child!(:priority => IssuePriority.find_by_name('Immediate'))
296 assert_equal 'Immediate', child1.reload.priority.name
297 assert_equal 'Immediate', parent.reload.priority.name
298 child3 = parent.generate_child!(:priority => IssuePriority.find_by_name('Low'))
299 assert_equal 'Immediate', parent.reload.priority.name
300 # Destroy a child
301 child1.destroy
302 assert_equal 'Low', parent.reload.priority.name
303 # Update a child
304 child3.reload.priority = IssuePriority.find_by_name('Normal')
305 child3.save!
306 assert_equal 'Normal', parent.reload.priority.name
307 end
308
309 def test_parent_dates_should_be_lowest_start_and_highest_due_dates
310 parent = Issue.generate!
311 parent.generate_child!(:start_date => '2010-01-25', :due_date => '2010-02-15')
312 parent.generate_child!( :due_date => '2010-02-13')
313 parent.generate_child!(:start_date => '2010-02-01', :due_date => '2010-02-22')
314 parent.reload
315 assert_equal Date.parse('2010-01-25'), parent.start_date
316 assert_equal Date.parse('2010-02-22'), parent.due_date
317 end
318
319 290 def test_parent_done_ratio_should_be_average_done_ratio_of_leaves
320 291 parent = Issue.generate!
321 292 parent.generate_child!(:done_ratio => 20)
@@ -390,20 +361,6 class IssueNestedSetTest < ActiveSupport::TestCase
390 361 assert_nil first_parent.reload.estimated_hours
391 362 end
392 363
393 def test_reschuling_a_parent_should_reschedule_subtasks
394 parent = Issue.generate!
395 c1 = parent.generate_child!(:start_date => '2010-05-12', :due_date => '2010-05-18')
396 c2 = parent.generate_child!(:start_date => '2010-06-03', :due_date => '2010-06-10')
397 parent.reload
398 parent.reschedule_on!(Date.parse('2010-06-02'))
399 c1.reload
400 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-08')], [c1.start_date, c1.due_date]
401 c2.reload
402 assert_equal [Date.parse('2010-06-03'), Date.parse('2010-06-10')], [c2.start_date, c2.due_date] # no change
403 parent.reload
404 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-10')], [parent.start_date, parent.due_date]
405 end
406
407 364 def test_project_copy_should_copy_issue_tree
408 365 p = Project.create!(:name => 'Tree copy', :identifier => 'tree-copy', :tracker_ids => [1, 2])
409 366 i1 = Issue.generate!(:project => p, :subject => 'i1')
General Comments 0
You need to be logged in to leave comments. Login now