Feature #4939 » Redmine5.0.1-or-query-patch.patch
| app/helpers/queries_helper.rb | ||
|---|---|---|
| 43 | 43 |
group = :label_time_tracking |
| 44 | 44 |
elsif %w(attachment attachment_description).include?(field) |
| 45 | 45 |
group = :label_attachment |
| 46 |
elsif field_options[:group] == 'or_filter' |
|
| 47 |
group = :label_orfilter |
|
| 46 | 48 |
end |
| 47 | 49 |
if group |
| 48 | 50 |
(grouped[group] ||= []) << [field_options[:name], field] |
| app/models/issue_query.rb | ||
|---|---|---|
| 269 | 269 |
add_available_filter "issue_id", :type => :integer, :label => :label_issue |
| 270 | 270 | |
| 271 | 271 |
Tracker.disabled_core_fields(trackers).each do |field| |
| 272 |
add_available_filter "and_any", |
|
| 273 |
:name => l(:label_orfilter_and_any), |
|
| 274 |
:type => :list, |
|
| 275 |
:values => [l(:general_text_Yes)], |
|
| 276 |
:group => 'or_filter' |
|
| 277 |
add_available_filter "or_any", |
|
| 278 |
:name => l(:label_orfilter_or_any), |
|
| 279 |
:type => :list, |
|
| 280 |
:values => [l(:general_text_Yes)], |
|
| 281 |
:group => 'or_filter' |
|
| 282 |
add_available_filter "or_all", |
|
| 283 |
:name => l(:label_orfilter_or_all), |
|
| 284 |
:type => :list, |
|
| 285 |
:values => [l(:general_text_Yes)], |
|
| 286 |
:group => 'or_filter' |
|
| 287 | ||
| 272 | 288 |
delete_available_filter field |
| 273 | 289 |
end |
| 274 | 290 |
end |
| app/models/query.rb | ||
|---|---|---|
| 307 | 307 |
"!p" => :label_no_issues_in_project, |
| 308 | 308 |
"*o" => :label_any_open_issues, |
| 309 | 309 |
"!o" => :label_no_open_issues, |
| 310 |
"match" => :label_match, |
|
| 311 |
"!match" => :label_not_match |
|
| 310 | 312 |
} |
| 311 | 313 | |
| 312 | 314 |
class_attribute :operators_by_filter_type |
| ... | ... | |
| 318 | 320 |
:date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "nd", "t", "ld", "nw", "w", "lw", "l2w", "nm", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ], |
| 319 | 321 |
:date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ], |
| 320 | 322 |
:string => [ "~", "=", "!~", "!", "^", "$", "!*", "*" ], |
| 321 |
:text => [ "~", "!~", "^", "$", "!*", "*" ], |
|
| 323 |
:text => [ "~", "!~", "^", "$", "!*", "*", "match", "!match" ],
|
|
| 322 | 324 |
:integer => [ "=", ">=", "<=", "><", "!*", "*" ], |
| 323 | 325 |
:float => [ "=", ">=", "<=", "><", "!*", "*" ], |
| 324 | 326 |
:relation => ["=", "!", "=p", "=!p", "!p", "*o", "!o", "!*", "*"], |
| ... | ... | |
| 959 | 961 |
end |
| 960 | 962 | |
| 961 | 963 |
def statement |
| 962 |
# filters clauses |
|
| 963 |
filters_clauses = [] |
|
| 964 |
filters_clauses=[] |
|
| 965 |
and_clauses=[] |
|
| 966 |
and_any_clauses=[] |
|
| 967 |
or_any_clauses=[] |
|
| 968 |
or_all_clauses=[] |
|
| 969 |
and_any_op = "" |
|
| 970 |
or_any_op = "" |
|
| 971 |
or_all_op = "" |
|
| 972 | ||
| 973 |
#the AND filter start first |
|
| 974 |
filters_clauses = and_clauses |
|
| 975 | ||
| 964 | 976 |
filters.each_key do |field| |
| 965 | 977 |
next if field == "subproject_id" |
| 978 |
if field == "and_any" |
|
| 979 |
#start the and any part, point filters_clause to and_any_clauses |
|
| 980 |
filters_clauses = and_any_clauses |
|
| 981 |
and_any_op = operator_for(field) == "=" ? " AND " : " AND NOT " |
|
| 982 |
next |
|
| 983 |
elsif field == "or_any" |
|
| 984 |
#start the or any part, point filters_clause to or_any_clauses |
|
| 985 |
filters_clauses = or_any_clauses |
|
| 986 |
or_any_op = operator_for(field) == "=" ? " OR " : " OR NOT " |
|
| 987 |
next |
|
| 988 |
elsif field == "or_all" |
|
| 989 |
#start the or any part, point filters_clause to or_any_clauses |
|
| 990 |
filters_clauses = or_all_clauses |
|
| 991 |
or_all_op = operator_for(field) == "=" ? " OR " : " OR NOT " |
|
| 992 |
next |
|
| 993 |
end |
|
| 966 | 994 | |
| 967 | 995 |
v = values_for(field).clone |
| 968 | 996 |
next unless v and !v.empty? |
| ... | ... | |
| 997 | 1025 |
filters_clauses << sql_for_custom_field(field, operator, v, $1) |
| 998 | 1026 |
elsif field =~ /^cf_(\d+)\.(.+)$/ |
| 999 | 1027 |
filters_clauses << sql_for_custom_field_attribute(field, operator, v, $1, $2) |
| 1000 |
elsif respond_to?(method = "sql_for_#{field.tr('.', '_')}_field")
|
|
| 1028 |
elsif respond_to?(method = "sql_for_#{field.gsub('.', '_')}_field")
|
|
| 1001 | 1029 |
# specific statement |
| 1002 | 1030 |
filters_clauses << send(method, field, operator, v) |
| 1003 | 1031 |
else |
| ... | ... | |
| 1011 | 1039 |
filters_clauses << c.custom_field.visibility_by_project_condition |
| 1012 | 1040 |
end |
| 1013 | 1041 | |
| 1014 |
filters_clauses << project_statement |
|
| 1015 |
filters_clauses.reject!(&:blank?) |
|
| 1042 |
#now start build the full statement, project filter is allways AND |
|
| 1043 |
and_clauses.reject!(&:blank?) |
|
| 1044 |
and_statement = and_clauses.any? ? and_clauses.join(" AND ") : nil
|
|
| 1045 | ||
| 1046 |
all_and_statement = ["#{project_statement}", "#{and_statement}"].reject(&:blank?)
|
|
| 1047 |
all_and_statement = all_and_statement.any? ? all_and_statement.join(" AND ") : nil
|
|
| 1048 | ||
| 1049 | ||
| 1050 |
# finish the traditional part. Now extended part |
|
| 1051 |
# add the and_any first |
|
| 1052 |
and_any_clauses.reject!(&:blank?) |
|
| 1053 |
and_any_statement = and_any_clauses.any? ? "("+ and_any_clauses.join(" OR ") +")" : nil
|
|
| 1054 | ||
| 1055 |
full_statement_ext_1 = ["#{all_and_statement}", "#{and_any_statement}"].reject(&:blank?)
|
|
| 1056 |
full_statement_ext_1 = full_statement_ext_1.any? ? full_statement_ext_1.join(and_any_op) : nil |
|
| 1057 | ||
| 1058 |
# then add the or_all |
|
| 1059 |
or_all_clauses.reject!(&:blank?) |
|
| 1060 |
or_all_statement = or_all_clauses.any? ? "("+ or_all_clauses.join(" AND ") +")" : nil
|
|
| 1061 | ||
| 1062 |
full_statement_ext_2 = ["#{full_statement_ext_1}", "#{or_all_statement}"].reject(&:blank?)
|
|
| 1063 |
full_statement_ext_2 = full_statement_ext_2.any? ? full_statement_ext_2.join(or_all_op) : nil |
|
| 1064 | ||
| 1065 |
# then add the or_any |
|
| 1066 |
or_any_clauses.reject!(&:blank?) |
|
| 1067 |
or_any_statement = or_any_clauses.any? ? "("+ or_any_clauses.join(" OR ") +")" : nil
|
|
| 1016 | 1068 | |
| 1017 |
filters_clauses.any? ? filters_clauses.join(' AND ') : nil
|
|
| 1069 |
full_statement = ["#{full_statement_ext_2}", "#{or_any_statement}"].reject(&:blank?)
|
|
| 1070 |
full_statement = full_statement.any? ? full_statement.join(or_any_op) : nil |
|
| 1071 | ||
| 1072 |
Rails.logger.info "STATEMENT #{full_statement}"
|
|
| 1073 | ||
| 1074 |
return full_statement |
|
| 1018 | 1075 |
end |
| 1019 | 1076 | |
| 1020 | 1077 |
# Returns the result count by group or nil if query is not grouped |
| ... | ... | |
| 1427 | 1484 |
sql = sql_contains("#{db_table}.#{db_field}", value.first, :starts_with => true)
|
| 1428 | 1485 |
when "$" |
| 1429 | 1486 |
sql = sql_contains("#{db_table}.#{db_field}", value.first, :ends_with => true)
|
| 1487 |
when "match" |
|
| 1488 |
sql = sql_for_match_operators(field, operator, value, db_table, db_field, is_custom_filter) |
|
| 1489 |
when "!match" |
|
| 1490 |
sql = sql_for_match_operators(field, operator, value, db_table, db_field, is_custom_filter) |
|
| 1430 | 1491 |
else |
| 1431 | 1492 |
raise QueryError, "Unknown query operator #{operator}"
|
| 1432 | 1493 |
end |
| ... | ... | |
| 1434 | 1495 |
return sql |
| 1435 | 1496 |
end |
| 1436 | 1497 | |
| 1498 |
def sql_for_match_operators(field, operator, value, db_table, db_field, is_custom_filter=false) |
|
| 1499 |
sql = '' |
|
| 1500 |
v = "(" + value.first.strip + ")"
|
|
| 1501 | ||
| 1502 |
match = true |
|
| 1503 |
op = "" |
|
| 1504 |
term = "" |
|
| 1505 |
in_term = false |
|
| 1506 | ||
| 1507 |
in_bracket = false |
|
| 1508 | ||
| 1509 |
v.chars.each do |c| |
|
| 1510 | ||
| 1511 |
if (!in_bracket && "()+~!".include?(c) && in_term ) || (in_bracket && "}".include?(c)) |
|
| 1512 |
if !term.empty? |
|
| 1513 |
sql += "(" + sql_contains("#{db_table}.#{db_field}", term, match) + ")"
|
|
| 1514 |
end |
|
| 1515 |
#reset |
|
| 1516 |
op = "" |
|
| 1517 |
term = "" |
|
| 1518 |
in_term = false |
|
| 1519 | ||
| 1520 |
in_bracket = (c == "{")
|
|
| 1521 |
end |
|
| 1522 | ||
| 1523 |
if in_bracket && (!"{}".include? c)
|
|
| 1524 |
term += c |
|
| 1525 |
in_term = true |
|
| 1526 |
else |
|
| 1527 | ||
| 1528 |
case c |
|
| 1529 |
when "{"
|
|
| 1530 |
in_bracket = true |
|
| 1531 |
when "}" |
|
| 1532 |
in_bracket = false |
|
| 1533 |
when "("
|
|
| 1534 |
sql += c |
|
| 1535 |
when ")" |
|
| 1536 |
sql += c |
|
| 1537 |
when "+" |
|
| 1538 |
sql += " AND " if sql.last != "("
|
|
| 1539 |
when "~" |
|
| 1540 |
sql += " OR " if sql.last != "("
|
|
| 1541 |
when "!" |
|
| 1542 |
sql += " NOT " |
|
| 1543 |
else |
|
| 1544 |
if c != " " |
|
| 1545 |
term += c |
|
| 1546 |
in_term = true |
|
| 1547 |
end |
|
| 1548 |
end |
|
| 1549 | ||
| 1550 |
end |
|
| 1551 |
end |
|
| 1552 | ||
| 1553 |
if operator.include? "!" |
|
| 1554 |
sql = " NOT " + sql |
|
| 1555 |
end |
|
| 1556 | ||
| 1557 |
Rails.logger.info "MATCH EXPRESSION: V=#{value.first}, SQL=#{sql}"
|
|
| 1558 |
return sql |
|
| 1559 |
end |
|
| 1560 | ||
| 1437 | 1561 |
# Returns a SQL LIKE statement with wildcards |
| 1438 | 1562 |
def sql_contains(db_field, value, options={})
|
| 1439 | 1563 |
options = {} unless options.is_a?(Hash)
|
| config/locales/de.yml | ||
|---|---|---|
| 843 | 843 |
label_year: Jahr |
| 844 | 844 |
label_yesterday: gestern |
| 845 | 845 |
label_default_query: Standardabfrage |
| 846 |
label_orfilter: "ODER Filter" |
|
| 847 |
label_orfilter_and_any: "UND einer der folgenden" |
|
| 848 |
label_orfilter_or_any: "ODER einer der folgenden" |
|
| 849 |
label_orfilter_or_all: "ODER alle folgenden" |
|
| 846 | 850 | |
| 847 | 851 |
mail_body_account_activation_request: "Ein neuer Benutzer (%{value}) hat sich registriert. Sein Konto wartet auf Ihre Genehmigung:"
|
| 848 | 852 |
mail_body_account_information: Ihre Konto-Informationen |
| config/locales/en.yml | ||
|---|---|---|
| 1124 | 1124 |
label_my_bookmarks: My bookmarks |
| 1125 | 1125 |
label_assign_to_me: Assign to me |
| 1126 | 1126 |
label_default_query: Default query |
| 1127 |
label_orfilter: "OR filters" |
|
| 1128 |
label_orfilter_and_any: "AND any following" |
|
| 1129 |
label_orfilter_or_any: "OR any following" |
|
| 1130 |
label_orfilter_or_all: "OR all following" |
|
| 1131 |
label_match: "match" |
|
| 1132 |
label_not_match: "not match" |
|
| 1127 | 1133 | |
| 1128 | 1134 |
button_login: Login |
| 1129 | 1135 |
button_submit: Submit |
| config/locales/ja.yml | ||
|---|---|---|
| 819 | 819 |
label_parent_revision: 親 |
| 820 | 820 |
label_child_revision: 子 |
| 821 | 821 |
label_gantt_progress_line: イナズマ線 |
| 822 |
label_orfilter: "ORフィルタ" |
|
| 823 |
label_orfilter_and_any: "上記 かつ (以下のいずれか)" |
|
| 824 |
label_orfilter_or_any: "上記 または (以下のいずれか)" |
|
| 825 |
label_orfilter_or_all: "上記 または (以下の全て)" |
|
| 826 |
label_match: "match" |
|
| 827 |
label_not_match: "not match" |
|
| 822 | 828 | |
| 823 | 829 |
button_login: ログイン |
| 824 | 830 |
button_submit: 送信 |
| test/unit/query_test.rb | ||
|---|---|---|
| 1598 | 1598 |
assert_equal [5, 8, 9], issues.collect(&:id).sort |
| 1599 | 1599 |
end |
| 1600 | 1600 | |
| 1601 |
def test_filter_on_subject_match |
|
| 1602 |
query = IssueQuery.new(:name => '_') |
|
| 1603 |
query.filters = {'subject' => {:operator => 'match', :values => ['issue']}}
|
|
| 1604 |
issues = find_issues_with_query(query) |
|
| 1605 |
assert_equal [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], issues.collect(&:id).sort |
|
| 1606 | ||
| 1607 |
query = IssueQuery.new(:name => '_') |
|
| 1608 |
query.filters = {'subject' => {:operator => 'match', :values => ['(~project ~recipe) +!sub']}}
|
|
| 1609 |
issues = find_issues_with_query(query) |
|
| 1610 |
assert_equal [1, 3, 4, 14], issues.collect(&:id).sort |
|
| 1611 | ||
| 1612 |
query = IssueQuery.new(:name => '_') |
|
| 1613 |
query.filters = {'subject' => {:operator => 'match', :values => ['!(~sub project ~block) +issue']}}
|
|
| 1614 |
issues = find_issues_with_query(query) |
|
| 1615 |
assert_equal [4, 7, 8, 11, 12, 14], issues.collect(&:id).sort |
|
| 1616 | ||
| 1617 |
query = IssueQuery.new(:name => '_') |
|
| 1618 |
query.filters = {'subject' => {:operator => 'match', :values => ['+{closed ver} ~{locked ver}']}}
|
|
| 1619 |
issues = find_issues_with_query(query) |
|
| 1620 |
assert_equal [11, 12], issues.collect(&:id).sort |
|
| 1621 |
end |
|
| 1622 | ||
| 1623 |
def test_filter_on_subject_not_match |
|
| 1624 |
query = IssueQuery.new(:name => '_') |
|
| 1625 |
query.filters = {'subject' => {:operator => '!match', :values => ['issue']}}
|
|
| 1626 |
issues = find_issues_with_query(query) |
|
| 1627 |
assert_equal [1, 2, 3], issues.collect(&:id).sort |
|
| 1628 | ||
| 1629 |
query = IssueQuery.new(:name => '_') |
|
| 1630 |
query.filters = {'subject' => {:operator => '!match', :values => ['(~project ~recipe) +!sub']}}
|
|
| 1631 |
issues = find_issues_with_query(query) |
|
| 1632 |
assert_equal [2, 5, 6, 7, 8, 9, 10, 11, 12, 13], issues.collect(&:id).sort |
|
| 1633 | ||
| 1634 |
query = IssueQuery.new(:name => '_') |
|
| 1635 |
query.filters = {'subject' => {:operator => '!match', :values => ['!(~sub project ~block) +issue']}}
|
|
| 1636 |
issues = find_issues_with_query(query) |
|
| 1637 |
assert_equal [1, 2, 3, 5, 6, 9, 10, 13], issues.collect(&:id).sort |
|
| 1638 | ||
| 1639 |
query = IssueQuery.new(:name => '_') |
|
| 1640 |
query.filters = {'subject' => {:operator => '!match', :values => ['+{closed ver} ~{locked ver}']}}
|
|
| 1641 |
issues = find_issues_with_query(query) |
|
| 1642 |
assert_equal [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 14], issues.collect(&:id).sort |
|
| 1643 |
end |
|
| 1644 | ||
| 1645 |
def test_filter_on_orfilter_and_any |
|
| 1646 |
query = IssueQuery.new(:name => '_') |
|
| 1647 |
query.filters = {'project_id' => {:operator => '=', :values => [1]},
|
|
| 1648 |
'and_any' => {:operator => '=', :values => [1]},
|
|
| 1649 |
'status_id' => {:operator => '!', :values => [1]},
|
|
| 1650 |
'assigned_to_id' => {:operator => '=', :values => [3]}}
|
|
| 1651 |
issues = find_issues_with_query(query) |
|
| 1652 |
assert_equal [2, 3, 8, 11, 12], issues.collect(&:id).sort |
|
| 1653 |
end |
|
| 1654 | ||
| 1655 |
def test_filter_on_orfilter_and_any_not |
|
| 1656 |
query = IssueQuery.new(:name => '_') |
|
| 1657 |
query.filters = {'project_id' => {:operator => '=', :values => [1]},
|
|
| 1658 |
'and_any' => {:operator => '!', :values => [1]},
|
|
| 1659 |
'status_id' => {:operator => '=', :values => [2]},
|
|
| 1660 |
'author_id' => {:operator => '=', :values => [3]}}
|
|
| 1661 |
issues = find_issues_with_query(query) |
|
| 1662 |
assert_equal [1, 3, 7, 8, 11], issues.collect(&:id).sort |
|
| 1663 |
end |
|
| 1664 | ||
| 1665 |
def test_filter_on_orfilter_or_any |
|
| 1666 |
query = IssueQuery.new(:name => '_') |
|
| 1667 |
query.filters = {'status_id' => {:operator => '!', :values => [1]},
|
|
| 1668 |
'or_any' => {:operator => '=', :values => [1]},
|
|
| 1669 |
'project_id' => {:operator => '=', :values => [3]},
|
|
| 1670 |
'assigned_to_id' => {:operator => '=', :values => [2]}}
|
|
| 1671 |
issues = find_issues_with_query(query) |
|
| 1672 |
assert_equal [2, 4, 5, 8, 11, 12, 13, 14], issues.collect(&:id).sort |
|
| 1673 |
end |
|
| 1674 | ||
| 1675 |
def test_filter_on_orfilter_or_any_not |
|
| 1676 |
query = IssueQuery.new(:name => '_') |
|
| 1677 |
query.filters = {'status_id' => {:operator => '!', :values => [1]},
|
|
| 1678 |
'or_any' => {:operator => '!', :values => [1]},
|
|
| 1679 |
'project_id' => {:operator => '=', :values => [3]},
|
|
| 1680 |
'assigned_to_id' => {:operator => '!', :values => [2]}}
|
|
| 1681 |
issues = find_issues_with_query(query) |
|
| 1682 |
assert_equal [2, 4, 8, 11, 12], issues.collect(&:id).sort |
|
| 1683 |
end |
|
| 1684 | ||
| 1685 |
def test_filter_on_orfilter_or_all |
|
| 1686 |
query = IssueQuery.new(:name => '_') |
|
| 1687 |
query.filters = {'project_id' => {:operator => '=', :values => [3]},
|
|
| 1688 |
'or_all' => {:operator => '=', :values => [1]},
|
|
| 1689 |
'author_id' => {:operator => '=', :values => [2]},
|
|
| 1690 |
'assigned_to_id' => {:operator => '=', :values => [2]}}
|
|
| 1691 |
issues = find_issues_with_query(query) |
|
| 1692 |
assert_equal [4, 5, 13, 14], issues.collect(&:id).sort |
|
| 1693 |
end |
|
| 1694 | ||
| 1695 |
def test_filter_on_orfilter_or_all_not |
|
| 1696 |
query = IssueQuery.new(:name => '_') |
|
| 1697 |
query.filters = {'project_id' => {:operator => '=', :values => [3]},
|
|
| 1698 |
'or_all' => {:operator => '!', :values => [1]},
|
|
| 1699 |
'author_id' => {:operator => '=', :values => [2]},
|
|
| 1700 |
'assigned_to_id' => {:operator => '=', :values => [2]}}
|
|
| 1701 |
issues = find_issues_with_query(query) |
|
| 1702 |
assert_equal [2, 3, 5, 12, 13, 14], issues.collect(&:id).sort |
|
| 1703 |
end |
|
| 1704 | ||
| 1601 | 1705 |
def test_statement_should_be_nil_with_no_filters |
| 1602 | 1706 |
q = IssueQuery.new(:name => '_') |
| 1603 | 1707 |
q.filters = {}
|