diff --git a/app/models/time_entry_query.rb b/app/models/time_entry_query.rb index 3b884820f9..4625ae6d5b 100644 --- a/app/models/time_entry_query.rb +++ b/app/models/time_entry_query.rb @@ -34,6 +34,7 @@ class TimeEntryQuery < Query QueryAssociationColumn.new(:issue, :status, :caption => :field_status, :sortable => "#{IssueStatus.table_name}.position"), QueryAssociationColumn.new(:issue, :category, :caption => :field_category, :sortable => "#{IssueCategory.table_name}.name"), QueryAssociationColumn.new(:issue, :fixed_version, :caption => :field_fixed_version, :sortable => Version.fields_for_order_statement), + QueryAssociationColumn.new(:issue, :parent, :caption => :field_parent_issue, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc'), QueryColumn.new(:comments), QueryColumn.new(:hours, :sortable => "#{TimeEntry.table_name}.hours", :totalable => true), ] @@ -71,6 +72,10 @@ class TimeEntryQuery < Query :type => :list, :name => l("label_attribute_of_issue", :name => l(:field_fixed_version)), :values => lambda {fixed_version_values}) + add_available_filter( + "issue.parent_id", + :type => :tree, + :name => l("label_attribute_of_issue", :name => l(:field_parent_issue))) add_available_filter( "issue.category_id", :type => :list_optional, @@ -200,6 +205,30 @@ class TimeEntryQuery < Query end end + def sql_for_issue_parent_id_field(field, operator, value) + case operator + when "=" + # accepts a comma separated list of ids + parent_ids = value.first.to_s.scan(/\d+/).map(&:to_i).uniq + issue_ids = Issue.where(:parent_id => parent_ids).pluck(:id) + if issue_ids.present? + "#{TimeEntry.table_name}.issue_id IN (#{issue_ids.join(',')})" + else + "1=0" + end + when "~" + root_id, lft, rgt = Issue.where(:id => value.first.to_i).pick(:root_id, :lft, :rgt) + issue_ids = Issue.where("#{Issue.table_name}.root_id = ? AND #{Issue.table_name}.lft > ? AND #{Issue.table_name}.rgt < ?", root_id, lft, rgt).pluck(:id) if root_id && lft && rgt + if issue_ids.present? + "#{TimeEntry.table_name}.issue_id IN (#{issue_ids.join(',')})" + else + "1=0" + end + else + sql_for_field("parent_id", operator, value, Issue.table_name, "parent_id") + end + end + def sql_for_activity_id_field(field, operator, value) ids = value.map(&:to_i).join(',') table_name = Enumeration.table_name diff --git a/test/functional/timelog_controller_test.rb b/test/functional/timelog_controller_test.rb index d36279d138..f859a88b24 100644 --- a/test/functional/timelog_controller_test.rb +++ b/test/functional/timelog_controller_test.rb @@ -1374,6 +1374,52 @@ class TimelogControllerTest < Redmine::ControllerTest assert_select 'td.issue-category', :text => 'Printing' end + def test_index_with_issue_parent_filter + issue1 = Issue.generate!(project_id: 'ecookbook', parent_id: 2) + entry1 = TimeEntry.generate!(issue: issue1, hours: 2.5) + issue2 = Issue.generate!(project_id: 'ecookbook', parent_id: 5) + entry2 = TimeEntry.generate!(issue: issue2, hours: 5.0) + + get :index, params: { + project_id: 'ecookbook', + f: ['issue.parent_id'], + op: {'issue.parent_id' => '='}, + v: {'issue.parent_id' => ['2,5']} + } + assert_response :success + assert_equal [entry1.id, entry2.id].sort, css_select('input[name="ids[]"]').map {|e| e.attr(:value).to_i}.sort + end + + def test_index_with_issue_parent_column + issue = Issue.generate!(project_id: 'ecookbook', parent_id: 2) + entry = TimeEntry.generate!(issue: issue, hours: 2.5) + + get :index, params: { + project_id: 'ecookbook', + c: %w(project spent_on issue comments hours issue.parent) + } + + assert_response :success + assert_select 'td.issue-parent', text: "#{issue.parent.tracker} ##{issue.parent.id}: #{issue.parent.subject}" + end + + def test_index_with_issue_parent_sort + issue1 = Issue.generate!(project_id: 'ecookbook', parent_id: 2) + entry1 = TimeEntry.generate!(issue: issue1, hours: 2.5) + issue2 = Issue.generate!(project_id: 'ecookbook', parent_id: 5) + entry2 = TimeEntry.generate!(issue: issue2, hours: 5.0) + + get :index, :params => { + :c => ["hours", 'issue.parent'], + :sort => 'issue.parent' + } + assert_response :success + + # Make sure that values are properly sorted + values = css_select("td.issue-parent").map(&:text).reject(&:blank?) + assert_equal ["#{issue1.parent.tracker} ##{issue1.parent.id}: #{issue1.parent.subject}", "#{issue2.parent.tracker} ##{issue2.parent.id}: #{issue2.parent.subject}"].sort, values.sort + end + def test_index_with_issue_fixed_version_column issue = Issue.find(1) issue.fixed_version = Version.find(3) diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb index 4d6650ee81..f17c3e8b5a 100644 --- a/test/unit/query_test.rb +++ b/test/unit/query_test.rb @@ -504,6 +504,32 @@ class QueryTest < ActiveSupport::TestCase find_issues_with_query(query) end + def test_time_entry_operator_is_on_issue_parent_id_should_accept_comma_separated_values + issue1 = Issue.generate!(project_id: 'ecookbook', parent_id: 2) + entry1 = TimeEntry.generate!(issue: issue1) + issue2 = Issue.generate!(project_id: 'ecookbook', parent_id: 5) + entry2 = TimeEntry.generate!(issue: issue2) + + query = TimeEntryQuery.new(:name => '_') + query.add_filter("issue.parent_id", '=', ['2,5']) + entries = TimeEntry.where(query.statement).to_a + assert_equal 2, entries.size + assert_equal [entry1.id, entry2.id].sort, entries.map(&:id).sort + end + + def test_time_entry_contains_operator_is_on_issue_parent_id + issue1 = Issue.generate!(project_id: 'ecookbook', parent_id: 2) + entry1 = TimeEntry.generate!(issue: issue1) + issue2 = Issue.generate!(project_id: 'ecookbook', parent_id: issue1.id) + entry2 = TimeEntry.generate!(issue: issue2) + + query = TimeEntryQuery.new(:name => '_') + query.add_filter("issue.parent_id", '~', ['2']) + entries = TimeEntry.where(query.statement).to_a + assert_equal 2, entries.size + assert_equal [entry1.id, entry2.id].sort, entries.map(&:id).sort + end + def test_date_filter_should_not_accept_non_date_values query = IssueQuery.new(:name => '_') query.add_filter('created_on', '=', ['a'])