From b457fa916bf0257fdf1e6ab5af7af41f7d5d039c Mon Sep 17 00:00:00 2001 From: Tilman Klaeger Date: Tue, 24 May 2022 00:23:09 +0200 Subject: [PATCH] Adding or-query patch --- app/helpers/queries_helper.rb | 2 + app/models/issue_query.rb | 16 ++++ app/models/query.rb | 138 ++++++++++++++++++++++++++++++++-- config/locales/de.yml | 4 + config/locales/en.yml | 6 ++ config/locales/ja.yml | 6 ++ test/unit/query_test.rb | 104 +++++++++++++++++++++++++ 7 files changed, 269 insertions(+), 7 deletions(-) diff --git a/app/helpers/queries_helper.rb b/app/helpers/queries_helper.rb index 77a577998..041c3963f 100644 --- a/app/helpers/queries_helper.rb +++ b/app/helpers/queries_helper.rb @@ -43,6 +43,8 @@ module QueriesHelper group = :label_time_tracking elsif %w(attachment attachment_description).include?(field) group = :label_attachment + elsif field_options[:group] == 'or_filter' + group = :label_orfilter end if group (grouped[group] ||= []) << [field_options[:name], field] diff --git a/app/models/issue_query.rb b/app/models/issue_query.rb index 45ffcb57c..1906608e3 100644 --- a/app/models/issue_query.rb +++ b/app/models/issue_query.rb @@ -269,6 +269,22 @@ class IssueQuery < Query add_available_filter "issue_id", :type => :integer, :label => :label_issue Tracker.disabled_core_fields(trackers).each do |field| + add_available_filter "and_any", + :name => l(:label_orfilter_and_any), + :type => :list, + :values => [l(:general_text_Yes)], + :group => 'or_filter' + add_available_filter "or_any", + :name => l(:label_orfilter_or_any), + :type => :list, + :values => [l(:general_text_Yes)], + :group => 'or_filter' + add_available_filter "or_all", + :name => l(:label_orfilter_or_all), + :type => :list, + :values => [l(:general_text_Yes)], + :group => 'or_filter' + delete_available_filter field end end diff --git a/app/models/query.rb b/app/models/query.rb index 0186cc379..0b178e185 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -307,6 +307,8 @@ class Query < ActiveRecord::Base "!p" => :label_no_issues_in_project, "*o" => :label_any_open_issues, "!o" => :label_no_open_issues, + "match" => :label_match, + "!match" => :label_not_match } class_attribute :operators_by_filter_type @@ -318,7 +320,7 @@ class Query < ActiveRecord::Base :date => [ "=", ">=", "<=", "><", "t+", ">t-", " [ "=", ">=", "<=", "><", ">t-", " [ "~", "=", "!~", "!", "^", "$", "!*", "*" ], - :text => [ "~", "!~", "^", "$", "!*", "*" ], + :text => [ "~", "!~", "^", "$", "!*", "*", "match", "!match" ], :integer => [ "=", ">=", "<=", "><", "!*", "*" ], :float => [ "=", ">=", "<=", "><", "!*", "*" ], :relation => ["=", "!", "=p", "=!p", "!p", "*o", "!o", "!*", "*"], @@ -959,10 +961,36 @@ class Query < ActiveRecord::Base end def statement - # filters clauses - filters_clauses = [] + filters_clauses=[] + and_clauses=[] + and_any_clauses=[] + or_any_clauses=[] + or_all_clauses=[] + and_any_op = "" + or_any_op = "" + or_all_op = "" + + #the AND filter start first + filters_clauses = and_clauses + filters.each_key do |field| next if field == "subproject_id" + if field == "and_any" + #start the and any part, point filters_clause to and_any_clauses + filters_clauses = and_any_clauses + and_any_op = operator_for(field) == "=" ? " AND " : " AND NOT " + next + elsif field == "or_any" + #start the or any part, point filters_clause to or_any_clauses + filters_clauses = or_any_clauses + or_any_op = operator_for(field) == "=" ? " OR " : " OR NOT " + next + elsif field == "or_all" + #start the or any part, point filters_clause to or_any_clauses + filters_clauses = or_all_clauses + or_all_op = operator_for(field) == "=" ? " OR " : " OR NOT " + next + end v = values_for(field).clone next unless v and !v.empty? @@ -997,7 +1025,7 @@ class Query < ActiveRecord::Base filters_clauses << sql_for_custom_field(field, operator, v, $1) elsif field =~ /^cf_(\d+)\.(.+)$/ filters_clauses << sql_for_custom_field_attribute(field, operator, v, $1, $2) - elsif respond_to?(method = "sql_for_#{field.tr('.', '_')}_field") + elsif respond_to?(method = "sql_for_#{field.gsub('.', '_')}_field") # specific statement filters_clauses << send(method, field, operator, v) else @@ -1011,10 +1039,39 @@ class Query < ActiveRecord::Base filters_clauses << c.custom_field.visibility_by_project_condition end - filters_clauses << project_statement - filters_clauses.reject!(&:blank?) + #now start build the full statement, project filter is allways AND + and_clauses.reject!(&:blank?) + and_statement = and_clauses.any? ? and_clauses.join(" AND ") : nil + + all_and_statement = ["#{project_statement}", "#{and_statement}"].reject(&:blank?) + all_and_statement = all_and_statement.any? ? all_and_statement.join(" AND ") : nil + + + # finish the traditional part. Now extended part + # add the and_any first + and_any_clauses.reject!(&:blank?) + and_any_statement = and_any_clauses.any? ? "("+ and_any_clauses.join(" OR ") +")" : nil + + full_statement_ext_1 = ["#{all_and_statement}", "#{and_any_statement}"].reject(&:blank?) + full_statement_ext_1 = full_statement_ext_1.any? ? full_statement_ext_1.join(and_any_op) : nil + + # then add the or_all + or_all_clauses.reject!(&:blank?) + or_all_statement = or_all_clauses.any? ? "("+ or_all_clauses.join(" AND ") +")" : nil + + full_statement_ext_2 = ["#{full_statement_ext_1}", "#{or_all_statement}"].reject(&:blank?) + full_statement_ext_2 = full_statement_ext_2.any? ? full_statement_ext_2.join(or_all_op) : nil + + # then add the or_any + or_any_clauses.reject!(&:blank?) + or_any_statement = or_any_clauses.any? ? "("+ or_any_clauses.join(" OR ") +")" : nil - filters_clauses.any? ? filters_clauses.join(' AND ') : nil + full_statement = ["#{full_statement_ext_2}", "#{or_any_statement}"].reject(&:blank?) + full_statement = full_statement.any? ? full_statement.join(or_any_op) : nil + + Rails.logger.info "STATEMENT #{full_statement}" + + return full_statement end # Returns the result count by group or nil if query is not grouped @@ -1427,6 +1484,10 @@ class Query < ActiveRecord::Base sql = sql_contains("#{db_table}.#{db_field}", value.first, :starts_with => true) when "$" sql = sql_contains("#{db_table}.#{db_field}", value.first, :ends_with => true) + when "match" + sql = sql_for_match_operators(field, operator, value, db_table, db_field, is_custom_filter) + when "!match" + sql = sql_for_match_operators(field, operator, value, db_table, db_field, is_custom_filter) else raise QueryError, "Unknown query operator #{operator}" end @@ -1434,6 +1495,69 @@ class Query < ActiveRecord::Base return sql end + def sql_for_match_operators(field, operator, value, db_table, db_field, is_custom_filter=false) + sql = '' + v = "(" + value.first.strip + ")" + + match = true + op = "" + term = "" + in_term = false + + in_bracket = false + + v.chars.each do |c| + + if (!in_bracket && "()+~!".include?(c) && in_term ) || (in_bracket && "}".include?(c)) + if !term.empty? + sql += "(" + sql_contains("#{db_table}.#{db_field}", term, match) + ")" + end + #reset + op = "" + term = "" + in_term = false + + in_bracket = (c == "{") + end + + if in_bracket && (!"{}".include? c) + term += c + in_term = true + else + + case c + when "{" + in_bracket = true + when "}" + in_bracket = false + when "(" + sql += c + when ")" + sql += c + when "+" + sql += " AND " if sql.last != "(" + when "~" + sql += " OR " if sql.last != "(" + when "!" + sql += " NOT " + else + if c != " " + term += c + in_term = true + end + end + + end + end + + if operator.include? "!" + sql = " NOT " + sql + end + + Rails.logger.info "MATCH EXPRESSION: V=#{value.first}, SQL=#{sql}" + return sql + end + # Returns a SQL LIKE statement with wildcards def sql_contains(db_field, value, options={}) options = {} unless options.is_a?(Hash) diff --git a/config/locales/de.yml b/config/locales/de.yml index 3a8ab0b7e..5c9017641 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -843,6 +843,10 @@ de: label_year: Jahr label_yesterday: gestern label_default_query: Standardabfrage + label_orfilter: "ODER Filter" + label_orfilter_and_any: "UND einer der folgenden" + label_orfilter_or_any: "ODER einer der folgenden" + label_orfilter_or_all: "ODER alle folgenden" mail_body_account_activation_request: "Ein neuer Benutzer (%{value}) hat sich registriert. Sein Konto wartet auf Ihre Genehmigung:" mail_body_account_information: Ihre Konto-Informationen diff --git a/config/locales/en.yml b/config/locales/en.yml index 62577376f..6bc77d715 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1124,6 +1124,12 @@ en: label_my_bookmarks: My bookmarks label_assign_to_me: Assign to me label_default_query: Default query + label_orfilter: "OR filters" + label_orfilter_and_any: "AND any following" + label_orfilter_or_any: "OR any following" + label_orfilter_or_all: "OR all following" + label_match: "match" + label_not_match: "not match" button_login: Login button_submit: Submit diff --git a/config/locales/ja.yml b/config/locales/ja.yml index f0435e523..65434910c 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -819,6 +819,12 @@ ja: label_parent_revision: 親 label_child_revision: 子 label_gantt_progress_line: イナズマ線 + label_orfilter: "ORフィルタ" + label_orfilter_and_any: "上記 かつ (以下のいずれか)" + label_orfilter_or_any: "上記 または (以下のいずれか)" + label_orfilter_or_all: "上記 または (以下の全て)" + label_match: "match" + label_not_match: "not match" button_login: ログイン button_submit: 送信 diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb index d6f7922a1..95f991326 100644 --- a/test/unit/query_test.rb +++ b/test/unit/query_test.rb @@ -1598,6 +1598,110 @@ class QueryTest < ActiveSupport::TestCase assert_equal [5, 8, 9], issues.collect(&:id).sort end + def test_filter_on_subject_match + query = IssueQuery.new(:name => '_') + query.filters = {'subject' => {:operator => 'match', :values => ['issue']}} + issues = find_issues_with_query(query) + assert_equal [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], issues.collect(&:id).sort + + query = IssueQuery.new(:name => '_') + query.filters = {'subject' => {:operator => 'match', :values => ['(~project ~recipe) +!sub']}} + issues = find_issues_with_query(query) + assert_equal [1, 3, 4, 14], issues.collect(&:id).sort + + query = IssueQuery.new(:name => '_') + query.filters = {'subject' => {:operator => 'match', :values => ['!(~sub project ~block) +issue']}} + issues = find_issues_with_query(query) + assert_equal [4, 7, 8, 11, 12, 14], issues.collect(&:id).sort + + query = IssueQuery.new(:name => '_') + query.filters = {'subject' => {:operator => 'match', :values => ['+{closed ver} ~{locked ver}']}} + issues = find_issues_with_query(query) + assert_equal [11, 12], issues.collect(&:id).sort + end + + def test_filter_on_subject_not_match + query = IssueQuery.new(:name => '_') + query.filters = {'subject' => {:operator => '!match', :values => ['issue']}} + issues = find_issues_with_query(query) + assert_equal [1, 2, 3], issues.collect(&:id).sort + + query = IssueQuery.new(:name => '_') + query.filters = {'subject' => {:operator => '!match', :values => ['(~project ~recipe) +!sub']}} + issues = find_issues_with_query(query) + assert_equal [2, 5, 6, 7, 8, 9, 10, 11, 12, 13], issues.collect(&:id).sort + + query = IssueQuery.new(:name => '_') + query.filters = {'subject' => {:operator => '!match', :values => ['!(~sub project ~block) +issue']}} + issues = find_issues_with_query(query) + assert_equal [1, 2, 3, 5, 6, 9, 10, 13], issues.collect(&:id).sort + + query = IssueQuery.new(:name => '_') + query.filters = {'subject' => {:operator => '!match', :values => ['+{closed ver} ~{locked ver}']}} + issues = find_issues_with_query(query) + assert_equal [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 14], issues.collect(&:id).sort + end + + def test_filter_on_orfilter_and_any + query = IssueQuery.new(:name => '_') + query.filters = {'project_id' => {:operator => '=', :values => [1]}, + 'and_any' => {:operator => '=', :values => [1]}, + 'status_id' => {:operator => '!', :values => [1]}, + 'assigned_to_id' => {:operator => '=', :values => [3]}} + issues = find_issues_with_query(query) + assert_equal [2, 3, 8, 11, 12], issues.collect(&:id).sort + end + + def test_filter_on_orfilter_and_any_not + query = IssueQuery.new(:name => '_') + query.filters = {'project_id' => {:operator => '=', :values => [1]}, + 'and_any' => {:operator => '!', :values => [1]}, + 'status_id' => {:operator => '=', :values => [2]}, + 'author_id' => {:operator => '=', :values => [3]}} + issues = find_issues_with_query(query) + assert_equal [1, 3, 7, 8, 11], issues.collect(&:id).sort + end + + def test_filter_on_orfilter_or_any + query = IssueQuery.new(:name => '_') + query.filters = {'status_id' => {:operator => '!', :values => [1]}, + 'or_any' => {:operator => '=', :values => [1]}, + 'project_id' => {:operator => '=', :values => [3]}, + 'assigned_to_id' => {:operator => '=', :values => [2]}} + issues = find_issues_with_query(query) + assert_equal [2, 4, 5, 8, 11, 12, 13, 14], issues.collect(&:id).sort + end + + def test_filter_on_orfilter_or_any_not + query = IssueQuery.new(:name => '_') + query.filters = {'status_id' => {:operator => '!', :values => [1]}, + 'or_any' => {:operator => '!', :values => [1]}, + 'project_id' => {:operator => '=', :values => [3]}, + 'assigned_to_id' => {:operator => '!', :values => [2]}} + issues = find_issues_with_query(query) + assert_equal [2, 4, 8, 11, 12], issues.collect(&:id).sort + end + + def test_filter_on_orfilter_or_all + query = IssueQuery.new(:name => '_') + query.filters = {'project_id' => {:operator => '=', :values => [3]}, + 'or_all' => {:operator => '=', :values => [1]}, + 'author_id' => {:operator => '=', :values => [2]}, + 'assigned_to_id' => {:operator => '=', :values => [2]}} + issues = find_issues_with_query(query) + assert_equal [4, 5, 13, 14], issues.collect(&:id).sort + end + + def test_filter_on_orfilter_or_all_not + query = IssueQuery.new(:name => '_') + query.filters = {'project_id' => {:operator => '=', :values => [3]}, + 'or_all' => {:operator => '!', :values => [1]}, + 'author_id' => {:operator => '=', :values => [2]}, + 'assigned_to_id' => {:operator => '=', :values => [2]}} + issues = find_issues_with_query(query) + assert_equal [2, 3, 5, 12, 13, 14], issues.collect(&:id).sort + end + def test_statement_should_be_nil_with_no_filters q = IssueQuery.new(:name => '_') q.filters = {} -- 2.30.2