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'