Project

General

Profile

Feature #24277 » 01_add_remaining_hours_field_15945.patch

Marius BĂLTEANU, 2016-11-07 01:22

View differences:

app/helpers/issues_helper.rb
118 118
    end
119 119
  end
120 120

  
121
  def issue_remaining_hours_details(issue)
122
    if issue.total_remaining_hours.present?
123
      if issue.total_remaining_hours == issue.remaining_hours
124
        l_hours_short(issue.remaining_hours)
125
      else
126
        s = issue.remaining_hours.present? ? l_hours_short(issue.remaining_hours) : ""
127
        s << " (#{l(:label_total)}: #{l_hours_short(issue.total_remaining_hours)})"
128
        s.html_safe
129
      end
130
    end
131
  end
132

  
121 133
  def issue_spent_hours_details(issue)
122 134
    if issue.total_spent_hours > 0
123 135
      path = project_time_entries_path(issue.project, :issue_id => "~#{issue.id}")
......
345 357
        value = find_name_by_reflection(field, detail.value)
346 358
        old_value = find_name_by_reflection(field, detail.old_value)
347 359

  
348
      when 'estimated_hours'
360
      when 'estimated_hours', 'remaining_hours'
349 361
        value = l_hours_short(detail.value.to_f) unless detail.value.blank?
350 362
        old_value = l_hours_short(detail.old_value.to_f) unless detail.old_value.blank?
351 363

  
app/models/issue.rb
71 71
  validates_length_of :subject, :maximum => 255
72 72
  validates_inclusion_of :done_ratio, :in => 0..100
73 73
  validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
74
  validates :remaining_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
74 75
  validates :start_date, :date => true
75 76
  validates :due_date, :date => true
76 77
  validate :validate_issue, :validate_required_fields
......
107 108
  before_validation :clear_disabled_fields
108 109
  before_create :default_assign
109 110
  before_save :close_duplicates, :update_done_ratio_from_issue_status,
110
              :force_updated_on_change, :update_closed_on, :set_assigned_to_was
111
              :force_updated_on_change, :update_closed_on, :set_assigned_to_was, :update_remaining_hours
111 112
  after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
112 113
  after_save :reschedule_following_issues, :update_nested_set_attributes,
113 114
             :update_parent_attributes, :delete_selected_attachments, :create_journal
......
244 245
    @spent_hours = nil
245 246
    @total_spent_hours = nil
246 247
    @total_estimated_hours = nil
248
    @total_remaining_hours = nil
247 249
    base_reload(*args)
248 250
  end
249 251

  
......
430 432
    write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
431 433
  end
432 434

  
435
  def remaining_hours=(h)
436
    h = h.is_a?(String) ? h.to_hours : h
437
    # remaining time cannot be less than zero
438
    h = 0 if !h.nil? && h < 0
439
    write_attribute :remaining_hours, h
440
  end
441

  
433 442
  safe_attributes 'project_id',
434 443
    'tracker_id',
435 444
    'status_id',
......
443 452
    'due_date',
444 453
    'done_ratio',
445 454
    'estimated_hours',
455
    'remaining_hours',
446 456
    'custom_field_values',
447 457
    'custom_fields',
448 458
    'lock_version',
......
1030 1040
    end
1031 1041
  end
1032 1042

  
1043
  def remaining_hours
1044
    @remaining_hours = read_attribute(:remaining_hours)
1045
    if @remaining_hours.nil? && estimated_hours
1046
      @remaining_hours = estimated_hours
1047
    else
1048
      @remaining_hours
1049
    end
1050
  end
1051

  
1052
  def total_remaining_hours
1053
    if leaf?
1054
      remaining_hours
1055
    else
1056
      @total_remaining_hours ||= self_and_descendants.sum(:remaining_hours)
1057
    end
1058
  end
1059

  
1033 1060
  def relations
1034 1061
    @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
1035 1062
  end
......
1703 1730
    end
1704 1731
  end
1705 1732

  
1733
  # Callback for setting remaining time to zero when the issue is closed.
1734
  def update_remaining_hours
1735
    if closing? && safe_attribute?('remaining_hours') && self.remaining_hours.to_f > 0
1736
      self.remaining_hours = 0
1737
    end
1738
  end
1739

  
1706 1740
  # Saves the changes in a Journal
1707 1741
  # Called after_save
1708 1742
  def create_journal
app/models/issue_import.rb
158 158
    if estimated_hours = row_value(row, 'estimated_hours')
159 159
      attributes['estimated_hours'] = estimated_hours
160 160
    end
161
    if remaining_hours = row_value(row, 'remaining_hours')
162
      attributes['remaining_hours'] = remaining_hours
163
    end
161 164
    if done_ratio = row_value(row, 'done_ratio')
162 165
      attributes['done_ratio'] = done_ratio
163 166
    end
app/models/issue_query.rb
36 36
    QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
37 37
    QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
38 38
    QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours", :totalable => true),
39
    QueryColumn.new(:remaining_hours, :sortable => "#{Issue.table_name}.remaining_hours", :totalable => true),
39 40
    QueryColumn.new(:total_estimated_hours,
40 41
      :sortable => "COALESCE((SELECT SUM(estimated_hours) FROM #{Issue.table_name} subtasks" +
41 42
        " WHERE subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)",
42 43
      :default_order => 'desc'),
44
    QueryColumn.new(:total_remaining_hours,
45
      :sortable => "COALESCE((SELECT SUM(remaining_hours) FROM #{Issue.table_name} subtasks" +
46
        " WHERE subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)",
47
      :default_order => 'desc'),
43 48
    QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
44 49
    QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
45 50
    QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'),
......
174 179
    add_available_filter "start_date", :type => :date
175 180
    add_available_filter "due_date", :type => :date
176 181
    add_available_filter "estimated_hours", :type => :float
182
    add_available_filter "remaining_hours", :type => :float
177 183
    add_available_filter "done_ratio", :type => :integer
178 184

  
179 185
    if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
......
285 291
    map_total(scope.sum(:estimated_hours)) {|t| t.to_f.round(2)}
286 292
  end
287 293

  
294
  # Returns sum of all the issue's remaining_hours
295
  def total_for_remaining_hours(scope)
296
    map_total(scope.sum(:remaining_hours)) {|t| t.to_f.round(2)}
297
  end
298

  
288 299
  # Returns sum of all the issue's time entries hours
289 300
  def total_for_spent_hours(scope)
290 301
    total = if group_by_column.try(:name) == :project
app/models/mail_handler.rb
424 424
      'start_date' => get_keyword(:start_date, :format => '\d{4}-\d{2}-\d{2}'),
425 425
      'due_date' => get_keyword(:due_date, :format => '\d{4}-\d{2}-\d{2}'),
426 426
      'estimated_hours' => get_keyword(:estimated_hours),
427
      'remaining_hours' => get_keyword(:remaining_hours),
427 428
      'done_ratio' => get_keyword(:done_ratio, :format => '(\d|10)?0')
428 429
    }.delete_if {|k, v| v.blank? }
429 430

  
app/models/tracker.rb
21 21
  CORE_FIELDS_UNDISABLABLE = %w(project_id tracker_id subject description priority_id is_private).freeze
22 22
  # Fields that can be disabled
23 23
  # Other (future) fields should be appended, not inserted!
24
  CORE_FIELDS = %w(assigned_to_id category_id fixed_version_id parent_issue_id start_date due_date estimated_hours done_ratio).freeze
24
  CORE_FIELDS = %w(assigned_to_id category_id fixed_version_id parent_issue_id start_date due_date estimated_hours remaining_hours done_ratio).freeze
25 25
  CORE_FIELDS_ALL = (CORE_FIELDS_UNDISABLABLE + CORE_FIELDS).freeze
26 26

  
27 27
  before_destroy :check_integrity
app/views/imports/_fields_mapping.html.erb
82 82
  <%= mapping_select_tag @import, 'estimated_hours' %>
83 83
</p>
84 84
<p>
85
  <label><%= l(:field_remaining_hours) %></label>
86
  <%= mapping_select_tag @import, 'remaining_hours' %>
87
</p>
88
<p>
85 89
  <label><%= l(:field_done_ratio) %></label>
86 90
  <%= mapping_select_tag @import, 'done_ratio' %>
87 91
</p>
app/views/issues/_attributes.html.erb
64 64
</p>
65 65
<% end %>
66 66

  
67
<% if @issue.safe_attribute?('done_ratio') && Issue.use_field_for_done_ratio? %>
68
<p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }), :required => @issue.required_attribute?('done_ratio') %></p>
69
<% end %>
70

  
67 71
<% if @issue.safe_attribute? 'estimated_hours' %>
68 72
<p><%= f.text_field :estimated_hours, :size => 3, :required => @issue.required_attribute?('estimated_hours') %> <%= l(:field_hours) %></p>
69 73
<% end %>
70 74

  
71
<% if @issue.safe_attribute?('done_ratio') && Issue.use_field_for_done_ratio? %>
72
<p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }), :required => @issue.required_attribute?('done_ratio') %></p>
75
<% if @issue.safe_attribute? 'remaining_hours' %>
76
<p><%= f.text_field :remaining_hours, :size => 3, :required => @issue.required_attribute?('remaining_hours') %> <%= l(:field_hours) %></p>
73 77
<% end %>
78

  
74 79
</div>
75 80
</div>
76 81

  
app/views/issues/_edit.html.erb
31 31
      <fieldset><legend><%= l(:field_notes) %></legend>
32 32
      <%= f.text_area :notes, :cols => 60, :rows => 10, :class => 'wiki-edit', :no_label => true %>
33 33
      <%= wikitoolbar_for 'issue_notes' %>
34
  
34

  
35 35
      <% if @issue.safe_attribute? 'private_notes' %>
36 36
      <%= f.check_box :private_notes, :no_label => true %> <label for="issue_private_notes"><%= l(:field_private_notes) %></label>
37 37
      <% end %>
38
  
38

  
39 39
      <%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %>
40 40
      </fieldset>
41 41

  
app/views/issues/bulk_edit.html.erb
170 170
</p>
171 171
<% end %>
172 172

  
173
<% if @safe_attributes.include?('remaining_hours') %>
174
<p>
175
  <label for='issue_remaining_hours'><%= l(:field_remaining_hours) %></label>
176
  <%= text_field_tag 'issue[remaining_hours]', '', :value => @issue_params[:remaining_hours], :size => 10 %>
177
  <label class="inline"><%= check_box_tag 'issue[remaining_hours]', 'none', (@issue_params[:remaining_hours] == 'none'), :id => nil, :data => {:disables => '#issue_remaining_hours'} %><%= l(:button_clear) %></label>
178
</p>
179
<% end %>
180

  
173 181
<% if @safe_attributes.include?('done_ratio') && Issue.use_field_for_done_ratio? %>
174 182
<p>
175 183
  <label for='issue_done_ratio'><%= l(:field_done_ratio) %></label>
app/views/issues/index.api.rsb
19 19
      api.done_ratio  issue.done_ratio
20 20
      api.is_private  issue.is_private
21 21
      api.estimated_hours issue.estimated_hours
22
      api.remaining_hours issue.remaining_hours
22 23

  
23 24
      render_api_custom_values issue.visible_custom_field_values, api
24 25

  
app/views/issues/show.api.rsb
17 17
  api.done_ratio @issue.done_ratio
18 18
  api.is_private @issue.is_private
19 19
  api.estimated_hours @issue.estimated_hours
20
  api.remaining_hours @issue.remaining_hours
20 21
  api.total_estimated_hours @issue.total_estimated_hours
22
  api.total_remaining_hours @issue.total_remaining_hours
21 23
  if User.current.allowed_to?(:view_time_entries, @project)
22 24
    api.spent_hours(@issue.spent_hours)
23 25
    api.total_spent_hours(@issue.total_spent_hours)
app/views/issues/show.html.erb
60 60
  unless @issue.disabled_core_fields.include?('estimated_hours')
61 61
    rows.right l(:field_estimated_hours), issue_estimated_hours_details(@issue), :class => 'estimated-hours'
62 62
  end
63
  unless @issue.disabled_core_fields.include?('remaining_hours')
64
      rows.right l(:field_remaining_hours), issue_remaining_hours_details(@issue), :class => 'remaining-hours'
65
  end
63 66
  if User.current.allowed_to_view_all_time_entries?(@project)
64 67
    if @issue.total_spent_hours > 0
65 68
      rows.right l(:label_spent_time), issue_spent_hours_details(@issue), :class => 'spent-time'
config/locales/en.yml
365 365
  field_default_version: Default version
366 366
  field_remote_ip: IP address
367 367
  field_textarea_font: Font used for text areas
368
  field_remaining_hours: Remaining time
369
  field_total_remaining_hours: Total remaining time
368 370

  
369 371
  setting_app_title: Application title
370 372
  setting_app_subtitle: Application subtitle
db/migrate/20160920184857_add_remaining_hours_to_issues.rb
1
class AddRemainingHoursToIssues < ActiveRecord::Migration
2
  def self.up
3
    add_column :issues, :remaining_hours, :float
4
    Issue.where("estimated_hours > ?", 0).update_all("remaining_hours = estimated_hours")
5
  end
6

  
7
  def self.down
8
    remove_column :issues, :remaining_hours
9
   end
10
end
test/fixtures/mail_handler/ticket_on_given_project.eml
18 18
X-Mailer: Microsoft Outlook Express 6.00.2900.2869
19 19
X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
20 20

  
21
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet 
22
turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus 
23
blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti 
24
sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In 
25
in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras 
26
sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum 
27
id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus 
28
eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique 
29
sed, mauris --- Pellentesque habitant morbi tristique senectus et netus et 
30
malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse 
21
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet
22
turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus
23
blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti
24
sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In
25
in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras
26
sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum
27
id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus
28
eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique
29
sed, mauris --- Pellentesque habitant morbi tristique senectus et netus et
30
malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse
31 31
platea dictumst.
32 32

  
33 33
Project: onlinestore
......
37 37
Assigned to: John Smith
38 38
fixed version: alpha
39 39
estimated hours: 2.5
40
remaining hours: 1
40 41
done ratio: 30
41 42

  
42 43
--- This line starts with a delimiter and should not be stripped
......
51 52

  
52 53
This paragraph is after the delimiter so it shouldn't appear.
53 54

  
54
Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque 
55
sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. 
56
Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, 
57
dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, 
58
massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo 
55
Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque
56
sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem.
57
Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et,
58
dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed,
59
massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo
59 60
pulvinar dui, a gravida orci mi eget odio. Nunc a lacus.
60 61

  
test/functional/issues_controller_test.rb
561 561
      str_big5  = "\xa4@\xa4\xeb".force_encoding('Big5')
562 562
      issue = Issue.generate!(:subject => str_utf8)
563 563

  
564
      get :index, :project_id => 1, 
565
                  :f => ['subject'], 
564
      get :index, :project_id => 1,
565
                  :f => ['subject'],
566 566
                  :op => '=', :values => [str_utf8],
567 567
                  :format => 'csv'
568 568
      assert_equal 'text/csv; header=present', @response.content_type
......
580 580
      str_utf8  = "\xe4\xbb\xa5\xe5\x86\x85".force_encoding('UTF-8')
581 581
      issue = Issue.generate!(:subject => str_utf8)
582 582

  
583
      get :index, :project_id => 1, 
584
                  :f => ['subject'], 
583
      get :index, :project_id => 1,
584
                  :f => ['subject'],
585 585
                  :op => '=', :values => [str_utf8],
586 586
                  :c => ['status', 'subject'],
587 587
                  :format => 'csv',
......
603 603
      str1  = "test_index_csv_tw"
604 604
      issue = Issue.generate!(:subject => str1, :estimated_hours => '1234.5')
605 605

  
606
      get :index, :project_id => 1, 
607
                  :f => ['subject'], 
606
      get :index, :project_id => 1,
607
                  :f => ['subject'],
608 608
                  :op => '=', :values => [str1],
609 609
                  :c => ['estimated_hours', 'subject'],
610 610
                  :format => 'csv',
......
620 620
      str1  = "test_index_csv_fr"
621 621
      issue = Issue.generate!(:subject => str1, :estimated_hours => '1234.5')
622 622

  
623
      get :index, :project_id => 1, 
624
                  :f => ['subject'], 
623
      get :index, :project_id => 1,
624
                  :f => ['subject'],
625 625
                  :op => '=', :values => [str1],
626 626
                  :c => ['estimated_hours', 'subject'],
627 627
                  :format => 'csv',
......
696 696
      assert_response :success
697 697
    end
698 698
  end
699
  
699

  
700 700
  def test_index_sort_by_assigned_to
701 701
    get :index, :sort => 'assigned_to'
702 702
    assert_response :success
......
705 705
    assert_equal assignees.sort, assignees
706 706
    assert_select 'table.issues.sort-by-assigned-to.sort-asc'
707 707
  end
708
  
708

  
709 709
  def test_index_sort_by_assigned_to_desc
710 710
    get :index, :sort => 'assigned_to:desc'
711 711
    assert_response :success
......
762 762
    assert_equal hours.sort.reverse, hours
763 763
  end
764 764

  
765
  def test_index_sort_by_total_remaining_hours
766
    get :index, :sort => 'total_remaining_hours:desc'
767
    assert_response :success
768
    hours = assigns(:issues).collect(&:total_remaining_hours)
769
    assert_equal hours.sort.reverse, hours
770
  end
771

  
765 772
  def test_index_sort_by_user_custom_field
766 773
    cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'user')
767 774
    CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '2')
......
899 906
    assert_select 'table.issues td.total_estimated_hours'
900 907
  end
901 908

  
909
  def test_index_with_total_remaining_hours_column
910
    get :index, :set_filter => 1, :c => %w(subject total_remaining_hours)
911
    assert_select 'table.issues td.total_remaining_hours'
912
  end
913

  
902 914
  def test_index_should_not_show_spent_hours_column_without_permission
903 915
    Role.anonymous.remove_permission! :view_time_entries
904 916
    get :index, :set_filter => 1, :c => %w(subject spent_hours)
......
982 994
    assert_select 'input[type=checkbox][name=?][value=estimated_hours][checked=checked]', 't[]'
983 995
  end
984 996

  
997
  def test_index_with_remaining_hours_total
998
    Issue.delete_all
999
    Issue.generate!(:remaining_hours => 5.4)
1000
    Issue.generate!(:remaining_hours => 1.1)
1001

  
1002
    get :index, :t => %w(remaining_hours)
1003
    assert_response :success
1004
    assert_select '.query-totals'
1005
    assert_select '.total-for-remaining-hours span.value', :text => '6.50'
1006
    assert_select 'input[type=checkbox][name=?][value=remaining_hours][checked=checked]', 't[]'
1007
  end
1008

  
985 1009
  def test_index_with_grouped_query_and_estimated_hours_total
986 1010
    Issue.delete_all
987 1011
    Issue.generate!(:estimated_hours => 5.5, :category_id => 1)
......
1093 1117
  def test_index_should_not_include_new_issue_tab_for_project_without_trackers
1094 1118
    with_settings :new_item_menu_tab => '1' do
1095 1119
      Project.find(1).trackers.clear
1096
  
1120

  
1097 1121
      @request.session[:user_id] = 2
1098 1122
      get :index, :project_id => 1
1099 1123
      assert_select '#main-menu a.new-issue', 0
......
1105 1129
      role = Role.find(1)
1106 1130
      role.remove_permission! :add_issues
1107 1131
      role.add_permission! :copy_issues
1108
  
1132

  
1109 1133
      @request.session[:user_id] = 2
1110 1134
      get :index, :project_id => 1
1111 1135
      assert_select '#main-menu a.new-issue', 0
......
1614 1638
  end
1615 1639

  
1616 1640
  def test_show_export_to_pdf
1617
    issue = Issue.find(3) 
1641
    issue = Issue.find(3)
1618 1642
    assert issue.relations.select{|r| r.other_issue(issue).visible?}.present?
1619 1643
    get :show, :id => 3, :format => 'pdf'
1620 1644
    assert_response :success
......
2062 2086
    get :new, :project_id => 'invalid'
2063 2087
    assert_response 404
2064 2088
  end
2065
 
2089

  
2066 2090
  def test_new_with_parent_id_should_only_propose_valid_trackers
2067 2091
    @request.session[:user_id] = 2
2068 2092
    t = Tracker.find(3)
......
2158 2182
                            :priority_id => 5,
2159 2183
                            :start_date => '2010-11-07',
2160 2184
                            :estimated_hours => '',
2185
                            :remaining_hours => '',
2161 2186
                            :custom_field_values => {'2' => 'Value for field 2'}}
2162 2187
      end
2163 2188
    end
......
2170 2195
    assert_equal 2, issue.status_id
2171 2196
    assert_equal Date.parse('2010-11-07'), issue.start_date
2172 2197
    assert_nil issue.estimated_hours
2198
    assert_nil issue.remaining_hours
2173 2199
    v = issue.custom_values.where(:custom_field_id => 2).first
2174 2200
    assert_not_nil v
2175 2201
    assert_equal 'Value for field 2', v.value
......
2611 2637
                              :custom_field_values => {'2' => 'Value for field 2'}}
2612 2638
      end
2613 2639
      assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
2614
  
2640

  
2615 2641
      assert_equal 1, ActionMailer::Base.deliveries.size
2616 2642
    end
2617 2643
  end
......
3160 3186
  def test_get_edit_should_display_the_time_entry_form_with_log_time_permission
3161 3187
    @request.session[:user_id] = 2
3162 3188
    Role.find_by_name('Manager').update_attribute :permissions, [:view_issues, :edit_issues, :log_time]
3163
    
3189

  
3164 3190
    get :edit, :id => 1
3165 3191
    assert_select 'input[name=?]', 'time_entry[hours]'
3166 3192
  end
......
3168 3194
  def test_get_edit_should_not_display_the_time_entry_form_without_log_time_permission
3169 3195
    @request.session[:user_id] = 2
3170 3196
    Role.find_by_name('Manager').remove_permission! :log_time
3171
    
3197

  
3172 3198
    get :edit, :id => 1
3173 3199
    assert_select 'input[name=?]', 'time_entry[hours]', 0
3174 3200
  end
......
3813 3839
    assert_response :redirect
3814 3840
    assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
3815 3841
  end
3816
 
3842

  
3817 3843
  def test_put_update_should_redirect_with_previous_and_next_issue_ids_params
3818 3844
    @request.session[:user_id] = 2
3819 3845

  
......
3867 3893

  
3868 3894
      assert_select 'select[name=?]', 'issue[project_id]'
3869 3895
      assert_select 'input[name=?]', 'issue[parent_issue_id]'
3870
  
3896

  
3871 3897
      # Project specific custom field, date type
3872 3898
      field = CustomField.find(9)
3873 3899
      assert !field.is_for_all?
3874 3900
      assert_equal 'date', field.field_format
3875 3901
      assert_select 'input[name=?]', 'issue[custom_field_values][9]'
3876
  
3902

  
3877 3903
      # System wide custom field
3878 3904
      assert CustomField.find(1).is_for_all?
3879 3905
      assert_select 'select[name=?]', 'issue[custom_field_values][1]'
3880
  
3906

  
3881 3907
      # Be sure we don't display inactive IssuePriorities
3882 3908
      assert ! IssuePriority.find(15).active?
3883 3909
      assert_select 'select[name=?]', 'issue[priority_id]' do
......
4064 4090
                                       :issue => {:priority_id => '',
4065 4091
                                                  :assigned_to_id => group.id,
4066 4092
                                                  :custom_field_values => {'2' => ''}}
4067
  
4093

  
4068 4094
      assert_response 302
4069 4095
      assert_equal [group, group], Issue.where(:id => [1, 2]).collect {|i| i.assigned_to}
4070 4096
    end
......
4208 4234
    assert_equal 4.25, Issue.find(2).estimated_hours
4209 4235
  end
4210 4236

  
4237
  def test_bulk_update_remaining_hours
4238
    @request.session[:user_id] = 2
4239
    post :bulk_update, :ids => [1, 2], :issue => {:remaining_hours => 4.15}
4240

  
4241
    assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
4242
    assert_equal 4.15, Issue.find(1).remaining_hours
4243
    assert_equal 4.15, Issue.find(2).remaining_hours
4244
  end
4245

  
4211 4246
  def test_bulk_update_custom_field
4212 4247
    @request.session[:user_id] = 2
4213 4248
    # update issues priority
......
4370 4405
      assert_select 'option[value="2"]'
4371 4406
    end
4372 4407
  end
4373
  
4408

  
4374 4409
  def test_bulk_copy_to_another_project
4375 4410
    @request.session[:user_id] = 2
4376 4411
    assert_difference 'Issue.count', 2 do
......
4423 4458
                    :assigned_to_id => 3)
4424 4459
    ]
4425 4460
    assert_difference 'Issue.count', issues.size do
4426
      post :bulk_update, :ids => issues.map(&:id), :copy => '1', 
4461
      post :bulk_update, :ids => issues.map(&:id), :copy => '1',
4427 4462
           :issue => {
4428 4463
             :project_id => '', :tracker_id => '', :assigned_to_id => '',
4429 4464
             :status_id => '', :start_date => '', :due_date => ''
......
4451 4486
    @request.session[:user_id] = 2
4452 4487
    assert_difference 'Issue.count', 2 do
4453 4488
      assert_no_difference 'Project.find(1).issues.count' do
4454
        post :bulk_update, :ids => [1, 2], :copy => '1', 
4489
        post :bulk_update, :ids => [1, 2], :copy => '1',
4455 4490
             :issue => {
4456 4491
               :project_id => '2', :tracker_id => '', :assigned_to_id => '2',
4457 4492
               :status_id => '1', :start_date => '2009-12-01', :due_date => '2009-12-31'
test/integration/api_test/issues_test.rb
354 354
    end
355 355
  end
356 356

  
357
  test "GET /issues/:id.xml should contains total_estimated_hours and total_spent_hours" do
357
  test "GET /issues/:id.xml should contains total_estimated_hours, total_remaining_hours and total_spent_hours" do
358 358
    parent = Issue.find(3)
359
    child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0)
359
    child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0, :remaining_hours => 1.0)
360 360
    TimeEntry.create!(:project => child.project, :issue => child, :user => child.author, :spent_on => child.author.today,
361 361
                      :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id)
362 362
    get '/issues/3.xml'
......
365 365
    assert_select 'issue' do
366 366
      assert_select 'estimated_hours',       parent.estimated_hours.to_s
367 367
      assert_select 'total_estimated_hours', (parent.estimated_hours.to_f + 3.0).to_s
368
      assert_select 'remaining_hours',       parent.remaining_hours.to_s
369
      assert_select 'total_remaining_hours', (parent.remaining_hours.to_f + 1.0).to_s
368 370
      assert_select 'spent_hours',           parent.spent_hours.to_s
369 371
      assert_select 'total_spent_hours',     (parent.spent_hours.to_f + 2.5).to_s
370 372
    end
371 373
  end
372 374

  
373
  test "GET /issues/:id.xml should contains total_estimated_hours, and should not contains spent_hours and total_spent_hours when permission does not exists" do
375
  test "GET /issues/:id.xml should contains total_estimated_hours and total_remaining_hours, and should not contains spent_hours and total_spent_hours when permission does not exists" do
374 376
    parent = Issue.find(3)
375
    child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0)
377
    child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0, :remaining_hours => 1.0)
376 378
    # remove permission!
377 379
    Role.anonymous.remove_permission! :view_time_entries
378 380
    #Role.all.each { |role| role.remove_permission! :view_time_entries }
......
382 384
    assert_select 'issue' do
383 385
      assert_select 'estimated_hours',       parent.estimated_hours.to_s
384 386
      assert_select 'total_estimated_hours', (parent.estimated_hours.to_f + 3.0).to_s
387
      assert_select 'remaining_hours',       parent.remaining_hours.to_s
388
      assert_select 'total_remaining_hours', (parent.remaining_hours.to_f + 1.0).to_s
385 389
      assert_select 'spent_hours',           false
386 390
      assert_select 'total_spent_hours',     false
387 391
    end
388 392
  end
389 393

  
390
  test "GET /issues/:id.json should contains total_estimated_hours and total_spent_hours" do
394
  test "GET /issues/:id.json should contains total_estimated_hours, total_remaining_hours and total_spent_hours" do
391 395
    parent = Issue.find(3)
392
    child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0)
396
    child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0, :remaining_hours => 1.0)
393 397
    TimeEntry.create!(:project => child.project, :issue => child, :user => child.author, :spent_on => child.author.today,
394 398
                      :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id)
395 399
    get '/issues/3.json'
......
398 402
    json = ActiveSupport::JSON.decode(response.body)
399 403
    assert_equal parent.estimated_hours, json['issue']['estimated_hours']
400 404
    assert_equal (parent.estimated_hours.to_f + 3.0), json['issue']['total_estimated_hours']
405
    assert_equal parent.remaining_hours, json['issue']['remaining_hours']
406
    assert_equal (parent.remaining_hours.to_f + 1.0), json['issue']['total_remaining_hours']
401 407
    assert_equal parent.spent_hours, json['issue']['spent_hours']
402 408
    assert_equal (parent.spent_hours.to_f + 2.5), json['issue']['total_spent_hours']
403 409
  end
404 410

  
405
  test "GET /issues/:id.json should contains total_estimated_hours, and should not contains spent_hours and total_spent_hours when permission does not exists" do
411
  test "GET /issues/:id.json should contains total_estimated_hours and total_remaining_hours, and should not contains spent_hours and total_spent_hours when permission does not exists" do
406 412
    parent = Issue.find(3)
407
    child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0)
413
    child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0, :remaining_hours => 1.0)
408 414
    # remove permission!
409 415
    Role.anonymous.remove_permission! :view_time_entries
410 416
    #Role.all.each { |role| role.remove_permission! :view_time_entries }
......
414 420
    json = ActiveSupport::JSON.decode(response.body)
415 421
    assert_equal parent.estimated_hours, json['issue']['estimated_hours']
416 422
    assert_equal (parent.estimated_hours.to_f + 3.0), json['issue']['total_estimated_hours']
423
    assert_equal parent.remaining_hours, json['issue']['remaining_hours']
424
    assert_equal (parent.remaining_hours.to_f + 1.0), json['issue']['total_remaining_hours']
417 425
    assert_equal nil, json['issue']['spent_hours']
418 426
    assert_equal nil, json['issue']['total_spent_hours']
419 427
  end
test/unit/helpers/issues_helper_test.rb
203 203
    assert_match '6.30', show_detail(detail, true)
204 204
  end
205 205

  
206
  test 'show_detail should show old and new values with a remaining hours attribute' do
207
    detail = JournalDetail.new(:property => 'attr', :prop_key => 'remaining_hours',
208
                               :old_value => '6.3', :value => '5')
209
    assert_match '5.00', show_detail(detail, true)
210
    assert_match '6.30', show_detail(detail, true)
211
  end
212

  
206 213
  test 'show_detail should not show values with a description attribute' do
207 214
    detail = JournalDetail.new(:property => 'attr', :prop_key => 'description',
208 215
                               :old_value => 'Foo', :value => 'Bar')
test/unit/issue_subtasking_test.rb
28 28
  def test_leaf_planning_fields_should_be_editable
29 29
    issue = Issue.generate!
30 30
    user = User.find(1)
31
    %w(priority_id done_ratio start_date due_date estimated_hours).each do |attribute|
31
    %w(priority_id done_ratio start_date due_date estimated_hours remaining_hours).each do |attribute|
32 32
      assert issue.safe_attribute?(attribute, user)
33 33
    end
34 34
  end
......
147 147
      assert_equal 20, parent.reload.done_ratio
148 148
      parent.generate_child!(:done_ratio => 70)
149 149
      assert_equal 45, parent.reload.done_ratio
150
  
150

  
151 151
      child = parent.generate_child!(:done_ratio => 0)
152 152
      assert_equal 30, parent.reload.done_ratio
153
  
153

  
154 154
      child.generate_child!(:done_ratio => 30)
155 155
      assert_equal 30, child.reload.done_ratio
156 156
      assert_equal 40, parent.reload.done_ratio
......
330 330
    parent.generate_child!(:estimated_hours => 7)
331 331
    assert_equal 12, parent.reload.total_estimated_hours
332 332
  end
333

  
334
  def test_parent_total_remaining_hours_should_be_sum_of_descendants
335
    parent = Issue.generate!
336
    parent.generate_child!(:remaining_hours => nil)
337
    assert_equal 0, parent.reload.total_remaining_hours
338
    parent.generate_child!(:remaining_hours => 5)
339
    assert_equal 5, parent.reload.total_remaining_hours
340
    parent.generate_child!(:remaining_hours => 7)
341
    assert_equal 12, parent.reload.total_remaining_hours
342
  end
333 343
end
test/unit/issue_test.rb
56 56
    issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
57 57
                      :status_id => 1, :priority => IssuePriority.all.first,
58 58
                      :subject => 'test_create',
59
                      :description => 'IssueTest#test_create', :estimated_hours => '1:30')
59
                      :description => 'IssueTest#test_create', :estimated_hours => '1:30', :remaining_hours => '1')
60 60
    assert issue.save
61 61
    issue.reload
62 62
    assert_equal 1.5, issue.estimated_hours
63
    assert_equal 1, issue.remaining_hours
63 64
  end
64 65

  
65 66
  def test_create_minimal
......
68 69
    assert_equal issue.tracker.default_status, issue.status
69 70
    assert issue.description.nil?
70 71
    assert_nil issue.estimated_hours
72
    assert_nil issue.remaining_hours
71 73
  end
72 74

  
73 75
  def test_create_with_all_fields_disabled
......
134 136
    end
135 137
  end
136 138

  
139
  def test_remaining_hours_update_with_negative_value_should_set_to_zero
140
    set_language_if_valid 'en'
141
    ['-4'].each do |invalid|
142
      issue = Issue.new(:remaining_hours => invalid)
143
      assert_equal 0, issue.remaining_hours
144
    end
145
  end
146

  
147
  def test_remaining_hours_should_be_set_from_estimated_hours_when_is_empty
148
      issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
149
                      :status_id => 1, :priority => IssuePriority.all.first,
150
                      :subject => 'test_create',
151
                      :description => 'IssueTest#test_create', :estimated_hours => '1:30')
152
      assert issue.save
153
      assert_equal 1.5, issue.remaining_hours
154
  end
155

  
137 156
  def test_create_with_required_custom_field
138 157
    set_language_if_valid 'en'
139 158
    field = IssueCustomField.find_by_name('Database')
......
2885 2904
    assert_equal false, issue.closing?
2886 2905
  end
2887 2906

  
2907
  def test_closing_should_set_remaining_hours_to_zero
2908
    issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
2909
                      :status_id => 1, :priority => IssuePriority.all.first,
2910
                      :subject => 'test_create',
2911
                      :description => 'IssueTest#test_create', :estimated_hours => '1:30', :remaining_hours => '1')
2912
    assert_equal 1, issue.remaining_hours
2913
    issue.status_id = 5
2914
    issue.save!
2915
    assert_equal 0, issue.remaining_hours
2916
  end
2917

  
2888 2918
  def test_reopening_should_return_true_when_reopening_an_issue
2889 2919
    issue = Issue.find(8)
2890 2920
    issue.status = IssueStatus.find(6)
test/unit/mail_handler_test.rb
42 42

  
43 43
  def test_add_issue_with_specific_overrides
44 44
    issue = submit_email('ticket_on_given_project.eml',
45
      :allow_override => ['status', 'start_date', 'due_date', 'assigned_to', 'fixed_version', 'estimated_hours', 'done_ratio']
45
      :allow_override => ['status', 'start_date', 'due_date', 'assigned_to', 'fixed_version', 'estimated_hours', 'remaining_hours', 'done_ratio']
46 46
    )
47 47
    assert issue.is_a?(Issue)
48 48
    assert !issue.new_record?
......
58 58
    assert_equal User.find_by_login('jsmith'), issue.assigned_to
59 59
    assert_equal Version.find_by_name('Alpha'), issue.fixed_version
60 60
    assert_equal 2.5, issue.estimated_hours
61
    assert_equal 1, issue.remaining_hours
61 62
    assert_equal 30, issue.done_ratio
62 63
    # keywords should be removed from the email body
63 64
    assert !issue.description.match(/^Project:/i)
64 65
    assert !issue.description.match(/^Status:/i)
65 66
    assert !issue.description.match(/^Start Date:/i)
67
    assert !issue.description.match(/^remaining hours:/i)
66 68
  end
67 69

  
68 70
  def test_add_issue_with_all_overrides
......
79 81
    assert_equal User.find_by_login('jsmith'), issue.assigned_to
80 82
    assert_equal Version.find_by_name('Alpha'), issue.fixed_version
81 83
    assert_equal 2.5, issue.estimated_hours
84
    assert_equal 1, issue.remaining_hours
82 85
    assert_equal 30, issue.done_ratio
83 86
  end
84 87

  
......
100 103
    assert_nil issue.assigned_to
101 104
    assert_nil issue.fixed_version
102 105
    assert_nil issue.estimated_hours
106
    assert_nil issue.remaining_hours
103 107
    assert_equal 0, issue.done_ratio
104 108
  end
105 109

  
test/unit/query_test.rb
1256 1256

  
1257 1257
  def test_set_totalable_names
1258 1258
    q = IssueQuery.new
1259
    q.totalable_names = ['estimated_hours', :spent_hours, '']
1260
    assert_equal [:estimated_hours, :spent_hours], q.totalable_columns.map(&:name)
1259
    q.totalable_names = ['estimated_hours', 'remaining_hours', :spent_hours, '']
1260
    assert_equal [:estimated_hours, :remaining_hours, :spent_hours], q.totalable_columns.map(&:name)
1261 1261
  end
1262 1262

  
1263 1263
  def test_totalable_columns_should_default_to_settings
......
1272 1272
    assert_include :estimated_hours, q.available_totalable_columns.map(&:name)
1273 1273
  end
1274 1274

  
1275
  def test_available_totalable_columns_should_include_remaining_hours
1276
    q = IssueQuery.new
1277
    assert_include :remaining_hours, q.available_totalable_columns.map(&:name)
1278
  end
1279

  
1275 1280
  def test_available_totalable_columns_should_include_spent_hours
1276 1281
    User.current = User.find(1)
1277 1282

  
......
1314 1319
    )
1315 1320
  end
1316 1321

  
1322
  def test_total_for_remaining_hours
1323
    Issue.delete_all
1324
    Issue.generate!(:remaining_hours => 5.5)
1325
    Issue.generate!(:remaining_hours => 1.1)
1326
    Issue.generate!
1327

  
1328
    q = IssueQuery.new
1329
    assert_equal 6.6, q.total_for(:remaining_hours)
1330
  end
1331

  
1332
  def test_total_by_group_for_remaining_hours
1333
    Issue.delete_all
1334
    Issue.generate!(:remaining_hours => 5.5, :assigned_to_id => 2)
1335
    Issue.generate!(:remaining_hours => 1.1, :assigned_to_id => 3)
1336
    Issue.generate!(:remaining_hours => 3.5)
1337

  
1338
    q = IssueQuery.new(:group_by => 'assigned_to')
1339
    assert_equal(
1340
      {nil => 3.5, User.find(2) => 5.5, User.find(3) => 1.1},
1341
      q.total_by_group_for(:remaining_hours)
1342
    )
1343
  end
1344

  
1317 1345
  def test_total_for_spent_hours
1318 1346
    TimeEntry.delete_all
1319 1347
    TimeEntry.generate!(:hours => 5.5)
(4-4/10)