Project

General

Profile

Feature #5490 » 0001-settings-configurable-issue-independent-time-trackin.patch

Lucile Quirion, 2015-02-23 19:47

View differences:

app/models/issue.rb
457 457
    attrs = delete_unsafe_attributes(attrs, user)
458 458
    return if attrs.empty?
459 459

  
460
    unless leaf?
460
    unless leaf? || Issue.use_independent_time_tracking?
461 461
      attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
462 462
    end
463 463

  
......
527 527
    required_attribute_names(user).include?(name.to_s)
528 528
  end
529 529

  
530
  # Returns true if the attribute is disabled for edition
531
  def disabled_attribute?(attribute)
532
    !self.leaf? && !Issue.use_independent_time_tracking?
533
  end
534

  
530 535
  # Returns a hash of the workflow rule by attribute for the given user
531 536
  #
532 537
  # Examples:
......
578 583
    Setting.issue_done_ratio == 'issue_field'
579 584
  end
580 585

  
586
  def self.use_independent_time_tracking?
587
    Setting.issue_independent_time_tracking?
588
  end
589

  
581 590
  def validate_issue
582 591
    if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
583 592
      errors.add :due_date, :greater_than_start_date
......
886 895
        sum("#{TimeEntry.table_name}.hours").to_f || 0.0
887 896
  end
888 897

  
898
  def total_estimated_hours
899
    @total_estimated_hours ||= self_and_descendants.sum(:estimated_hours)
900
  end
901

  
889 902
  def relations
890 903
    @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
891 904
  end
......
1079 1092
  # If the issue is a parent task, this is done by rescheduling its subtasks.
1080 1093
  def reschedule_on!(date)
1081 1094
    return if date.nil?
1082
    if leaf?
1095
    if leaf? || Issue.use_independent_time_tracking?
1083 1096
      if start_date.nil? || start_date != date
1084 1097
        if start_date && start_date > date
1085 1098
          # Issue can not be moved earlier than its soonest start date
......
1394 1407
  end
1395 1408

  
1396 1409
  def recalculate_attributes_for(issue_id)
1397
    if issue_id && p = Issue.find_by_id(issue_id)
1410
    if issue_id && (p = Issue.find_by_id(issue_id)) && !Issue.use_independent_time_tracking?
1411

  
1398 1412
      # priority = highest priority of children
1399 1413
      if priority_position = p.children.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1400 1414
        p.priority = IssuePriority.find_by_position(priority_position)
app/views/issues/_attributes.html.erb
11 11
<% end %>
12 12

  
13 13
<% if @issue.safe_attribute? 'priority_id' %>
14
<p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), {:required => true}, :disabled => !@issue.leaf? %></p>
14
<p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), {:required => true}, :disabled => @issue.disabled_attribute?('priority_id') %></p>
15 15
<% end %>
16 16

  
17 17
<% if @issue.safe_attribute? 'assigned_to_id' %>
......
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?,
51
  <%= f.text_field(:start_date, :size => 10,
52
                   :disabled => @issue.disabled_attribute?('start_date'),
52 53
                   :required => @issue.required_attribute?('start_date')) %>
53 54
  <%= calendar_for('issue_start_date') if @issue.leaf? %>
54 55
</p>
......
56 57

  
57 58
<% if @issue.safe_attribute? 'due_date' %>
58 59
<p id="due_date_area">
59
  <%= f.text_field(:due_date, :size => 10, :disabled => !@issue.leaf?,
60
  <%= f.text_field(:due_date, :size => 10,
61
                   :disabled => @issue.disabled_attribute?('due_date'),
60 62
                   :required => @issue.required_attribute?('due_date')) %>
61 63
  <%= calendar_for('issue_due_date') if @issue.leaf? %>
62 64
</p>
63 65
<% end %>
64 66

  
65 67
<% 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>
68
<p><%= f.text_field :estimated_hours, :size => 3,
69
                    :disabled => @issue.disabled_attribute?('estimated_hours'),
70
                    :required => @issue.required_attribute?('estimated_hours') %> <%= l(:field_hours) %></p>
67 71
<% end %>
68 72

  
69
<% if @issue.safe_attribute?('done_ratio') && @issue.leaf? && Issue.use_field_for_done_ratio? %>
73
<% if @issue.safe_attribute?('done_ratio') && !@issue.disabled_attribute?('done_ratio') && Issue.use_field_for_done_ratio? %>
70 74
<p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }), :required => @issue.required_attribute?('done_ratio') %></p>
71 75
<% end %>
72 76
</div>
app/views/issues/show.html.erb
61 61
    unless @issue.estimated_hours.nil?
62 62
      rows.right l(:field_estimated_hours), l_hours(@issue.estimated_hours), :class => 'estimated-hours'
63 63
    end
64
    unless @issue.leaf? || !Issue.use_independent_time_tracking? || @issue.total_estimated_hours.nil?
65
      rows.right l(:field_total_estimated_hours), l_hours(@issue.total_estimated_hours), :class => 'estimated_hours'
66
    end
64 67
  end
65 68
  if User.current.allowed_to?(:view_time_entries, @project)
66
    rows.right l(:label_spent_time), (@issue.total_spent_hours > 0 ? link_to(l_hours(@issue.total_spent_hours), issue_time_entries_path(@issue)) : "-"), :class => 'spent-time'
69
      rows.right l(:label_spent_time), (@issue.spent_hours > 0 ? link_to(l_hours(@issue.spent_hours), issue_time_entries_path(@issue)) : "-"), :class => 'spent-time'
70
    if (!@issue.leaf? && Issue.use_independent_time_tracking?)
71
      rows.right l(:label_total_spent_time), (@issue.total_spent_hours > 0 ? link_to(l_hours(@issue.total_spent_hours), issue_time_entries_path(@issue)) : "-"), :class => 'spent-time'
72
    end
67 73
  end
68 74
end %>
69 75
<%= render_custom_fields_rows(@issue) %>
app/views/settings/_issues.html.erb
15 15

  
16 16
<p><%= setting_select :issue_done_ratio, Issue::DONE_RATIO_OPTIONS.collect {|i| [l("setting_issue_done_ratio_#{i}"), i]} %></p>
17 17

  
18
<p><%= setting_check_box :issue_independent_time_tracking %></p>
19

  
18 20
<p><%= setting_multiselect :non_working_week_days, (1..7).map {|d| [day_name(d), d.to_s]}, :inline => true %></p>
19 21

  
20 22
<p><%= setting_text_field :issues_export_limit, :size => 6 %></p>
config/locales/en.yml
300 300
  field_delay: Delay
301 301
  field_assignable: Issues can be assigned to this role
302 302
  field_redirect_existing_links: Redirect existing links
303
  field_total_estimated_hours: Total estimated time
303 304
  field_estimated_hours: Estimated time
304 305
  field_column_names: Columns
305 306
  field_time_entries: Log time
......
392 393
  setting_issue_done_ratio: Calculate the issue done ratio with
393 394
  setting_issue_done_ratio_issue_field: Use the issue field
394 395
  setting_issue_done_ratio_issue_status: Use the issue status
396
  setting_issue_independent_time_tracking: Estimate and track time for parent issue independently from children issues
395 397
  setting_start_of_week: Start calendars on
396 398
  setting_rest_api_enabled: Enable REST web service
397 399
  setting_cache_formatted_text: Cache formatted text
......
744 746
  label_changes_details: Details of all changes
745 747
  label_issue_tracking: Issue tracking
746 748
  label_spent_time: Spent time
749
  label_total_spent_time: Total spent time
747 750
  label_overall_spent_time: Overall spent time
748 751
  label_f_hour: "%{value} hour"
749 752
  label_f_hour_plural: "%{value} hours"
config/settings.yml
174 174
  default: 1
175 175
issue_done_ratio:
176 176
  default: 'issue_field'
177
issue_independent_time_tracking:
178
  default: 0
177 179
default_projects_public:
178 180
  default: 1
179 181
default_projects_modules:
test/functional/issues_controller_test.rb
1368 1368
    assert_select 'div.thumbnails', 0
1369 1369
  end
1370 1370

  
1371
  def test_show_should_disable_parent_time_tracking_attributes
1372
    issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :parent_issue_id => 2, :subject => 'Child Issue')
1373

  
1374
    @request.session[:user_id] = 2
1375
    get :show, :id => 2
1376
    assert_response :success
1377

  
1378
    assert_select 'form#issue-form' do
1379
      assert_select 'select[disabled="disabled"]' do
1380
        assert_select '[name=?]', 'issue[priority_id]', true, "priority_id shall be disabled"
1381
      end
1382
      assert_select 'input[disabled="disabled"]' do
1383
        assert_select '[name=?]', 'issue[start_date]', true, "start_date shall be disabled"
1384
        assert_select '[name=?]', 'issue[due_date]', true, "due_date shall be disabled"
1385
        assert_select '[name=?]', 'issue[estimated_hours]', true, "estimated_hours shall be disabled"
1386
      end
1387
      assert_select 'select[name=?]', 'issue[done_ratio]', false, "done_ratio shall not be present"
1388
    end
1389
  end
1390

  
1391
  def test_show_should_enable_parent_time_tracking_attributes_with_independent_time_tracking
1392
    with_settings :issue_independent_time_tracking => '1' do
1393
      issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :parent_issue_id => 2, :subject => 'Child Issue')
1394

  
1395
      @request.session[:user_id] = 2
1396
      get :show, :id => 2
1397
      assert_response :success
1398

  
1399
      assert_select 'form#issue-form' do
1400
        assert_select 'select[disabled="disabled"]', 0
1401
        assert_select 'input[disabled="disabled"]', 0
1402

  
1403
        assert_select 'select[name=?]', 'issue[priority_id]', true, "priority_id shall be editable"
1404
        assert_select 'input[name=?]', 'issue[start_date]', true, "start_date shall be editable"
1405
        assert_select 'input[name=?]', 'issue[due_date]', true, "due_date shall be editable"
1406
        assert_select 'input[name=?]', 'issue[estimated_hours]', true, "estimated_hours shall be editable"
1407
        assert_select 'select[name=?]', 'issue[done_ratio]', true, "done_ratio shall be editable"
1408
      end
1409
    end
1410
  end
1411

  
1371 1412
  def test_show_with_multi_custom_field
1372 1413
    field = CustomField.find(1)
1373 1414
    field.update_attribute :multiple, true
test/unit/issue_nested_set_test.rb
306 306
    assert_equal 'Normal', parent.reload.priority.name
307 307
  end
308 308

  
309
  def test_parent_priority_should_not_be_the_highest_child_priority_with_independent_time_tracking
310
    with_settings :issue_independent_time_tracking => '1' do
311
      parent = Issue.generate!(:priority => IssuePriority.find_by_name('Normal'))
312
      # Create children
313
      child1 = parent.generate_child!(:priority => IssuePriority.find_by_name('High'))
314
      assert_equal 'Normal', parent.reload.priority.name
315
      child2 = child1.generate_child!(:priority => IssuePriority.find_by_name('Immediate'))
316
      assert_equal 'High', child1.reload.priority.name
317
      assert_equal 'Normal', parent.reload.priority.name
318
      child3 = parent.generate_child!(:priority => IssuePriority.find_by_name('Low'))
319
      assert_equal 'Normal', parent.reload.priority.name
320
      # Destroy a child
321
      child1.destroy
322
      assert_equal 'Normal', parent.reload.priority.name
323
      # Update a child
324
      child3.reload.priority = IssuePriority.find_by_name('Normal')
325
      child3.save!
326
      assert_equal 'Normal', parent.reload.priority.name
327
    end
328

  
329
  end
330

  
309 331
  def test_parent_dates_should_be_lowest_start_and_highest_due_dates
310 332
    parent = Issue.generate!
311 333
    parent.generate_child!(:start_date => '2010-01-25', :due_date => '2010-02-15')
......
316 338
    assert_equal Date.parse('2010-02-22'), parent.due_date
317 339
  end
318 340

  
341
  def test_parent_dates_should_not_be_lowest_start_and_highest_due_dates_with_independent_time_tracking
342
    with_settings :issue_independent_time_tracking => '1' do
343
      parent = Issue.generate!
344
      parent.generate_child!(:start_date => '2010-01-25', :due_date => '2010-02-15')
345
      parent.generate_child!(                             :due_date => '2010-02-13')
346
      parent.generate_child!(:start_date => '2010-02-01', :due_date => '2010-02-22')
347
      parent.reload
348
      assert_equal nil, parent.start_date
349
      assert_equal nil, parent.due_date
350
    end
351
  end
352

  
319 353
  def test_parent_done_ratio_should_be_average_done_ratio_of_leaves
320 354
    parent = Issue.generate!
321 355
    parent.generate_child!(:done_ratio => 20)
......
331 365
    assert_equal 40, parent.reload.done_ratio
332 366
  end
333 367

  
368
  def test_parent_done_ratio_should_not_be_average_done_ratio_of_leaves_with_independent_time_tracking
369
    with_settings :issue_independent_time_tracking => '1' do
370
      parent = Issue.generate!
371
      parent.generate_child!(:done_ratio => 20)
372
      assert_equal 0, parent.reload.done_ratio
373
      parent.generate_child!(:done_ratio => 70)
374
      assert_equal 0, parent.reload.done_ratio
375

  
376
      child = parent.generate_child!(:done_ratio => 0)
377
      assert_equal 0, parent.reload.done_ratio
378

  
379
      child.generate_child!(:done_ratio => 30)
380
      assert_equal 0, child.reload.done_ratio
381
      assert_equal 0, parent.reload.done_ratio
382
    end
383
  end
384

  
334 385
  def test_parent_done_ratio_should_be_weighted_by_estimated_times_if_any
335 386
    parent = Issue.generate!
336 387
    parent.generate_child!(:estimated_hours => 10, :done_ratio => 20)
......
350 401
    assert_equal 100, parent.reload.done_ratio
351 402
  end
352 403

  
404
  def test_parent_done_ratio_should_not_reach_100_when_child_is_closed_with_independent_time_tracking
405
    parent = Issue.generate!
406
    with_settings :issue_independent_time_tracking => '1' do
407
      issue1 = parent.generate_child!
408
      issue2 = parent.generate_child!(:estimated_hours => 0)
409
      assert_equal 0, parent.reload.done_ratio
410
      issue1.reload.close!
411
      assert_equal 0, parent.reload.done_ratio
412
      issue2.reload.close!
413
      assert_equal 0, parent.reload.done_ratio
414
    end
415
  end
416

  
353 417
  def test_parent_estimate_should_be_sum_of_leaves
354 418
    parent = Issue.generate!
355 419
    parent.generate_child!(:estimated_hours => nil)
......
360 424
    assert_equal 12, parent.reload.estimated_hours
361 425
  end
362 426

  
427
  def test_parent_estimated_should_not_be_the_sum_of_leaves_with_independent_time_tracking
428
    parent = Issue.generate!
429
    with_settings :issue_independent_time_tracking => '1' do
430
      parent.generate_child!(:estimated_hours => nil)
431
      assert_equal nil, parent.reload.estimated_hours
432
      parent.generate_child!(:estimated_hours => 5)
433
      assert_equal nil, parent.reload.estimated_hours
434
      parent.generate_child!(:estimated_hours => 7)
435
      assert_equal nil, parent.reload.estimated_hours
436
    end
437
  end
438

  
363 439
  def test_done_ratio_of_parent_with_a_child_without_estimated_time_should_not_exceed_100
364 440
    parent = Issue.generate!
365 441
    parent.generate_child!(:estimated_hours => 40)
......
390 466
    assert_nil first_parent.reload.estimated_hours
391 467
  end
392 468

  
393
  def test_reschuling_a_parent_should_reschedule_subtasks
469
  def test_move_parent_should_not_updates_old_parent_attributes_with_independent_time_tracking
470
    with_settings :issue_independent_time_tracking => '1' do
471
      first_parent = Issue.generate!
472
      second_parent = Issue.generate!
473
      child = first_parent.generate_child!(:estimated_hours => 5)
474
      assert_nil first_parent.reload.estimated_hours
475
      child.update_attributes(:estimated_hours => 7, :parent_issue_id => second_parent.id)
476
      assert_nil second_parent.reload.estimated_hours
477
      assert_nil first_parent.reload.estimated_hours
478
    end
479
  end
480

  
481
  def test_rescheduling_a_parent_should_reschedule_subtasks
394 482
    parent = Issue.generate!
395 483
    c1 = parent.generate_child!(:start_date => '2010-05-12', :due_date => '2010-05-18')
396 484
    c2 = parent.generate_child!(:start_date => '2010-06-03', :due_date => '2010-06-10')
......
404 492
    assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-10')], [parent.start_date, parent.due_date]
405 493
  end
406 494

  
495
  def test_rescheduling_a_parent_should_not_reschedule_subtasks_with_independent_time_tracking
496
    with_settings :issue_independent_time_tracking => '1' do
497
      parent = Issue.generate!(:start_date => '2010-05-12', :due_date => '2010-06-10')
498
      c1 = parent.generate_child!(:start_date => '2010-05-12', :due_date => '2010-05-18')
499
      c2 = parent.generate_child!(:start_date => '2010-06-03', :due_date => '2010-06-10')
500
      parent.reload
501
      parent.reschedule_on!(Date.parse('2010-06-02'))
502
      c1.reload
503
      assert_equal [Date.parse('2010-05-12'), Date.parse('2010-05-18')], [c1.start_date, c1.due_date], "child 1"
504
      c2.reload
505
      assert_equal [Date.parse('2010-06-03'), Date.parse('2010-06-10')], [c2.start_date, c2.due_date], "child 2" # no change
506
      parent.reload
507
      assert_equal [Date.parse('2010-06-02'), Date.parse('2010-07-01')], [parent.start_date, parent.due_date], "parent"
508
    end
509
  end
510

  
407 511
  def test_project_copy_should_copy_issue_tree
408 512
    p = Project.create!(:name => 'Tree copy', :identifier => 'tree-copy', :tracker_ids => [1, 2])
409 513
    i1 = Issue.generate!(:project => p, :subject => 'i1')
test/unit/issue_test.rb
2215 2215
    end
2216 2216
  end
2217 2217

  
2218
  test "#time tracking attributes should be independent according to Setting.issue_independent_time_tracking" do
2219
    @issue = Issue.find(1)
2220
    @issue.update_attribute(:estimated_hours, '1:30')
2221

  
2222
    with_settings :issue_independent_time_tracking => '1' do
2223
      issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :parent_id => 1,
2224
                      :status_id => 1, :priority => IssuePriority.all.first,
2225
                      :subject => 'test_independent_time_tracking',
2226
                      :description => 'IssueTest#test_independent_time_tracking', :estimated_hours => '0:30')
2227
      assert issue.save
2228
      issue.reload
2229
      @issue.reload
2230
      assert_equal 0.5, issue.estimated_hours
2231
      assert_equal 1.5, @issue.estimated_hours
2232
    end
2233

  
2234
    with_settings :issue_independent_time_tracking => '0' do
2235
      issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :parent_id => 1,
2236
                      :status_id => 1, :priority => IssuePriority.all.first,
2237
                      :subject => 'test_independent_time_tracking',
2238
                      :description => 'IssueTest#test_independent_time_tracking', :estimated_hours => '0:30')
2239
      assert issue.save
2240
      issue.reload
2241
      @issue.reload
2242
      assert_equal 0.5, issue.estimated_hours
2243
      assert_equal 0.5, issue.estimated_hours
2244
    end
2245

  
2246
  end
2247

  
2248

  
2218 2249
  test "#by_tracker" do
2219 2250
    User.current = User.anonymous
2220 2251
    groups = Issue.by_tracker(Project.find(1))
(7-7/9)