diff --git a/lib/redmine/helpers/time_report.rb b/lib/redmine/helpers/time_report.rb index 2ae5fd8eb4..4b68d2ad7a 100644 --- a/lib/redmine/helpers/time_report.rb +++ b/lib/redmine/helpers/time_report.rb @@ -47,7 +47,7 @@ module Redmine time_columns = %w(tyear tmonth tweek spent_on) @hours = [] @scope.includes(:activity). - reorder(nil). + reorder(@criteria.collect{|criteria| @available_criteria[criteria][:order]}.compact). group(@criteria.collect{|criteria| @available_criteria[criteria][:sql]} + time_columns). joins(@criteria.collect{|criteria| @available_criteria[criteria][:joins]}.compact). sum(:hours).each do |hash, hours| @@ -105,27 +105,39 @@ module Redmine def load_available_criteria @available_criteria = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id", + :order => {Arel.sql("LOWER(#{Project.table_name}.name)") => :asc, "#{TimeEntry.table_name}.project_id" => :asc}, :klass => Project, :label => :label_project}, 'status' => {:sql => "#{Issue.table_name}.status_id", + :joins => "LEFT OUTER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id", + :order => {"#{IssueStatus.table_name}.position" => :asc, "#{Issue.table_name}.status_id" => :asc}, :klass => IssueStatus, :label => :field_status}, 'version' => {:sql => "#{Issue.table_name}.fixed_version_id", + :joins => "LEFT OUTER JOIN #{Version.table_name} ON #{Version.table_name}.id = #{Issue.table_name}.fixed_version_id", + :order => {"#{Version.table_name}.name" => :asc, "#{Issue.table_name}.fixed_version_id" => :asc}, :klass => ::Version, :label => :label_version}, 'category' => {:sql => "#{Issue.table_name}.category_id", + :joins => "LEFT OUTER JOIN #{IssueCategory.table_name} ON #{IssueCategory.table_name}.id = #{Issue.table_name}.category_id", + :order => {"#{IssueCategory.table_name}.name" => :asc, "#{Issue.table_name}.category_id" => :asc}, :klass => IssueCategory, :label => :field_category}, 'user' => {:sql => "#{TimeEntry.table_name}.user_id", + :order => User.fields_for_order_statement.index_with{|_| :asc}, :klass => User, :label => :label_user}, 'tracker' => {:sql => "#{Issue.table_name}.tracker_id", + :joins => "LEFT OUTER JOIN #{Tracker.table_name} ON #{Tracker.table_name}.id = #{Issue.table_name}.tracker_id", + :order => {"#{Tracker.table_name}.position" => :asc, "#{Issue.table_name}.tracker_id" => :asc}, :klass => Tracker, :label => :label_tracker}, 'activity' => {:sql => "COALESCE(#{TimeEntryActivity.table_name}.parent_id, #{TimeEntryActivity.table_name}.id)", + :order => {"#{TimeEntryActivity.table_name}.position" => :asc}, :klass => TimeEntryActivity, :label => :field_activity}, 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id", + :order => {"#{TimeEntry.table_name}.issue_id" => :asc}, :klass => Issue, :label => :label_issue} } @@ -141,8 +153,17 @@ module Redmine # Add list and boolean custom fields as available criteria custom_fields.select {|cf| %w(list bool).include?(cf.field_format) && !cf.multiple?}.each do |cf| + subquery_name = "#{cf.format.__send__(:join_alias, cf)}_options" + subquery = + cf.format.possible_values_options(cf).collect.with_index(1) do |option, idx| + value = option.is_a?(Array) ? option.last : option + cf.class.sanitize_sql_array(["SELECT %d AS id, '%s' AS value", idx, value]) + end.join(' UNION ') + joins = cf.join_for_order_statement + joins << " LEFT OUTER JOIN (#{subquery}) AS #{subquery_name} ON #{subquery_name}.value = #{cf.group_statement}" @available_criteria["cf_#{cf.id}"] = {:sql => cf.group_statement, - :joins => cf.join_for_order_statement, + :joins => joins, + :order => {"#{subquery_name}.id" => :asc}, :format => cf.field_format, :custom_field => cf, :label => cf.name} diff --git a/test/functional/timelog_report_test.rb b/test/functional/timelog_report_test.rb index d72491418c..9fd979857c 100644 --- a/test/functional/timelog_report_test.rb +++ b/test/functional/timelog_report_test.rb @@ -27,7 +27,8 @@ class TimelogReportTest < Redmine::ControllerTest :issues, :time_entries, :users, :trackers, :enumerations, :issue_statuses, :custom_fields, :custom_values, :projects_trackers, :custom_fields_trackers, - :custom_fields_projects + :custom_fields_projects, + :versions, :issue_categories include Redmine::I18n @@ -237,6 +238,146 @@ class TimelogReportTest < Redmine::ControllerTest end end + def test_report_with_project_criteria_should_be_sorted_by_project_name + project = Project.find(5) + project.update_attribute(:is_public, true) + project.enable_module!(:time_tracking) + + get :report, :params => {:criteria => ['project']} + assert_response :success + assert_select '#time-report tbody' do + assert_select 'tr:nth-child(1) td.name', :text => 'eCookbook' + assert_select 'tr:nth-child(2) td.name', :text => 'eCookbook Subproject 1' + assert_select 'tr:nth-child(3) td.name', :text => 'Private child of eCookbook' + end + end + + def test_report_with_status_criteria_should_be_sorted_by_position + Project.find(3).update_attribute(:parent_id, nil) + Issue.find(1).update_attribute(:status_id, 3) + + get :report, :params => {:project_id => 1, :criteria => ['status']} + assert_response :success + assert_select '#time-report tbody' do + assert_select 'tr:nth-child(1) td.name', :text => 'New' + assert_select 'tr:nth-child(2) td.name', :text => 'Resolved' + end + end + + def test_report_with_version_criteria_should_be_sorted_by_version_name + Project.find(3).update_attribute(:parent_id, nil) + Issue.find(1).update_attribute(:fixed_version_id, 3) + Issue.find(3).update_attribute(:fixed_version_id, 2) + + get :report, :params => {:project_id => 1, :criteria => ['version']} + assert_response :success + assert_select '#time-report tbody' do + assert_select 'tr:nth-child(1) td.name', :text => '1.0' + assert_select 'tr:nth-child(2) td.name', :text => '2.0' + end + end + + def test_report_with_category_criteria_should_be_sorted_by_category_name + Project.find(3).update_attribute(:parent_id, nil) + Issue.find(1).update_attribute(:category_id, 2) + Issue.find(3).update_attribute(:category_id, 1) + + get :report, :params => {:project_id => 1, :criteria => ['category']} + assert_response :success + assert_select '#time-report tbody' do + assert_select 'tr:nth-child(1) td.name', :text => 'Printing' + assert_select 'tr:nth-child(2) td.name', :text => 'Recipes' + end + end + + def test_report_with_user_criteria_should_be_sorted_by_user_name + TimeEntry.find(2).update_attribute(:user_id, 3) + + get :report, :params => {:project_id => 1, :criteria => ['user']} + assert_response :success + assert_select '#time-report tbody' do + assert_select 'tr:nth-child(1) td.name', :text => 'Dave Lopper' + assert_select 'tr:nth-child(2) td.name', :text => 'John Smith' + assert_select 'tr:nth-child(3) td.name', :text => 'Redmine Admin' + end + end + + def test_report_with_tracker_criteria_should_be_sorted_by_position + Project.find(3).update_attribute(:parent_id, nil) + Issue.find(1).update_attribute(:tracker_id, 3) + TimeEntry.find(1).update_attribute(:issue_id, 2) + + get :report, :params => {:project_id => 1, :criteria => ['tracker']} + assert_response :success + assert_select '#time-report tbody' do + assert_select 'tr:nth-child(1) td.name', :text => 'Bug' + assert_select 'tr:nth-child(2) td.name', :text => 'Feature request' + assert_select 'tr:nth-child(3) td.name', :text => 'Support request' + end + end + + def test_report_with_activity_criteria_should_be_sorted_by_position + TimeEntry.find(1).update_attribute(:activity_id, 11) + + get :report, :params => {:project_id => 1, :criteria => ['activity']} + assert_response :success + assert_select '#time-report tbody' do + assert_select 'tr:nth-child(1) td.name', :text => 'Design' + assert_select 'tr:nth-child(2) td.name', :text => 'Development' + assert_select 'tr:nth-child(3) td.name', :text => 'QA' + end + end + + def test_report_with_issue_criteria_should_be_sorted_by_id + Project.find(3).update_attribute(:parent_id, nil) + TimeEntry.find(1).update_attribute(:issue_id, 2) + + get :report, :params => {:project_id => 1, :criteria => ['issue']} + assert_response :success + assert_select '#time-report tbody' do + assert_select 'tr:nth-child(1) td.name a[href="/issues/1"]' + assert_select 'tr:nth-child(2) td.name a[href="/issues/2"]' + assert_select 'tr:nth-child(3) td.name a[href="/issues/3"]' + end + end + + def test_report_with_bool_format_custom_field_criteria_should_be_sorted_by_yes_no + Project.find(3).update_attribute(:parent_id, nil) + TimeEntry.find(3).destroy + time_entry1 = TimeEntry.find(1) + time_entry1.custom_field_values = {'10' => '0'} + time_entry1.save! + time_entry2 = TimeEntry.find(2) + time_entry2.custom_field_values = {'10' => '1'} + time_entry2.save! + + get :report, :params => {:project_id => 1, :criteria => ['cf_10']} + assert_response :success + assert_select '#time-report tbody' do + assert_select 'tr:nth-child(1) td.name', :text => 'Yes' + assert_select 'tr:nth-child(2) td.name', :text => 'No' + end + end + + def test_report_with_list_format_custom_field_criteria_should_be_sorted_by_possible_values_order + Project.find(3).update_attribute(:parent_id, nil) + TimeEntry.find(1).update_attribute(:issue_id, 7) + issue1 = Issue.find(1) + issue1.custom_field_values = {'1' => 'Oracle'} + issue1.save! + issue7 = Issue.find(7) + issue7.custom_field_values = {'1' => 'PostgreSQL'} + issue7.save! + + get :report, :params => {:project_id => 1, :criteria => ['cf_1']} + assert_response :success + assert_select '#time-report tbody' do + assert_select 'tr:nth-child(1) td.name', :text => 'MySQL' + assert_select 'tr:nth-child(2) td.name', :text => 'PostgreSQL' + assert_select 'tr:nth-child(3) td.name', :text => 'Oracle' + end + end + def test_report_all_projects_csv_export get :report, :params => { :columns => 'month',