Project

General

Profile

Feature #4939 » or-query-patch-for-Redmine-5.1.0.patch

Yasu Saku, 2023-11-12 14:01

View differences:

app/helpers/queries_helper.rb
45 45
        group = :label_attachment
46 46
      elsif [:string, :text, :search].include?(field_options[:type])
47 47
        group = :label_string
48
      elsif field_options[:group] == 'or_filter'
49
        group = :label_orfilter
48 50
      end
49 51
      if group
50 52
        (grouped[group] ||= []) << [field_options[:name], field]
app/models/issue_query.rb
274 274

  
275 275
    add_available_filter "any_searchable", :type => :search
276 276

  
277
    add_available_filter "and_any",
278
        :name => l(:label_orfilter_and_any),
279
        :type => :list,
280
        :values => [l(:general_text_Yes)],
281
        :group => 'or_filter'
282
    add_available_filter "or_any",
283
        :name => l(:label_orfilter_or_any),
284
        :type => :list,
285
        :values => [l(:general_text_Yes)],
286
        :group => 'or_filter'
287
    add_available_filter "or_all",
288
        :name => l(:label_orfilter_or_all),
289
        :type => :list,
290
        :values => [l(:general_text_Yes)],
291
        :group => 'or_filter'
292

  
277 293
    Tracker.disabled_core_fields(trackers).each do |field|
278 294
      delete_available_filter field
279 295
    end
app/models/query.rb
316 316
    "!o"  => :label_no_open_issues,
317 317
    "ev"  => :label_has_been,       # "ev" stands for "ever"
318 318
    "!ev" => :label_has_never_been,
319
    "cf"  => :label_changed_from
319
    "cf"  => :label_changed_from,
320
    "match"  => :label_match,
321
    "!match" => :label_not_match
320 322
  }
321 323

  
322 324
  class_attribute :operators_by_filter_type
......
330 332
    :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "nd", "t", "ld", "nw", "w", "lw", "l2w", "nm", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
331 333
    :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
332 334
    :string => [ "~", "*~", "=", "!~", "!", "^", "$", "!*", "*" ],
333
    :text => [  "~", "*~", "!~", "^", "$", "!*", "*" ],
335
    :text => [  "~", "*~", "!~", "^", "$", "!*", "*", "match", "!match" ],
334 336
    :search => [ "~", "*~", "!~" ],
335 337
    :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
336 338
    :float => [ "=", ">=", "<=", "><", "!*", "*" ],
......
972 974
  end
973 975

  
974 976
  def statement
975
    # filters clauses
976
    filters_clauses = []
977
    filters_clauses=[]
978
    and_clauses=[]
979
    and_any_clauses=[]
980
    or_any_clauses=[]
981
    or_all_clauses=[]
982
    and_any_op = ""
983
    or_any_op = ""
984
    or_all_op = ""
985

  
986
    #the AND filter start first
987
    filters_clauses = and_clauses
988

  
977 989
    filters.each_key do |field|
978 990
      next if field == "subproject_id"
991
      if field == "and_any"
992
         #start the and any part, point filters_clause to and_any_clauses
993
         filters_clauses = and_any_clauses
994
         and_any_op = operator_for(field) == "=" ? " AND " : " AND NOT "
995
         next
996
      elsif field == "or_any"
997
         #start the or any part, point filters_clause to or_any_clauses
998
         filters_clauses = or_any_clauses
999
         or_any_op = operator_for(field) == "=" ? " OR " : " OR NOT "
1000
         next
1001
      elsif  field == "or_all"
1002
         #start the or any part, point filters_clause to or_any_clauses
1003
         filters_clauses = or_all_clauses
1004
         or_all_op = operator_for(field) == "=" ? " OR " : " OR NOT "
1005
         next
1006
      end
979 1007

  
980 1008
      v = values_for(field).clone
981 1009
      next unless v and !v.empty?
......
1010 1038
        filters_clauses << sql_for_custom_field(field, operator, v, $1)
1011 1039
      elsif field =~ /^cf_(\d+)\.(.+)$/
1012 1040
        filters_clauses << sql_for_custom_field_attribute(field, operator, v, $1, $2)
1013
      elsif respond_to?(method = "sql_for_#{field.tr('.', '_')}_field")
1041
      elsif respond_to?(method = "sql_for_#{field.gsub('.', '_')}_field")
1014 1042
        # specific statement
1015 1043
        filters_clauses << send(method, field, operator, v)
1016 1044
      else
......
1024 1052
      filters_clauses << c.custom_field.visibility_by_project_condition
1025 1053
    end
1026 1054

  
1027
    filters_clauses << project_statement
1028
    filters_clauses.reject!(&:blank?)
1055
    #now start build the full statement, project filter is allways AND
1056
    and_clauses.reject!(&:blank?)
1057
    and_statement = and_clauses.any? ? and_clauses.join(" AND ") : nil
1058

  
1059
    # finish the traditional part. Now extended part
1060
    # add the and_any first
1061
    and_any_clauses.reject!(&:blank?)
1062
    and_any_statement = and_any_clauses.any? ? "("+ and_any_clauses.join(" OR ") +")" : nil
1063

  
1064
    full_statement_ext_1 = ["#{and_statement}", "#{and_any_statement}"].reject(&:blank?)
1065
    full_statement_ext_1 = full_statement_ext_1.any? ? full_statement_ext_1.join(and_any_op) : nil
1066

  
1067
    # then add the or_all
1068
    or_all_clauses.reject!(&:blank?)
1069
    or_all_statement = or_all_clauses.any? ? "("+ or_all_clauses.join(" AND ") +")" : nil
1070

  
1071
    full_statement_ext_2 = ["#{full_statement_ext_1}", "#{or_all_statement}"].reject(&:blank?)
1072
    full_statement_ext_2 = full_statement_ext_2.any? ? full_statement_ext_2.join(or_all_op) : nil
1073

  
1074
    # then add the or_any
1075
    or_any_clauses.reject!(&:blank?)
1076
    or_any_statement = or_any_clauses.any? ? "("+ or_any_clauses.join(" OR ") +")" : nil
1077

  
1078
    # full statement without project
1079
    full_statement_without_project = ["#{full_statement_ext_2}", "#{or_any_statement}"].reject(&:blank?)
1080
    full_statement_without_project = full_statement_without_project.any? ? "("+ full_statement_without_project.join(or_any_op) +")" : nil
1081

  
1082
    # full statement
1083
    full_statement = ["#{project_statement}", "#{full_statement_without_project}"].reject(&:blank?)
1084
    full_statement = full_statement.any? ? full_statement.join(" AND ") : nil
1029 1085

  
1030
    filters_clauses.any? ? filters_clauses.join(' AND ') : nil
1086
    Rails.logger.info "STATEMENT #{full_statement}"
1087

  
1088
    return full_statement
1031 1089
  end
1032 1090

  
1033 1091
  # Returns the result count by group or nil if query is not grouped
......
1468 1526
      else
1469 1527
        sql = '1=0'
1470 1528
      end
1529
    when "match"
1530
      sql = sql_for_match_operators(field, operator, value, db_table, db_field, is_custom_filter)
1531
    when "!match"
1532
      sql = sql_for_match_operators(field, operator, value, db_table, db_field, is_custom_filter)
1471 1533
    else
1472 1534
      raise QueryError, "Unknown query operator #{operator}"
1473 1535
    end
......
1475 1537
    return sql
1476 1538
  end
1477 1539

  
1540
  def sql_for_match_operators(field, operator, value, db_table, db_field, is_custom_filter=false)
1541
    sql = ''
1542
    v = "(" + value.first.strip + ")"
1543

  
1544
    match = true
1545
    op = ""
1546
    term = ""
1547
    in_term = false
1548

  
1549
    in_bracket = false
1550

  
1551
    v.chars.each do |c|
1552

  
1553
      if (!in_bracket && "()+~!".include?(c) && in_term  ) || (in_bracket && "}".include?(c))
1554
        if !term.empty?
1555
          sql += "(" + sql_contains("#{db_table}.#{db_field}", term, match) + ")"
1556
        end
1557
        #reset
1558
        op = ""
1559
        term = ""
1560
        in_term = false
1561

  
1562
        in_bracket = (c == "{")
1563
      end
1564

  
1565
      if in_bracket && (!"{}".include? c)
1566
        term += c
1567
        in_term = true
1568
      else
1569

  
1570
        case c
1571
        when "{"
1572
          in_bracket = true
1573
        when "}"
1574
          in_bracket = false
1575
        when "("
1576
          sql += c
1577
        when ")"
1578
          sql += c
1579
        when "+"
1580
          sql += " AND " if sql.last != "("
1581
        when "~"
1582
          sql += " OR " if sql.last != "("
1583
        when "!"
1584
          sql += " NOT "
1585
        else
1586
          if c != " "
1587
            term += c
1588
            in_term = true
1589
          end
1590
        end
1591

  
1592
      end
1593
    end
1594

  
1595
    if operator.include? "!"
1596
      sql = " NOT " + sql
1597
    end
1598

  
1599
    Rails.logger.info "MATCH EXPRESSION: V=#{value.first}, SQL=#{sql}"
1600
    return sql
1601
  end
1602

  
1478 1603
  # Returns a SQL LIKE statement with wildcards
1479 1604
  #
1480 1605
  # valid options:
config/locales/de.yml
842 842
  label_year: Jahr
843 843
  label_yesterday: gestern
844 844
  label_default_query: Standardabfrage
845
  label_orfilter: "ODER Filter"
846
  label_orfilter_and_any: "UND einer der folgenden"
847
  label_orfilter_or_any: "ODER einer der folgenden"
848
  label_orfilter_or_all: "ODER alle folgenden"
845 849

  
846 850
  mail_body_account_activation_request: "Ein neuer Benutzer (%{value}) hat sich registriert. Sein Konto wartet auf Ihre Genehmigung:"
847 851
  mail_body_account_information: Ihre Konto-Informationen
config/locales/en.yml
1138 1138
  label_default_query: Default query
1139 1139
  label_edited: Edited
1140 1140
  label_time_by_author: "%{time} by %{author}"
1141
  label_orfilter: "OR filters"
1142
  label_orfilter_and_any: "AND any following"
1143
  label_orfilter_or_any: "OR any following"
1144
  label_orfilter_or_all: "OR all following"
1145
  label_match: "match"
1146
  label_not_match: "not match"
1141 1147

  
1142 1148
  button_login: Login
1143 1149
  button_submit: Submit
config/locales/ja.yml
814 814
  label_parent_revision: 親
815 815
  label_child_revision: 子
816 816
  label_gantt_progress_line: イナズマ線
817
  label_orfilter: "ORフィルタ"
818
  label_orfilter_and_any: "上記 かつ (以下のいずれか)"
819
  label_orfilter_or_any: "上記 または (以下のいずれか)"
820
  label_orfilter_or_all: "上記 または (以下の全て)"
821
  label_match: "match"
822
  label_not_match: "not match"
817 823

  
818 824
  button_login: ログイン
819 825
  button_submit: 送信
test/unit/query_test.rb
1950 1950
    assert_equal [5, 8, 9], issues.collect(&:id).sort
1951 1951
  end
1952 1952

  
1953
  def test_filter_on_subject_match
1954
    query = IssueQuery.new(:name => '_')
1955
    query.filters = {'subject' => {:operator => 'match', :values => ['issue']}}
1956
    issues = find_issues_with_query(query)
1957
    assert_equal [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], issues.collect(&:id).sort
1958

  
1959
    query = IssueQuery.new(:name => '_')
1960
    query.filters = {'subject' => {:operator => 'match', :values => ['(~project ~recipe) +!sub']}}
1961
    issues = find_issues_with_query(query)
1962
    assert_equal [1, 3, 4, 14], issues.collect(&:id).sort
1963

  
1964
    query = IssueQuery.new(:name => '_')
1965
    query.filters = {'subject' => {:operator => 'match', :values => ['!(~sub project ~block) +issue']}}
1966
    issues = find_issues_with_query(query)
1967
    assert_equal [4, 7, 8, 11, 12, 14], issues.collect(&:id).sort
1968

  
1969
    query = IssueQuery.new(:name => '_')
1970
    query.filters = {'subject' => {:operator => 'match', :values => ['+{closed ver} ~{locked ver}']}}
1971
    issues = find_issues_with_query(query)
1972
    assert_equal [11, 12], issues.collect(&:id).sort
1973
  end
1974

  
1975
  def test_filter_on_subject_not_match
1976
    query = IssueQuery.new(:name => '_')
1977
    query.filters = {'subject' => {:operator => '!match', :values => ['issue']}}
1978
    issues = find_issues_with_query(query)
1979
    assert_equal [1, 2, 3], issues.collect(&:id).sort
1980

  
1981
    query = IssueQuery.new(:name => '_')
1982
    query.filters = {'subject' => {:operator => '!match', :values => ['(~project ~recipe) +!sub']}}
1983
    issues = find_issues_with_query(query)
1984
    assert_equal [2, 5, 6, 7, 8, 9, 10, 11, 12, 13], issues.collect(&:id).sort
1985

  
1986
    query = IssueQuery.new(:name => '_')
1987
    query.filters = {'subject' => {:operator => '!match', :values => ['!(~sub project ~block) +issue']}}
1988
    issues = find_issues_with_query(query)
1989
    assert_equal [1, 2, 3, 5, 6, 9, 10, 13], issues.collect(&:id).sort
1990

  
1991
    query = IssueQuery.new(:name => '_')
1992
    query.filters = {'subject' => {:operator => '!match', :values => ['+{closed ver} ~{locked ver}']}}
1993
    issues = find_issues_with_query(query)
1994
    assert_equal [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 14], issues.collect(&:id).sort
1995
  end
1996

  
1997
  def test_filter_on_orfilter_and_any
1998
    query = IssueQuery.new(:name => '_')
1999
    query.filters = {'project_id' => {:operator => '=', :values => [1]},
2000
                     'and_any' => {:operator => '=', :values => [1]},
2001
                     'status_id' => {:operator => '!', :values => [1]},
2002
                     'assigned_to_id' => {:operator => '=', :values => [3]}}
2003
    issues = find_issues_with_query(query)
2004
    assert_equal [2, 3, 8, 11, 12], issues.collect(&:id).sort
2005
  end
2006

  
2007
  def test_filter_on_orfilter_and_any_not
2008
    query = IssueQuery.new(:name => '_')
2009
    query.filters = {'project_id' => {:operator => '=', :values => [1]},
2010
                     'and_any' => {:operator => '!', :values => [1]},
2011
                     'status_id' => {:operator => '=', :values => [2]},
2012
                     'author_id' => {:operator => '=', :values => [3]}}
2013
    issues = find_issues_with_query(query)
2014
    assert_equal [1, 3, 7, 8, 11], issues.collect(&:id).sort
2015
  end
2016

  
2017
  def test_filter_on_orfilter_or_any
2018
    query = IssueQuery.new(:name => '_')
2019
    query.filters = {'status_id' => {:operator => '!', :values => [1]},
2020
                     'or_any' => {:operator => '=', :values => [1]},
2021
                     'project_id' => {:operator => '=', :values => [3]},
2022
                     'assigned_to_id' => {:operator => '=', :values => [2]}}
2023
    issues = find_issues_with_query(query)
2024
    assert_equal [2, 4, 5, 8, 11, 12, 13, 14], issues.collect(&:id).sort
2025
  end
2026

  
2027
  def test_filter_on_orfilter_or_any_not
2028
    query = IssueQuery.new(:name => '_')
2029
    query.filters = {'status_id' => {:operator => '!', :values => [1]},
2030
                     'or_any' => {:operator => '!', :values => [1]},
2031
                     'project_id' => {:operator => '=', :values => [3]},
2032
                     'assigned_to_id' => {:operator => '!', :values => [2]}}
2033
    issues = find_issues_with_query(query)
2034
    assert_equal [2, 4, 8, 11, 12], issues.collect(&:id).sort
2035
  end
2036

  
2037
  def test_filter_on_orfilter_or_all
2038
    query = IssueQuery.new(:name => '_')
2039
    query.filters = {'project_id' => {:operator => '=', :values => [3]},
2040
                     'or_all' => {:operator => '=', :values => [1]},
2041
                     'author_id' => {:operator => '=', :values => [2]},
2042
                     'assigned_to_id' => {:operator => '=', :values => [2]}}
2043
    issues = find_issues_with_query(query)
2044
    assert_equal [4, 5, 13, 14], issues.collect(&:id).sort
2045
  end
2046

  
2047
  def test_filter_on_orfilter_or_all_not
2048
    query = IssueQuery.new(:name => '_')
2049
    query.filters = {'project_id' => {:operator => '=', :values => [3]},
2050
                     'or_all' => {:operator => '!', :values => [1]},
2051
                     'author_id' => {:operator => '=', :values => [2]},
2052
                     'assigned_to_id' => {:operator => '=', :values => [2]}}
2053
    issues = find_issues_with_query(query)
2054
    assert_equal [2, 3, 5, 12, 13, 14], issues.collect(&:id).sort
2055
  end
2056

  
1953 2057
  def test_statement_should_be_nil_with_no_filters
1954 2058
    q = IssueQuery.new(:name => '_')
1955 2059
    q.filters = {}
(15-15/15)