diff --git a/app/models/query.rb b/app/models/query.rb index 4452618934..baeb0cf5a2 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -259,6 +259,9 @@ class Query < ApplicationRecord VISIBILITY_PRIVATE = 0 VISIBILITY_ROLES = 1 VISIBILITY_PUBLIC = 2 + FILTER_CONNECTION_AND = 'AND' + FILTER_CONNECTION_OR = 'OR' + FILTER_CONNECTIONS = [FILTER_CONNECTION_AND, FILTER_CONNECTION_OR] belongs_to :project belongs_to :user @@ -455,6 +458,7 @@ class Query < ApplicationRecord end query_params = params[:query] || defaults || {} + self.filter_connection = query_params[:filter_connection] || self.filter_connection self.group_by = params[:group_by] || query_params[:group_by] || self.group_by self.column_names = params[:c] || query_params[:column_names] || self.column_names self.totalable_names = params[:t] || query_params[:totalable_names] || self.totalable_names @@ -1029,6 +1033,9 @@ class Query < ApplicationRecord end end if filters and valid? + filters_clauses.reject!(&:blank?) + filters_clauses = filters_clauses.any? ? [filters_clauses.join(" #{filter_connection} ")] : [] + if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn) # Excludes results for which the grouped custom field is not visible filters_clauses << c.custom_field.visibility_by_project_condition @@ -1080,6 +1087,22 @@ class Query < ApplicationRecord end end + def filter_connection + if FILTER_CONNECTIONS.include?(options[:filter_connection].to_s) + options[:filter_connection] + else + FILTER_CONNECTION_AND + end + end + + def filter_connection=(connection) + if FILTER_CONNECTIONS.include?(connection) + options[:filter_connection] = connection + else + options.delete(:filter_connection) + end + end + def display_type options[:display_type] || self.default_display_type end diff --git a/app/views/queries/_filters.html.erb b/app/views/queries/_filters.html.erb index 42756775a5..7d55600079 100644 --- a/app/views/queries/_filters.html.erb +++ b/app/views/queries/_filters.html.erb @@ -15,6 +15,11 @@ $(document).ready(function(){ <% end %>
+
+
+ <%= hidden_field_tag 'selected_filter_connection', @query.filter_connection %> + <%= select_tag 'query[filter_connection]', options_for_select(Query::FILTER_CONNECTIONS.map{|c| [l("label_filter_connection_#{c.downcase}"), c]}, @query.filter_connection) %> +
diff --git a/config/locales/en.yml b/config/locales/en.yml index 1584f5ef2e..08a1050cd2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -791,6 +791,9 @@ en: label_filter_plural: Filters label_equals: is label_not_equals: is not + field_filter_connection: Filter Connection + label_filter_connection_and: (AND) Match all filters + label_filter_connection_or: (OR) Match any filter label_in_less_than: in less than label_in_more_than: in more than label_in_the_next_days: in the next diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 8a67577576..fcc000faa5 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -625,6 +625,9 @@ ja: label_filter_plural: フィルタ label_equals: 等しい label_not_equals: 等しくない + field_filter_connection: フィルタの接続条件 + label_filter_connection_and: (AND) すべてのフィルタに一致 + label_filter_connection_or: (OR) いずれかのフィルタに一致 label_in_less_than: N日後以前 label_in_more_than: N日後以降 label_greater_or_equal: 以上 diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index 3b7521d407..4d5da0cfdc 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -344,6 +344,76 @@ class IssuesControllerTest < Redmine::ControllerTest assert_not_include 4, issues end + def test_index_with_filters_using_filter_connection_and + Issue.destroy_all + issues_filterd = [ + Issue.create!(:project_id => 1, :tracker_id => 1, :subject => 'Issue created by user 2 and assigned_to user 2', :author_id => 2, :assigned_to_id => 2) + ] + issues_not_filtered = [ + Issue.create!(:project_id => 1, :tracker_id => 1, :subject => 'Issue created by user 2', :author_id => 2, :assigned_to_id => nil), + Issue.create!(:project_id => 1, :tracker_id => 1, :subject => 'issue assisgned to user 2', :author_id => 3, :assigned_to_id => 2) + ] + + get( + :index, + :params => { + :project_id => 1, + :set_filter => 1, + :query => { :filter_connection => 'AND', }, + :f => ['author_id', 'assigned_to_id'], + :op => { 'author_id' => '=', 'assigned_to_id' => '=' }, + :v => { 'author_id' => ['2'], 'assigned_to_id' => ['2'] } + } + ) + assert_response :success + + issues = issues_in_list.map(&:id).uniq.sort + issues_filterd.each do |issue| + assert_include issue.id, issues + end + issues_not_filtered.each do |issue| + assert_not_include issue.id, issues + end + end + + def test_index_with_filters_using_filter_connection_or + Issue.destroy_all + issues_filterd = [ + Issue.create!(:project_id => 1, :tracker_id => 1, + :subject => 'Issue created by user 2', + :author_id => 2, :assigned_to_id => nil), + Issue.create!(:project_id => 1, :tracker_id => 1, + :subject => 'issue assisgned to user 2', + :author_id => 3, :assigned_to_id => 2) + ] + issues_not_filtered = [ + Issue.create!(:project_id => 1, :tracker_id => 1, + :subject => 'Issue created by user 3', + :author_id => 3, :assigned_to_id => nil) + ] + + get( + :index, + :params => { + :project_id => 1, + :set_filter => 1, + :query => { :filter_connection => 'OR' }, + :f => ['author_id', 'assigned_to_id'], + :op => { 'author_id' => '=', 'assigned_to_id' => '=' }, + :v => { 'author_id' => ['2'], 'assigned_to_id' => ['2'] } + } + ) + assert_response :success + + issues = issues_in_list.map(&:id).uniq.sort + issues_filterd.each do |issue| + assert_include issue.id, issues + end + issues_not_filtered.each do |issue| + assert_not_include issue.id, issues + end + end + def test_index_with_query get( :index, diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb index dd3b67928f..b1d7183060 100644 --- a/test/unit/query_test.rb +++ b/test/unit/query_test.rb @@ -2031,6 +2031,39 @@ class QueryTest < ActiveSupport::TestCase assert_nil q.statement end + def test_statement_should_connect_filters_with_and_when_filter_connection_is_blank + q = IssueQuery.new(:name => '_') + q.filters = { + 'status_id' => {:operator => '=', :values => ['1']}, + 'priority_id' => {:operator => '=', :values => ['4']} + } + + assert q.valid? + assert_equal "(issues.status_id IN ('1')) AND (issues.priority_id IN ('4'))", q.statement + end + + def test_statument_should_connect_filters_with_and_when_filter_connection_is_and + q = IssueQuery.new(:name => '_', :filter_connection => 'AND') + q.filters = { + 'status_id' => {:operator => '=', :values => ['1']}, + 'priority_id' => {:operator => '=', :values => ['4']} + } + + assert q.valid? + assert_equal "(issues.status_id IN ('1')) AND (issues.priority_id IN ('4'))", q.statement + end + + def test_statement_should_be_changed_connected_with_or_when_filter_connection_is_or + q = IssueQuery.new(:name => '_', :filter_connection => 'OR') + q.filters = { + 'status_id' => {:operator => '=', :values => ['1']}, + 'priority_id' => {:operator => '=', :values => ['4']} + } + + assert q.valid? + assert_equal "(issues.status_id IN ('1')) OR (issues.priority_id IN ('4'))", q.statement + end + def test_available_filters_as_json_should_include_missing_assigned_to_id_values user = User.generate! with_current_user User.find(1) do @@ -3391,6 +3424,20 @@ class QueryTest < ActiveSupport::TestCase end end + def test_filter_connection_should_accept_known_values + query = IssueQuery.new(:name => '_') + query.filter_connection = 'AND' + assert_equal 'AND', query.filter_connection + query.filter_connection = 'OR' + assert_equal 'OR', query.filter_connection + end + + def test_filter_connection_should_not_accept_unknown_values + query = IssueQuery.new(:name => '_') + query.filter_connection = 'invalid' + assert_equal 'AND', query.filter_connection + end + def test_display_type_should_accept_known_types query = ProjectQuery.new(:name => '_') query.display_type = 'list'