From 404a5b1de03011f65bf172a7f01a7cadbcf3ba9b Mon Sep 17 00:00:00 2001 From: Go MAEDA Date: Fri, 14 Apr 2023 23:50:26 +0000 Subject: [PATCH] "contains any of" operator for text filters to perform OR search of multiple terms (#38435). Patch by Go MAEDA. git-svn-id: https://svn.redmine.org/redmine/trunk@22188 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/issue_query.rb | 8 +++++++- app/models/query.rb | 18 ++++++++++++++---- config/locales/en.yml | 1 + test/unit/query_test.rb | 31 +++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/app/models/issue_query.rb b/app/models/issue_query.rb index 68ce1c104..cd363b5c8 100644 --- a/app/models/issue_query.rb +++ b/app/models/issue_query.rb @@ -791,8 +791,14 @@ class IssueQuery < Query projects = nil end + is_all_words = + case operator + when '~' then true + when '|~', '!~' then false + end + fetcher = Redmine::Search::Fetcher.new( - question, User.current, ['issue'], projects, all_words: (operator != '!~'), attachments: '0' + question, User.current, ['issue'], projects, all_words: is_all_words, attachments: '0' ) ids = fetcher.result_ids.map(&:last) if ids.present? diff --git a/app/models/query.rb b/app/models/query.rb index 289d683f6..10bd23adf 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -306,6 +306,7 @@ class Query < ActiveRecord::Base "t-" => :label_ago, "~" => :label_contains, "!~" => :label_not_contains, + "|~" => :label_contains_any_of, "^" => :label_starts_with, "$" => :label_ends_with, "=p" => :label_any_issues_in_project, @@ -323,9 +324,9 @@ class Query < ActiveRecord::Base :list_subprojects => [ "*", "!*", "=", "!" ], :date => [ "=", ">=", "<=", "><", "t+", ">t-", " [ "=", ">=", "<=", "><", ">t-", " [ "~", "=", "!~", "!", "^", "$", "!*", "*" ], - :text => [ "~", "!~", "^", "$", "!*", "*" ], - :search => [ "~", "!~" ], + :string => [ "~", "|~", "=", "!~", "!", "^", "$", "!*", "*" ], + :text => [ "~", "|~", "!~", "^", "$", "!*", "*" ], + :search => [ "~", "|~", "!~" ], :integer => [ "=", ">=", "<=", "><", "!*", "*" ], :float => [ "=", ">=", "<=", "><", "!*", "*" ], :relation => ["=", "!", "=p", "=!p", "!p", "*o", "!o", "!*", "*"], @@ -1431,6 +1432,8 @@ class Query < ActiveRecord::Base sql = sql_contains("#{db_table}.#{db_field}", value.first) when "!~" sql = sql_contains("#{db_table}.#{db_field}", value.first, :match => false) + when "|~" + sql = sql_contains("#{db_table}.#{db_field}", value.first, :all_words => false) when "^" sql = sql_contains("#{db_table}.#{db_field}", value.first, :starts_with => true) when "$" @@ -1443,6 +1446,12 @@ class Query < ActiveRecord::Base end # Returns a SQL LIKE statement with wildcards + # + # valid options: + # * :match - use NOT LIKE if false + # * :starts_with - use LIKE 'value%' if true + # * :ends_with - use LIKE '%value' if true + # * :all_words - use OR instead of AND if false def sql_contains(db_field, value, options={}) options = {} unless options.is_a?(Hash) options.symbolize_keys! @@ -1465,10 +1474,11 @@ class Query < ActiveRecord::Base def self.tokenized_like_conditions(db_field, value, **options) tokens = Redmine::Search::Tokenizer.new(value).tokens tokens = [value] unless tokens.present? + logical_opr = options.delete(:all_words) == false ? ' OR ' : ' AND ' sql, values = tokens.map do |token| [Redmine::Database.like(db_field, '?', options), "%#{sanitize_sql_like token}%"] end.transpose - [sql.join(" AND "), *values] + [sql.join(logical_opr), *values] end # rubocop:enable Lint/IneffectiveAccessModifier diff --git a/config/locales/en.yml b/config/locales/en.yml index 8c9edb8bc..6b7d0834a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -810,6 +810,7 @@ en: label_more_than_ago: more than days ago label_ago: days ago label_contains: contains + label_contains_any_of: contains any of label_not_contains: doesn't contain label_starts_with: starts with label_ends_with: ends with diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb index 55500ff13..f58cd142d 100644 --- a/test/unit/query_test.rb +++ b/test/unit/query_test.rb @@ -736,6 +736,37 @@ class QueryTest < ActiveSupport::TestCase assert_not_include issue, result end + def test_operator_contains_any_of + User.current = User.find(1) + query = IssueQuery.new( + :name => '_', + :filters => { + 'subject' => { + :operator => '|~', + :values => ['close block'] + } + } + ) + result = find_issues_with_query(query) + assert_equal [8, 9, 10, 11, 12], result.map(&:id).sort + result.each {|issue| assert issue.subject =~ /(close|block)/i} + end + + def test_operator_contains_any_of_with_any_searchable_text + User.current = User.find(1) + query = IssueQuery.new( + :name => '_', + :filters => { + 'any_searchable' => { + :operator => '|~', + :values => ['recipe categories'] + } + } + ) + result = find_issues_with_query(query) + assert_equal [1, 2, 3], result.map(&:id).sort + end + def test_range_for_this_week_with_week_starting_on_monday I18n.locale = :fr assert_equal '1', I18n.t(:general_first_day_of_week) -- 2.39.5