diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 72f33da..5232fce 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -118,6 +118,18 @@ module IssuesHelper end end + def issue_remaining_hours_details(issue) + if issue.total_remaining_hours.present? + if issue.total_remaining_hours == issue.remaining_hours + l_hours_short(issue.remaining_hours) + else + s = issue.remaining_hours.present? ? l_hours_short(issue.remaining_hours) : "" + s << " (#{l(:label_total)}: #{l_hours_short(issue.total_remaining_hours)})" + s.html_safe + end + end + end + def issue_spent_hours_details(issue) if issue.total_spent_hours > 0 path = project_time_entries_path(issue.project, :issue_id => "~#{issue.id}") @@ -345,7 +357,7 @@ module IssuesHelper value = find_name_by_reflection(field, detail.value) old_value = find_name_by_reflection(field, detail.old_value) - when 'estimated_hours' + when 'estimated_hours', 'remaining_hours' value = l_hours_short(detail.value.to_f) unless detail.value.blank? old_value = l_hours_short(detail.old_value.to_f) unless detail.old_value.blank? diff --git a/app/models/issue.rb b/app/models/issue.rb old mode 100644 new mode 100755 index 92cfad3..25fe74a --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -71,6 +71,7 @@ class Issue < ActiveRecord::Base validates_length_of :subject, :maximum => 255 validates_inclusion_of :done_ratio, :in => 0..100 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid} + validates :remaining_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid} validates :start_date, :date => true validates :due_date, :date => true validate :validate_issue, :validate_required_fields @@ -107,7 +108,7 @@ class Issue < ActiveRecord::Base before_validation :clear_disabled_fields before_create :default_assign before_save :close_duplicates, :update_done_ratio_from_issue_status, - :force_updated_on_change, :update_closed_on, :set_assigned_to_was + :force_updated_on_change, :update_closed_on, :set_assigned_to_was, :update_remaining_hours after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?} after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :delete_selected_attachments, :create_journal @@ -244,6 +245,7 @@ class Issue < ActiveRecord::Base @spent_hours = nil @total_spent_hours = nil @total_estimated_hours = nil + @total_remaining_hours = nil base_reload(*args) end @@ -430,6 +432,13 @@ class Issue < ActiveRecord::Base write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h) end + def remaining_hours=(h) + h = h.is_a?(String) ? h.to_hours : h + # remaining time cannot be less than zero + h = 0 if !h.nil? && h < 0 + write_attribute :remaining_hours, h + end + safe_attributes 'project_id', 'tracker_id', 'status_id', @@ -443,6 +452,7 @@ class Issue < ActiveRecord::Base 'due_date', 'done_ratio', 'estimated_hours', + 'remaining_hours', 'custom_field_values', 'custom_fields', 'lock_version', @@ -1030,6 +1040,23 @@ class Issue < ActiveRecord::Base end end + def remaining_hours + @remaining_hours = read_attribute(:remaining_hours) + if @remaining_hours.nil? && estimated_hours + @remaining_hours = estimated_hours + else + @remaining_hours + end + end + + def total_remaining_hours + if leaf? + remaining_hours + else + @total_remaining_hours ||= self_and_descendants.sum(:remaining_hours) + end + end + def relations @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort) end @@ -1703,6 +1730,13 @@ class Issue < ActiveRecord::Base end end + # Callback for setting remaining time to zero when the issue is closed. + def update_remaining_hours + if closing? && safe_attribute?('remaining_hours') && self.remaining_hours.to_f > 0 + self.remaining_hours = 0 + end + end + # Saves the changes in a Journal # Called after_save def create_journal diff --git a/app/models/issue_import.rb b/app/models/issue_import.rb index 4ecd4b5..1c310d1 100644 --- a/app/models/issue_import.rb +++ b/app/models/issue_import.rb @@ -158,6 +158,9 @@ class IssueImport < Import if estimated_hours = row_value(row, 'estimated_hours') attributes['estimated_hours'] = estimated_hours end + if remaining_hours = row_value(row, 'remaining_hours') + attributes['remaining_hours'] = remaining_hours + end if done_ratio = row_value(row, 'done_ratio') attributes['done_ratio'] = done_ratio end diff --git a/app/models/issue_query.rb b/app/models/issue_query.rb index 137764e..a793d05 100644 --- a/app/models/issue_query.rb +++ b/app/models/issue_query.rb @@ -36,10 +36,15 @@ class IssueQuery < Query QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"), QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"), QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours", :totalable => true), + QueryColumn.new(:remaining_hours, :sortable => "#{Issue.table_name}.remaining_hours", :totalable => true), QueryColumn.new(:total_estimated_hours, :sortable => "COALESCE((SELECT SUM(estimated_hours) FROM #{Issue.table_name} subtasks" + " WHERE subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)", :default_order => 'desc'), + QueryColumn.new(:total_remaining_hours, + :sortable => "COALESCE((SELECT SUM(remaining_hours) FROM #{Issue.table_name} subtasks" + + " WHERE subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)", + :default_order => 'desc'), QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true), QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'), QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'), @@ -174,6 +179,7 @@ class IssueQuery < Query add_available_filter "start_date", :type => :date add_available_filter "due_date", :type => :date add_available_filter "estimated_hours", :type => :float + add_available_filter "remaining_hours", :type => :float add_available_filter "done_ratio", :type => :integer if User.current.allowed_to?(:set_issues_private, nil, :global => true) || @@ -285,6 +291,11 @@ class IssueQuery < Query map_total(scope.sum(:estimated_hours)) {|t| t.to_f.round(2)} end + # Returns sum of all the issue's remaining_hours + def total_for_remaining_hours(scope) + map_total(scope.sum(:remaining_hours)) {|t| t.to_f.round(2)} + end + # Returns sum of all the issue's time entries hours def total_for_spent_hours(scope) total = if group_by_column.try(:name) == :project diff --git a/app/models/mail_handler.rb b/app/models/mail_handler.rb index 2884131..e427e67 100644 --- a/app/models/mail_handler.rb +++ b/app/models/mail_handler.rb @@ -424,6 +424,7 @@ class MailHandler < ActionMailer::Base 'start_date' => get_keyword(:start_date, :format => '\d{4}-\d{2}-\d{2}'), 'due_date' => get_keyword(:due_date, :format => '\d{4}-\d{2}-\d{2}'), 'estimated_hours' => get_keyword(:estimated_hours), + 'remaining_hours' => get_keyword(:remaining_hours), 'done_ratio' => get_keyword(:done_ratio, :format => '(\d|10)?0') }.delete_if {|k, v| v.blank? } diff --git a/app/models/tracker.rb b/app/models/tracker.rb index 2dcbaed..4b4edbb 100644 --- a/app/models/tracker.rb +++ b/app/models/tracker.rb @@ -21,7 +21,7 @@ class Tracker < ActiveRecord::Base CORE_FIELDS_UNDISABLABLE = %w(project_id tracker_id subject description priority_id is_private).freeze # Fields that can be disabled # Other (future) fields should be appended, not inserted! - CORE_FIELDS = %w(assigned_to_id category_id fixed_version_id parent_issue_id start_date due_date estimated_hours done_ratio).freeze + 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 CORE_FIELDS_ALL = (CORE_FIELDS_UNDISABLABLE + CORE_FIELDS).freeze before_destroy :check_integrity diff --git a/app/views/imports/_fields_mapping.html.erb b/app/views/imports/_fields_mapping.html.erb index 0e1d455..76826e9 100644 --- a/app/views/imports/_fields_mapping.html.erb +++ b/app/views/imports/_fields_mapping.html.erb @@ -82,6 +82,10 @@ <%= mapping_select_tag @import, 'estimated_hours' %>

+ + <%= mapping_select_tag @import, 'remaining_hours' %> +

+

<%= mapping_select_tag @import, 'done_ratio' %>

diff --git a/app/views/issues/_attributes.html.erb b/app/views/issues/_attributes.html.erb index 960256e..43ab5ef 100644 --- a/app/views/issues/_attributes.html.erb +++ b/app/views/issues/_attributes.html.erb @@ -64,13 +64,18 @@

<% end %> +<% if @issue.safe_attribute?('done_ratio') && Issue.use_field_for_done_ratio? %> +

<%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }), :required => @issue.required_attribute?('done_ratio') %>

+<% end %> + <% if @issue.safe_attribute? 'estimated_hours' %>

<%= f.text_field :estimated_hours, :size => 3, :required => @issue.required_attribute?('estimated_hours') %> <%= l(:field_hours) %>

<% end %> -<% if @issue.safe_attribute?('done_ratio') && Issue.use_field_for_done_ratio? %> -

<%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }), :required => @issue.required_attribute?('done_ratio') %>

+<% if @issue.safe_attribute? 'remaining_hours' %> +

<%= f.text_field :remaining_hours, :size => 3, :required => @issue.required_attribute?('remaining_hours') %> <%= l(:field_hours) %>

<% end %> + diff --git a/app/views/issues/_edit.html.erb b/app/views/issues/_edit.html.erb old mode 100644 new mode 100755 index 3291ba7..36cec27 --- a/app/views/issues/_edit.html.erb +++ b/app/views/issues/_edit.html.erb @@ -31,11 +31,11 @@
<%= l(:field_notes) %> <%= f.text_area :notes, :cols => 60, :rows => 10, :class => 'wiki-edit', :no_label => true %> <%= wikitoolbar_for 'issue_notes' %> - + <% if @issue.safe_attribute? 'private_notes' %> <%= f.check_box :private_notes, :no_label => true %> <% end %> - + <%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %>
diff --git a/app/views/issues/bulk_edit.html.erb b/app/views/issues/bulk_edit.html.erb index 29b4881..89b4e23 100644 --- a/app/views/issues/bulk_edit.html.erb +++ b/app/views/issues/bulk_edit.html.erb @@ -170,6 +170,14 @@

<% end %> +<% if @safe_attributes.include?('remaining_hours') %> +

+ + <%= text_field_tag 'issue[remaining_hours]', '', :value => @issue_params[:remaining_hours], :size => 10 %> + +

+<% end %> + <% if @safe_attributes.include?('done_ratio') && Issue.use_field_for_done_ratio? %>

diff --git a/app/views/issues/index.api.rsb b/app/views/issues/index.api.rsb index 7660ccb..8d8a938 100644 --- a/app/views/issues/index.api.rsb +++ b/app/views/issues/index.api.rsb @@ -19,6 +19,7 @@ api.array :issues, api_meta(:total_count => @issue_count, :offset => @offset, :l api.done_ratio issue.done_ratio api.is_private issue.is_private api.estimated_hours issue.estimated_hours + api.remaining_hours issue.remaining_hours render_api_custom_values issue.visible_custom_field_values, api diff --git a/app/views/issues/show.api.rsb b/app/views/issues/show.api.rsb index f474ed9..7e7fe1d 100644 --- a/app/views/issues/show.api.rsb +++ b/app/views/issues/show.api.rsb @@ -17,7 +17,9 @@ api.issue do api.done_ratio @issue.done_ratio api.is_private @issue.is_private api.estimated_hours @issue.estimated_hours + api.remaining_hours @issue.remaining_hours api.total_estimated_hours @issue.total_estimated_hours + api.total_remaining_hours @issue.total_remaining_hours if User.current.allowed_to?(:view_time_entries, @project) api.spent_hours(@issue.spent_hours) api.total_spent_hours(@issue.total_spent_hours) diff --git a/app/views/issues/show.html.erb b/app/views/issues/show.html.erb old mode 100644 new mode 100755 index b9e5111..480d35f --- a/app/views/issues/show.html.erb +++ b/app/views/issues/show.html.erb @@ -60,6 +60,9 @@ unless @issue.disabled_core_fields.include?('estimated_hours') rows.right l(:field_estimated_hours), issue_estimated_hours_details(@issue), :class => 'estimated-hours' end + unless @issue.disabled_core_fields.include?('remaining_hours') + rows.right l(:field_remaining_hours), issue_remaining_hours_details(@issue), :class => 'remaining-hours' + end if User.current.allowed_to_view_all_time_entries?(@project) if @issue.total_spent_hours > 0 rows.right l(:label_spent_time), issue_spent_hours_details(@issue), :class => 'spent-time' diff --git a/config/locales/en.yml b/config/locales/en.yml old mode 100644 new mode 100755 index 56a06c7..75b576a --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -365,6 +365,8 @@ en: field_default_version: Default version field_remote_ip: IP address field_textarea_font: Font used for text areas + field_remaining_hours: Remaining time + field_total_remaining_hours: Total remaining time setting_app_title: Application title setting_app_subtitle: Application subtitle diff --git a/db/migrate/20160920184857_add_remaining_hours_to_issues.rb b/db/migrate/20160920184857_add_remaining_hours_to_issues.rb new file mode 100644 index 0000000..ecfbe11 --- /dev/null +++ b/db/migrate/20160920184857_add_remaining_hours_to_issues.rb @@ -0,0 +1,10 @@ +class AddRemainingHoursToIssues < ActiveRecord::Migration + def self.up + add_column :issues, :remaining_hours, :float + Issue.where("estimated_hours > ?", 0).update_all("remaining_hours = estimated_hours") + end + + def self.down + remove_column :issues, :remaining_hours + end +end diff --git a/test/fixtures/mail_handler/ticket_on_given_project.eml b/test/fixtures/mail_handler/ticket_on_given_project.eml old mode 100644 new mode 100755 index 7c0fe32..1bf97ea --- a/test/fixtures/mail_handler/ticket_on_given_project.eml +++ b/test/fixtures/mail_handler/ticket_on_given_project.eml @@ -18,16 +18,16 @@ X-MSMail-Priority: Normal X-Mailer: Microsoft Outlook Express 6.00.2900.2869 X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 -Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet -turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus -blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti -sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In -in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras -sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum -id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus -eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique -sed, mauris --- Pellentesque habitant morbi tristique senectus et netus et -malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris --- Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse platea dictumst. Project: onlinestore @@ -37,6 +37,7 @@ Start Date:2010-01-01 Assigned to: John Smith fixed version: alpha estimated hours: 2.5 +remaining hours: 1 done ratio: 30 --- This line starts with a delimiter and should not be stripped @@ -51,10 +52,10 @@ This paragraph is between delimiters. This paragraph is after the delimiter so it shouldn't appear. -Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque -sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. -Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, -dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, -massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb old mode 100644 new mode 100755 index a588f6a..55a0406 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -561,8 +561,8 @@ class IssuesControllerTest < Redmine::ControllerTest str_big5 = "\xa4@\xa4\xeb".force_encoding('Big5') issue = Issue.generate!(:subject => str_utf8) - get :index, :project_id => 1, - :f => ['subject'], + get :index, :project_id => 1, + :f => ['subject'], :op => '=', :values => [str_utf8], :format => 'csv' assert_equal 'text/csv; header=present', @response.content_type @@ -580,8 +580,8 @@ class IssuesControllerTest < Redmine::ControllerTest str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85".force_encoding('UTF-8') issue = Issue.generate!(:subject => str_utf8) - get :index, :project_id => 1, - :f => ['subject'], + get :index, :project_id => 1, + :f => ['subject'], :op => '=', :values => [str_utf8], :c => ['status', 'subject'], :format => 'csv', @@ -603,8 +603,8 @@ class IssuesControllerTest < Redmine::ControllerTest str1 = "test_index_csv_tw" issue = Issue.generate!(:subject => str1, :estimated_hours => '1234.5') - get :index, :project_id => 1, - :f => ['subject'], + get :index, :project_id => 1, + :f => ['subject'], :op => '=', :values => [str1], :c => ['estimated_hours', 'subject'], :format => 'csv', @@ -620,8 +620,8 @@ class IssuesControllerTest < Redmine::ControllerTest str1 = "test_index_csv_fr" issue = Issue.generate!(:subject => str1, :estimated_hours => '1234.5') - get :index, :project_id => 1, - :f => ['subject'], + get :index, :project_id => 1, + :f => ['subject'], :op => '=', :values => [str1], :c => ['estimated_hours', 'subject'], :format => 'csv', @@ -696,7 +696,7 @@ class IssuesControllerTest < Redmine::ControllerTest assert_response :success end end - + def test_index_sort_by_assigned_to get :index, :sort => 'assigned_to' assert_response :success @@ -705,7 +705,7 @@ class IssuesControllerTest < Redmine::ControllerTest assert_equal assignees.sort, assignees assert_select 'table.issues.sort-by-assigned-to.sort-asc' end - + def test_index_sort_by_assigned_to_desc get :index, :sort => 'assigned_to:desc' assert_response :success @@ -762,6 +762,13 @@ class IssuesControllerTest < Redmine::ControllerTest assert_equal hours.sort.reverse, hours end + def test_index_sort_by_total_remaining_hours + get :index, :sort => 'total_remaining_hours:desc' + assert_response :success + hours = assigns(:issues).collect(&:total_remaining_hours) + assert_equal hours.sort.reverse, hours + end + def test_index_sort_by_user_custom_field cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'user') CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '2') @@ -899,6 +906,11 @@ class IssuesControllerTest < Redmine::ControllerTest assert_select 'table.issues td.total_estimated_hours' end + def test_index_with_total_remaining_hours_column + get :index, :set_filter => 1, :c => %w(subject total_remaining_hours) + assert_select 'table.issues td.total_remaining_hours' + end + def test_index_should_not_show_spent_hours_column_without_permission Role.anonymous.remove_permission! :view_time_entries get :index, :set_filter => 1, :c => %w(subject spent_hours) @@ -982,6 +994,18 @@ class IssuesControllerTest < Redmine::ControllerTest assert_select 'input[type=checkbox][name=?][value=estimated_hours][checked=checked]', 't[]' end + def test_index_with_remaining_hours_total + Issue.delete_all + Issue.generate!(:remaining_hours => 5.4) + Issue.generate!(:remaining_hours => 1.1) + + get :index, :t => %w(remaining_hours) + assert_response :success + assert_select '.query-totals' + assert_select '.total-for-remaining-hours span.value', :text => '6.50' + assert_select 'input[type=checkbox][name=?][value=remaining_hours][checked=checked]', 't[]' + end + def test_index_with_grouped_query_and_estimated_hours_total Issue.delete_all Issue.generate!(:estimated_hours => 5.5, :category_id => 1) @@ -1093,7 +1117,7 @@ class IssuesControllerTest < Redmine::ControllerTest def test_index_should_not_include_new_issue_tab_for_project_without_trackers with_settings :new_item_menu_tab => '1' do Project.find(1).trackers.clear - + @request.session[:user_id] = 2 get :index, :project_id => 1 assert_select '#main-menu a.new-issue', 0 @@ -1105,7 +1129,7 @@ class IssuesControllerTest < Redmine::ControllerTest role = Role.find(1) role.remove_permission! :add_issues role.add_permission! :copy_issues - + @request.session[:user_id] = 2 get :index, :project_id => 1 assert_select '#main-menu a.new-issue', 0 @@ -1614,7 +1638,7 @@ class IssuesControllerTest < Redmine::ControllerTest end def test_show_export_to_pdf - issue = Issue.find(3) + issue = Issue.find(3) assert issue.relations.select{|r| r.other_issue(issue).visible?}.present? get :show, :id => 3, :format => 'pdf' assert_response :success @@ -2062,7 +2086,7 @@ class IssuesControllerTest < Redmine::ControllerTest get :new, :project_id => 'invalid' assert_response 404 end - + def test_new_with_parent_id_should_only_propose_valid_trackers @request.session[:user_id] = 2 t = Tracker.find(3) @@ -2158,6 +2182,7 @@ class IssuesControllerTest < Redmine::ControllerTest :priority_id => 5, :start_date => '2010-11-07', :estimated_hours => '', + :remaining_hours => '', :custom_field_values => {'2' => 'Value for field 2'}} end end @@ -2170,6 +2195,7 @@ class IssuesControllerTest < Redmine::ControllerTest assert_equal 2, issue.status_id assert_equal Date.parse('2010-11-07'), issue.start_date assert_nil issue.estimated_hours + assert_nil issue.remaining_hours v = issue.custom_values.where(:custom_field_id => 2).first assert_not_nil v assert_equal 'Value for field 2', v.value @@ -2611,7 +2637,7 @@ class IssuesControllerTest < Redmine::ControllerTest :custom_field_values => {'2' => 'Value for field 2'}} end assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id - + assert_equal 1, ActionMailer::Base.deliveries.size end end @@ -3160,7 +3186,7 @@ class IssuesControllerTest < Redmine::ControllerTest def test_get_edit_should_display_the_time_entry_form_with_log_time_permission @request.session[:user_id] = 2 Role.find_by_name('Manager').update_attribute :permissions, [:view_issues, :edit_issues, :log_time] - + get :edit, :id => 1 assert_select 'input[name=?]', 'time_entry[hours]' end @@ -3168,7 +3194,7 @@ class IssuesControllerTest < Redmine::ControllerTest def test_get_edit_should_not_display_the_time_entry_form_without_log_time_permission @request.session[:user_id] = 2 Role.find_by_name('Manager').remove_permission! :log_time - + get :edit, :id => 1 assert_select 'input[name=?]', 'time_entry[hours]', 0 end @@ -3813,7 +3839,7 @@ class IssuesControllerTest < Redmine::ControllerTest assert_response :redirect assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id end - + def test_put_update_should_redirect_with_previous_and_next_issue_ids_params @request.session[:user_id] = 2 @@ -3867,17 +3893,17 @@ class IssuesControllerTest < Redmine::ControllerTest assert_select 'select[name=?]', 'issue[project_id]' assert_select 'input[name=?]', 'issue[parent_issue_id]' - + # Project specific custom field, date type field = CustomField.find(9) assert !field.is_for_all? assert_equal 'date', field.field_format assert_select 'input[name=?]', 'issue[custom_field_values][9]' - + # System wide custom field assert CustomField.find(1).is_for_all? assert_select 'select[name=?]', 'issue[custom_field_values][1]' - + # Be sure we don't display inactive IssuePriorities assert ! IssuePriority.find(15).active? assert_select 'select[name=?]', 'issue[priority_id]' do @@ -4064,7 +4090,7 @@ class IssuesControllerTest < Redmine::ControllerTest :issue => {:priority_id => '', :assigned_to_id => group.id, :custom_field_values => {'2' => ''}} - + assert_response 302 assert_equal [group, group], Issue.where(:id => [1, 2]).collect {|i| i.assigned_to} end @@ -4208,6 +4234,15 @@ class IssuesControllerTest < Redmine::ControllerTest assert_equal 4.25, Issue.find(2).estimated_hours end + def test_bulk_update_remaining_hours + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1, 2], :issue => {:remaining_hours => 4.15} + + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook' + assert_equal 4.15, Issue.find(1).remaining_hours + assert_equal 4.15, Issue.find(2).remaining_hours + end + def test_bulk_update_custom_field @request.session[:user_id] = 2 # update issues priority @@ -4370,7 +4405,7 @@ class IssuesControllerTest < Redmine::ControllerTest assert_select 'option[value="2"]' end end - + def test_bulk_copy_to_another_project @request.session[:user_id] = 2 assert_difference 'Issue.count', 2 do @@ -4423,7 +4458,7 @@ class IssuesControllerTest < Redmine::ControllerTest :assigned_to_id => 3) ] assert_difference 'Issue.count', issues.size do - post :bulk_update, :ids => issues.map(&:id), :copy => '1', + post :bulk_update, :ids => issues.map(&:id), :copy => '1', :issue => { :project_id => '', :tracker_id => '', :assigned_to_id => '', :status_id => '', :start_date => '', :due_date => '' @@ -4451,7 +4486,7 @@ class IssuesControllerTest < Redmine::ControllerTest @request.session[:user_id] = 2 assert_difference 'Issue.count', 2 do assert_no_difference 'Project.find(1).issues.count' do - post :bulk_update, :ids => [1, 2], :copy => '1', + post :bulk_update, :ids => [1, 2], :copy => '1', :issue => { :project_id => '2', :tracker_id => '', :assigned_to_id => '2', :status_id => '1', :start_date => '2009-12-01', :due_date => '2009-12-31' diff --git a/test/integration/api_test/issues_test.rb b/test/integration/api_test/issues_test.rb old mode 100644 new mode 100755 index 077ae96..df79669 --- a/test/integration/api_test/issues_test.rb +++ b/test/integration/api_test/issues_test.rb @@ -354,9 +354,9 @@ class Redmine::ApiTest::IssuesTest < Redmine::ApiTest::Base end end - test "GET /issues/:id.xml should contains total_estimated_hours and total_spent_hours" do + test "GET /issues/:id.xml should contains total_estimated_hours, total_remaining_hours and total_spent_hours" do parent = Issue.find(3) - child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0) + child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0, :remaining_hours => 1.0) TimeEntry.create!(:project => child.project, :issue => child, :user => child.author, :spent_on => child.author.today, :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id) get '/issues/3.xml' @@ -365,14 +365,16 @@ class Redmine::ApiTest::IssuesTest < Redmine::ApiTest::Base assert_select 'issue' do assert_select 'estimated_hours', parent.estimated_hours.to_s assert_select 'total_estimated_hours', (parent.estimated_hours.to_f + 3.0).to_s + assert_select 'remaining_hours', parent.remaining_hours.to_s + assert_select 'total_remaining_hours', (parent.remaining_hours.to_f + 1.0).to_s assert_select 'spent_hours', parent.spent_hours.to_s assert_select 'total_spent_hours', (parent.spent_hours.to_f + 2.5).to_s end end - 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 + 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 parent = Issue.find(3) - child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0) + child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0, :remaining_hours => 1.0) # remove permission! Role.anonymous.remove_permission! :view_time_entries #Role.all.each { |role| role.remove_permission! :view_time_entries } @@ -382,14 +384,16 @@ class Redmine::ApiTest::IssuesTest < Redmine::ApiTest::Base assert_select 'issue' do assert_select 'estimated_hours', parent.estimated_hours.to_s assert_select 'total_estimated_hours', (parent.estimated_hours.to_f + 3.0).to_s + assert_select 'remaining_hours', parent.remaining_hours.to_s + assert_select 'total_remaining_hours', (parent.remaining_hours.to_f + 1.0).to_s assert_select 'spent_hours', false assert_select 'total_spent_hours', false end end - test "GET /issues/:id.json should contains total_estimated_hours and total_spent_hours" do + test "GET /issues/:id.json should contains total_estimated_hours, total_remaining_hours and total_spent_hours" do parent = Issue.find(3) - child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0) + child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0, :remaining_hours => 1.0) TimeEntry.create!(:project => child.project, :issue => child, :user => child.author, :spent_on => child.author.today, :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id) get '/issues/3.json' @@ -398,13 +402,15 @@ class Redmine::ApiTest::IssuesTest < Redmine::ApiTest::Base json = ActiveSupport::JSON.decode(response.body) assert_equal parent.estimated_hours, json['issue']['estimated_hours'] assert_equal (parent.estimated_hours.to_f + 3.0), json['issue']['total_estimated_hours'] + assert_equal parent.remaining_hours, json['issue']['remaining_hours'] + assert_equal (parent.remaining_hours.to_f + 1.0), json['issue']['total_remaining_hours'] assert_equal parent.spent_hours, json['issue']['spent_hours'] assert_equal (parent.spent_hours.to_f + 2.5), json['issue']['total_spent_hours'] end - 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 + 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 parent = Issue.find(3) - child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0) + child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0, :remaining_hours => 1.0) # remove permission! Role.anonymous.remove_permission! :view_time_entries #Role.all.each { |role| role.remove_permission! :view_time_entries } @@ -414,6 +420,8 @@ class Redmine::ApiTest::IssuesTest < Redmine::ApiTest::Base json = ActiveSupport::JSON.decode(response.body) assert_equal parent.estimated_hours, json['issue']['estimated_hours'] assert_equal (parent.estimated_hours.to_f + 3.0), json['issue']['total_estimated_hours'] + assert_equal parent.remaining_hours, json['issue']['remaining_hours'] + assert_equal (parent.remaining_hours.to_f + 1.0), json['issue']['total_remaining_hours'] assert_equal nil, json['issue']['spent_hours'] assert_equal nil, json['issue']['total_spent_hours'] end diff --git a/test/unit/helpers/issues_helper_test.rb b/test/unit/helpers/issues_helper_test.rb old mode 100644 new mode 100755 index 514d831..0901c05 --- a/test/unit/helpers/issues_helper_test.rb +++ b/test/unit/helpers/issues_helper_test.rb @@ -203,6 +203,13 @@ class IssuesHelperTest < Redmine::HelperTest assert_match '6.30', show_detail(detail, true) end + test 'show_detail should show old and new values with a remaining hours attribute' do + detail = JournalDetail.new(:property => 'attr', :prop_key => 'remaining_hours', + :old_value => '6.3', :value => '5') + assert_match '5.00', show_detail(detail, true) + assert_match '6.30', show_detail(detail, true) + end + test 'show_detail should not show values with a description attribute' do detail = JournalDetail.new(:property => 'attr', :prop_key => 'description', :old_value => 'Foo', :value => 'Bar') diff --git a/test/unit/issue_subtasking_test.rb b/test/unit/issue_subtasking_test.rb old mode 100644 new mode 100755 index e878601..2550d47 --- a/test/unit/issue_subtasking_test.rb +++ b/test/unit/issue_subtasking_test.rb @@ -28,7 +28,7 @@ class IssueSubtaskingTest < ActiveSupport::TestCase def test_leaf_planning_fields_should_be_editable issue = Issue.generate! user = User.find(1) - %w(priority_id done_ratio start_date due_date estimated_hours).each do |attribute| + %w(priority_id done_ratio start_date due_date estimated_hours remaining_hours).each do |attribute| assert issue.safe_attribute?(attribute, user) end end @@ -147,10 +147,10 @@ class IssueSubtaskingTest < ActiveSupport::TestCase assert_equal 20, parent.reload.done_ratio parent.generate_child!(:done_ratio => 70) assert_equal 45, parent.reload.done_ratio - + child = parent.generate_child!(:done_ratio => 0) assert_equal 30, parent.reload.done_ratio - + child.generate_child!(:done_ratio => 30) assert_equal 30, child.reload.done_ratio assert_equal 40, parent.reload.done_ratio @@ -330,4 +330,14 @@ class IssueSubtaskingTest < ActiveSupport::TestCase parent.generate_child!(:estimated_hours => 7) assert_equal 12, parent.reload.total_estimated_hours end + + def test_parent_total_remaining_hours_should_be_sum_of_descendants + parent = Issue.generate! + parent.generate_child!(:remaining_hours => nil) + assert_equal 0, parent.reload.total_remaining_hours + parent.generate_child!(:remaining_hours => 5) + assert_equal 5, parent.reload.total_remaining_hours + parent.generate_child!(:remaining_hours => 7) + assert_equal 12, parent.reload.total_remaining_hours + end end diff --git a/test/unit/issue_test.rb b/test/unit/issue_test.rb old mode 100644 new mode 100755 index fe056a7..16b4cc0 --- a/test/unit/issue_test.rb +++ b/test/unit/issue_test.rb @@ -56,10 +56,11 @@ class IssueTest < ActiveSupport::TestCase issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', - :description => 'IssueTest#test_create', :estimated_hours => '1:30') + :description => 'IssueTest#test_create', :estimated_hours => '1:30', :remaining_hours => '1') assert issue.save issue.reload assert_equal 1.5, issue.estimated_hours + assert_equal 1, issue.remaining_hours end def test_create_minimal @@ -68,6 +69,7 @@ class IssueTest < ActiveSupport::TestCase assert_equal issue.tracker.default_status, issue.status assert issue.description.nil? assert_nil issue.estimated_hours + assert_nil issue.remaining_hours end def test_create_with_all_fields_disabled @@ -134,6 +136,23 @@ class IssueTest < ActiveSupport::TestCase end end + def test_remaining_hours_update_with_negative_value_should_set_to_zero + set_language_if_valid 'en' + ['-4'].each do |invalid| + issue = Issue.new(:remaining_hours => invalid) + assert_equal 0, issue.remaining_hours + end + end + + def test_remaining_hours_should_be_set_from_estimated_hours_when_is_empty + issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, + :status_id => 1, :priority => IssuePriority.all.first, + :subject => 'test_create', + :description => 'IssueTest#test_create', :estimated_hours => '1:30') + assert issue.save + assert_equal 1.5, issue.remaining_hours + end + def test_create_with_required_custom_field set_language_if_valid 'en' field = IssueCustomField.find_by_name('Database') @@ -2885,6 +2904,17 @@ class IssueTest < ActiveSupport::TestCase assert_equal false, issue.closing? end + def test_closing_should_set_remaining_hours_to_zero + issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, + :status_id => 1, :priority => IssuePriority.all.first, + :subject => 'test_create', + :description => 'IssueTest#test_create', :estimated_hours => '1:30', :remaining_hours => '1') + assert_equal 1, issue.remaining_hours + issue.status_id = 5 + issue.save! + assert_equal 0, issue.remaining_hours + end + def test_reopening_should_return_true_when_reopening_an_issue issue = Issue.find(8) issue.status = IssueStatus.find(6) diff --git a/test/unit/mail_handler_test.rb b/test/unit/mail_handler_test.rb old mode 100644 new mode 100755 index c1afdae..b868930 --- a/test/unit/mail_handler_test.rb +++ b/test/unit/mail_handler_test.rb @@ -42,7 +42,7 @@ class MailHandlerTest < ActiveSupport::TestCase def test_add_issue_with_specific_overrides issue = submit_email('ticket_on_given_project.eml', - :allow_override => ['status', 'start_date', 'due_date', 'assigned_to', 'fixed_version', 'estimated_hours', 'done_ratio'] + :allow_override => ['status', 'start_date', 'due_date', 'assigned_to', 'fixed_version', 'estimated_hours', 'remaining_hours', 'done_ratio'] ) assert issue.is_a?(Issue) assert !issue.new_record? @@ -58,11 +58,13 @@ class MailHandlerTest < ActiveSupport::TestCase assert_equal User.find_by_login('jsmith'), issue.assigned_to assert_equal Version.find_by_name('Alpha'), issue.fixed_version assert_equal 2.5, issue.estimated_hours + assert_equal 1, issue.remaining_hours assert_equal 30, issue.done_ratio # keywords should be removed from the email body assert !issue.description.match(/^Project:/i) assert !issue.description.match(/^Status:/i) assert !issue.description.match(/^Start Date:/i) + assert !issue.description.match(/^remaining hours:/i) end def test_add_issue_with_all_overrides @@ -79,6 +81,7 @@ class MailHandlerTest < ActiveSupport::TestCase assert_equal User.find_by_login('jsmith'), issue.assigned_to assert_equal Version.find_by_name('Alpha'), issue.fixed_version assert_equal 2.5, issue.estimated_hours + assert_equal 1, issue.remaining_hours assert_equal 30, issue.done_ratio end @@ -100,6 +103,7 @@ class MailHandlerTest < ActiveSupport::TestCase assert_nil issue.assigned_to assert_nil issue.fixed_version assert_nil issue.estimated_hours + assert_nil issue.remaining_hours assert_equal 0, issue.done_ratio end diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb old mode 100644 new mode 100755 index 5c34d58..5ca9eeb --- a/test/unit/query_test.rb +++ b/test/unit/query_test.rb @@ -1256,8 +1256,8 @@ class QueryTest < ActiveSupport::TestCase def test_set_totalable_names q = IssueQuery.new - q.totalable_names = ['estimated_hours', :spent_hours, ''] - assert_equal [:estimated_hours, :spent_hours], q.totalable_columns.map(&:name) + q.totalable_names = ['estimated_hours', 'remaining_hours', :spent_hours, ''] + assert_equal [:estimated_hours, :remaining_hours, :spent_hours], q.totalable_columns.map(&:name) end def test_totalable_columns_should_default_to_settings @@ -1272,6 +1272,11 @@ class QueryTest < ActiveSupport::TestCase assert_include :estimated_hours, q.available_totalable_columns.map(&:name) end + def test_available_totalable_columns_should_include_remaining_hours + q = IssueQuery.new + assert_include :remaining_hours, q.available_totalable_columns.map(&:name) + end + def test_available_totalable_columns_should_include_spent_hours User.current = User.find(1) @@ -1314,6 +1319,29 @@ class QueryTest < ActiveSupport::TestCase ) end + def test_total_for_remaining_hours + Issue.delete_all + Issue.generate!(:remaining_hours => 5.5) + Issue.generate!(:remaining_hours => 1.1) + Issue.generate! + + q = IssueQuery.new + assert_equal 6.6, q.total_for(:remaining_hours) + end + + def test_total_by_group_for_remaining_hours + Issue.delete_all + Issue.generate!(:remaining_hours => 5.5, :assigned_to_id => 2) + Issue.generate!(:remaining_hours => 1.1, :assigned_to_id => 3) + Issue.generate!(:remaining_hours => 3.5) + + q = IssueQuery.new(:group_by => 'assigned_to') + assert_equal( + {nil => 3.5, User.find(2) => 5.5, User.find(3) => 1.1}, + q.total_by_group_for(:remaining_hours) + ) + end + def test_total_for_spent_hours TimeEntry.delete_all TimeEntry.generate!(:hours => 5.5)