Feature #4939 » or-query-patch-for-Redmine-5.1.0.patch
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 = {} |
- « Previous
- 1
- …
- 13
- 14
- 15
- Next »