From 8aacd466590a53b7034b86944780f73d37b58234 Mon Sep 17 00:00:00 2001 From: Vegaminer Date: Fri, 31 Jan 2020 13:10:58 +0300 Subject: [PATCH 1/6] Russian translation --- config/locales/ru.yml | 61 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 config/locales/ru.yml diff --git a/config/locales/ru.yml b/config/locales/ru.yml new file mode 100644 index 0000000..00102b3 --- /dev/null +++ b/config/locales/ru.yml @@ -0,0 +1,61 @@ +# encoding: utf-8 +# +# Redmine plugin to add necessary filters to queries +# +# Copyright © 2019-2020 Stephan Wenzel +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# + +ru: + + label_begins_with: "начинается с" + label_begins_with_any: "начинается с любого из" + label_not_begins_with: "не начинается с" + label_not_begins_with_any: "не начинается с любого из" + + label_ends_with: "заканчивается на" + label_ends_with_any: "заканчивается на любой из" + label_not_ends_with: "не заканчивается на" + label_not_ends_with_any: "не заканчивается на любой из" + + label_contains_any: "содержит любой из" + label_not_contains_any: "не содержит любой из" + label_contains_all: "содержит всё из" + label_not_contains_all: "не содержит ни один из" + + label_any_of: "один из" + label_none_of: "ни один из" + + label_tomorrow: "завтра" + label_next_week: "следующая неделя" + label_next_month: "следующий месяц" + + label_is_strict: "является (точно)" + label_is_not_strict: "не является (точно)" + + label_past: "прошлое" + label_future: "ожидается" + + label_time: "время" + label_hour_plural: "часы" + label_between_ago: "с по" + label_less_than_hours_ago: "меньше чем часов тому назад" + label_more_than_hours_ago: "больше чем часов тому назад" + label_o_clock: "время" + + field_created_on_by_clock_time: "Создано (время)" + field_updated_on_by_clock_time: "Изменено (время)" + From 7150bb315348eb9ee9f8174450b3b9698d81547d Mon Sep 17 00:00:00 2001 From: Vegaminer Date: Thu, 13 Feb 2020 20:10:33 +0300 Subject: [PATCH 2/6] Adding OR filters discussed at https://www.redmine.org/issues/4939#note-56 --- config/locales/ja.yml | 7 ++ test/unit/query_test.rb | 145 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 config/locales/ja.yml create mode 100644 test/unit/query_test.rb diff --git a/config/locales/ja.yml b/config/locales/ja.yml new file mode 100644 index 0000000..26f14f5 --- /dev/null +++ b/config/locales/ja.yml @@ -0,0 +1,7 @@ +ja: + label_orfilter: "ORフィルタ" + label_orfilter_and_any: "上記 かつ (以下のいずれか)" + label_orfilter_or_any: "上記 または (以下のいずれか)" + label_orfilter_or_all: "上記 または (以下の全て)" + label_match: "match" + label_not_match: "not match" \ No newline at end of file diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb new file mode 100644 index 0000000..ce6db86 --- /dev/null +++ b/test/unit/query_test.rb @@ -0,0 +1,145 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2017 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class QueryTest < ActiveSupport::TestCase + include Redmine::I18n + + fixtures :projects, :enabled_modules, :users, :members, + :member_roles, :roles, :trackers, :issue_statuses, + :issue_categories, :enumerations, :issues, + :watchers, :custom_fields, :custom_values, :versions, + :queries, + :projects_trackers, + :custom_fields_trackers, + :workflows, :journals, + :attachments + + INTEGER_KLASS = RUBY_VERSION >= "2.4" ? Integer : Fixnum + + def setup + User.current = nil + 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 + +end From 2c5b4082df34e3a18e164eabe3b31ec83c0b74eb Mon Sep 17 00:00:00 2001 From: Vegaminer Date: Thu, 13 Feb 2020 20:33:23 +0300 Subject: [PATCH 3/6] Adding OR filters discussed at https://www.redmine.org/issues/4939#note-56 --- config/locales/de.yml | 6 + config/locales/en.yml | 6 + config/locales/ru.yml | 7 + .../patches/issue_query_patch.rb | 27 +++- .../patches/queries_helper_patch.rb | 3 + .../patches/query_patch.rb | 141 +++++++++++++++++- 6 files changed, 178 insertions(+), 12 deletions(-) diff --git a/config/locales/de.yml b/config/locales/de.yml index 86428df..1e7adb2 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -58,4 +58,10 @@ de: field_created_on_by_clock_time: "Angelegt (Uhrzeit)" field_updated_on_by_clock_time: "Aktualisiert (Uhrzeit)" + + 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" + diff --git a/config/locales/en.yml b/config/locales/en.yml index 67906af..68ea2ed 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -59,3 +59,9 @@ en: field_created_on_by_clock_time: "Created (Time of Day)" field_updated_on_by_clock_time: "Updated (Time of Day)" + 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" \ No newline at end of file diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 00102b3..1dedc7e 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -58,4 +58,11 @@ ru: field_created_on_by_clock_time: "Создано (время)" field_updated_on_by_clock_time: "Изменено (время)" + + label_orfilter: "ИЛИ фильтры" + label_orfilter_and_any: "И любое из следующих" + label_orfilter_or_any: "ИЛИ любое из следующих" + label_orfilter_or_all: "ИЛИ всё из следующих" + label_match: "соответствует" + label_not_match: "не соответствует" diff --git a/lib/redmine_more_filters/patches/issue_query_patch.rb b/lib/redmine_more_filters/patches/issue_query_patch.rb index 485cd8b..8318535 100644 --- a/lib/redmine_more_filters/patches/issue_query_patch.rb +++ b/lib/redmine_more_filters/patches/issue_query_patch.rb @@ -55,13 +55,28 @@ def initialize_available_filters_with_more_filters initialize_available_filters_without_more_filters - add_available_filter("created_on_by_clock_time", - :type => :time_past - ) - add_available_filter("updated_on_by_clock_time", - :type => :time_past - ) + add_available_filter("created_on_by_clock_time", + :type => :time_past + ) + add_available_filter("updated_on_by_clock_time", + :type => :time_past + ) + 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' end #def def sql_for_created_on_by_clock_time_field(field, operator, value) diff --git a/lib/redmine_more_filters/patches/queries_helper_patch.rb b/lib/redmine_more_filters/patches/queries_helper_patch.rb index 8ebea36..d02e80f 100644 --- a/lib/redmine_more_filters/patches/queries_helper_patch.rb +++ b/lib/redmine_more_filters/patches/queries_helper_patch.rb @@ -60,6 +60,7 @@ def filters_options_for_select_with_more_filters(query) ungrouped = [] grouped = {} query.available_filters.map do |field, field_options| + Rails.logger.warn field.to_s if field_options[:type] == :relation group = :label_relations elsif field_options[:type] == :tree @@ -75,6 +76,8 @@ def filters_options_for_select_with_more_filters(query) group = :label_time elsif field_options[:type] == :date_past || field_options[:type] == :date group = :label_date + elsif field_options[:group] == 'or_filter' + group = :label_orfilter end if group (grouped[group] ||= []) << [field_options[:name], field] diff --git a/lib/redmine_more_filters/patches/query_patch.rb b/lib/redmine_more_filters/patches/query_patch.rb index 4c8550f..2cfff8d 100644 --- a/lib/redmine_more_filters/patches/query_patch.rb +++ b/lib/redmine_more_filters/patches/query_patch.rb @@ -80,12 +80,14 @@ def self.included(base) "> :label_between, "< :label_past, - "t>>" => :label_future + "t>>" => :label_future, + "match" => :label_match, + "!match" => :label_not_match ) self.operators_by_filter_type[:string].insert(1, "*=", "!*=", "^=", "*^=", "!^=", "!*^=", "$=", "*$=", "!$=", "!*$=", "*~", "!*~", "[~]", "![~]") - self.operators_by_filter_type[:text].insert(1, "^=", "*^=", "!^=", "!*^=", "$=", "*$=", "!$=", "!*$=", "*~", "!*~", "[~]", "![~]") + self.operators_by_filter_type[:text].insert(1, "^=", "*^=", "!^=", "!*^=", "$=", "*$=", "!$=", "!*$=", "*~", "!*~", "[~]", "![~]", "match", "!match" ) self.operators_by_filter_type[:date].insert(14, "nm") self.operators_by_filter_type[:date].insert(11, "nw") @@ -225,12 +227,79 @@ def sql_for_field_with_more_filters(field, operator, value, db_table, db_field, date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter) when "=" sql_for_field_without_more_filters(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) + when "!match" + sql = sql_for_match_operators(field, operator, value, db_table, db_field, is_custom_filter) else sql_for_field_without_more_filters(field, operator, value, db_table, db_field, is_custom_filter) end return sql end #def - + + 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 + def sql_for_custom_field_with_more_filters(field, operator, value, custom_field_id) db_table = CustomValue.table_name db_field = 'value' @@ -272,8 +341,38 @@ def sql_for_custom_field_with_more_filters(field, operator, value, custom_field_ def statement_with_more_filters # 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? operator = operator_for(field) @@ -349,10 +448,40 @@ def statement_with_more_filters 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 + + # then add the or_any + or_any_clauses.reject!(&:blank?) + or_any_statement = or_any_clauses.any? ? "("+ or_any_clauses.join(" OR ") +")" : 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 - filters_clauses.any? ? filters_clauses.join(' AND ') : 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 a SQL LIKE statement with wildcards From def0612ba3dd4480c3ccaebaa9aa5e5065b53c0d Mon Sep 17 00:00:00 2001 From: Keks Keksov Date: Wed, 24 Jul 2024 19:46:53 +0300 Subject: [PATCH 4/6] sql_for_field chainig fix --- .../patches/database_patch.rb | 3 +- .../patches/query_patch.rb | 794 +++++++++--------- 2 files changed, 411 insertions(+), 386 deletions(-) diff --git a/lib/redmine_more_filters/patches/database_patch.rb b/lib/redmine_more_filters/patches/database_patch.rb index e6dfdba..673dd10 100644 --- a/lib/redmine_more_filters/patches/database_patch.rb +++ b/lib/redmine_more_filters/patches/database_patch.rb @@ -87,7 +87,8 @@ def query_db_timezone if postgresql? "SELECT current_setting('TIMEZONE');" elsif mysql? - "SELECT @@system_time_zone;" + #"SELECT @@system_time_zone;" + "SELECT 'UTC';" elsif sqlserver? "EXEC MASTER.dbo.xp_regread 'HKEY_LOCAL_MACHINE', 'SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation','TimeZoneKeyName'" else diff --git a/lib/redmine_more_filters/patches/query_patch.rb b/lib/redmine_more_filters/patches/query_patch.rb index 2cfff8d..6c67dc8 100644 --- a/lib/redmine_more_filters/patches/query_patch.rb +++ b/lib/redmine_more_filters/patches/query_patch.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 # # Redmine plugin to add necessary filters to queries # @@ -24,292 +23,319 @@ module Patches module QueryPatch def self.included(base) base.send(:include, InstanceMethods) - + base.class_eval do unloadable - + if Rails::VERSION::MAJOR < 5 alias_method_chain :sql_for_field, :more_filters alias_method_chain :sql_for_custom_field, :more_filters alias_method_chain :statement, :more_filters - + else # Rails 5+ alias_method :sql_for_field_without_more_filters, :sql_for_field alias_method :sql_for_field, :sql_for_field_with_more_filters - + alias_method :sql_for_custom_field_without_more_filters, :sql_for_custom_field alias_method :sql_for_custom_field, :sql_for_custom_field_with_more_filters - + alias_method :statement_without_more_filters, :statement alias_method :statement, :statement_with_more_filters end - - self.operators.merge!( - - "==" => :label_is_strict, - "!!" => :label_is_not_strict, - - "^=" => :label_begins_with, - "*^=" => :label_begins_with_any, - "!^=" => :label_not_begins_with, - "!*^=" => :label_not_begins_with_any, - - "$=" => :label_ends_with, - "*$=" => :label_ends_with_any, - "!$=" => :label_not_ends_with, - "!*$=" => :label_not_ends_with_any, - - "*=" => :label_any_of, - "!*=" => :label_none_of, - - "*~" => :label_contains_any, - "!*~" => :label_not_contains_any, - "[~]" => :label_contains_all, - "![~]" => :label_not_contains_all, - - "nd" => :label_tomorrow, - "nw" => :label_next_week, - "nm" => :label_next_month, - - "<<" => :label_past, - ">>" => :label_future, - - ">h-" => :label_less_than_hours_ago, - " :label_more_than_hours_ago, - "> :label_between_ago, - "> :label_between, - - "< :label_past, - "t>>" => :label_future, - - "match" => :label_match, - "!match" => :label_not_match + + operators.merge!( + '==' => :label_is_strict, + '!!' => :label_is_not_strict, + + '^=' => :label_begins_with, + '*^=' => :label_begins_with_any, + '!^=' => :label_not_begins_with, + '!*^=' => :label_not_begins_with_any, + + '$=' => :label_ends_with, + '*$=' => :label_ends_with_any, + '!$=' => :label_not_ends_with, + '!*$=' => :label_not_ends_with_any, + + '*=' => :label_any_of, + '!*=' => :label_none_of, + + '*~' => :label_contains_any, + '!*~' => :label_not_contains_any, + '[~]' => :label_contains_all, + '![~]' => :label_not_contains_all, + + 'nd' => :label_tomorrow, + 'nw' => :label_next_week, + 'nm' => :label_next_month, + + '<<' => :label_past, + '>>' => :label_future, + + '>h-' => :label_less_than_hours_ago, + ' :label_more_than_hours_ago, + '> :label_between_ago, + '> :label_between, + + '< :label_past, + 't>>' => :label_future, + + 'match' => :label_match, + '!match' => :label_not_match ) - - self.operators_by_filter_type[:string].insert(1, "*=", "!*=", "^=", "*^=", "!^=", "!*^=", "$=", "*$=", "!$=", "!*$=", "*~", "!*~", "[~]", "![~]") - self.operators_by_filter_type[:text].insert(1, "^=", "*^=", "!^=", "!*^=", "$=", "*$=", "!$=", "!*$=", "*~", "!*~", "[~]", "![~]", "match", "!match" ) - - self.operators_by_filter_type[:date].insert(14, "nm") - self.operators_by_filter_type[:date].insert(11, "nw") - self.operators_by_filter_type[:date].insert( 9, "nd") - self.operators_by_filter_type[:date].insert( 9, ">>") - self.operators_by_filter_type[:date].insert( 8, "<<") - - self.operators_by_filter_type[:date_past].insert( 8, "<<") - - self.operators_by_filter_type[:time_past] = [ ">h-", "=", "<=", "><" - add_filter_error(field, :invalid) if values_for(field).detect {|v| - v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?) - } - when ">t-", "t+", "h-", ">') + operators_by_filter_type[:date].insert(8, '<<') + + operators_by_filter_type[:date_past].insert(8, '<<') + + operators_by_filter_type[:time_past] = ['>h-', '=', '<=', '><' + add_filter_error(field, :invalid) if values_for(field).detect do |v| + v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?) + end + when '>t-', 't+', 'h-', '>', '<>', 'w', 'lw', 'nw', 'l2w', 'm', 'lm', + 'nm', 'y', '*o', '!o'].include? operator_for(field) end - add_filter_error(field, :blank) unless - # filter requires one or more values - (values_for(field) and !values_for(field).first.blank?) or - # filter doesn't require any value - ["o", "c", "!*", "*", "t", "ld", "nd", "<<", ">>", "<>", "w", "lw", "nw", "l2w", "m", "lm", "nm", "y", "*o", "!o"].include? operator_for(field) - end if filters - end #def - - # Returns Time from the given filter value - def parse_time(arg) - Time.parse(arg) rescue nil - end #def - - - end #base - end #self - + end # def + + # Returns Time from the given filter value + def parse_time(arg) + Time.parse(arg) + rescue StandardError + nil + end # def + end # base + end # self + module InstanceMethods - - def sql_for_field_with_more_filters(field, operator, value, db_table, db_field, is_custom_filter=false) - sql = case operator - when "!*" - s = "#{db_table}.#{db_field} IS NULL" - s << " OR RTRIM(#{db_table}.#{db_field}) = ''" if (is_custom_filter || [:text, :string].include?(type_for(field))) - s - when "*" - s = "#{db_table}.#{db_field} IS NOT NULL" - s << " AND RTRIM(#{db_table}.#{db_field}) <> ''" if (is_custom_filter || [:text, :string].include?(type_for(field))) - s - when "^=" - sql_begins_with("#{db_table}.#{db_field}", value.first) - when "*^=" - value.first.split(" ").select{|s| s.present?}.map{|s| sql_begins_with("#{db_table}.#{db_field}", s)}.join(" OR ") - when "!^=" - sql_begins_with("#{db_table}.#{db_field}", value.first, false) - when "!*^=" - value.first.split(" ").select{|s| s.present?}.map{|s| sql_begins_with("#{db_table}.#{db_field}", s, false)}.join(" AND ") - - when "*=" - sql_one_of("#{db_table}.#{db_field}", value.first.split(" ").select{|s| s.present?}, true) - - when "!*=" - sql_one_of("#{db_table}.#{db_field}", value.first.split(" ").select{|s| s.present?}, false) - - when "$=" - sql = sql_ends_with("#{db_table}.#{db_field}", value.first) - when "*$=" - value.first.split(" ").select{|s| s.present?}.map{|s| sql_ends_with("#{db_table}.#{db_field}", s)}.join(" OR ") - when "!$=" - sql_ends_with("#{db_table}.#{db_field}", value.first, false) - when "!*$=" - value.first.split(" ").select{|s| s.present?}.map{|s| sql_ends_with("#{db_table}.#{db_field}", s, false)}.join(" AND ") - - when "*~" - value.first.split(" ").select{|s| s.present?}.map{|s| sql_contains("#{db_table}.#{db_field}", s)}.join(" OR ") - when "!*~" - value.first.split(" ").select{|s| s.present?}.map{|s| sql_contains("#{db_table}.#{db_field}", s, false)}.join(" AND ") - when "[~]" - value.first.split(" ").select{|s| s.present?}.map{|s| sql_contains("#{db_table}.#{db_field}", s)}.join(" AND ") - when "![~]" - value.first.split(" ").select{|s| s.present?}.map{|s| sql_contains("#{db_table}.#{db_field}", s, false)}.join(" OR ") - - when "<>" - relative_time_clause(db_table, db_field, 0, nil, nil, nil) - - when ">h-" - relative_time_clause(db_table, db_field, value.first.to_i * (-1), "hour", nil, nil) - when ">" - relative_date_clause(db_table, db_field, 0, nil, is_custom_filter) - - when "nd" - # = tomorrow - relative_date_clause(db_table, db_field, 1, 1, is_custom_filter) - when "nw" - # = next week - first_day_of_week = l(:general_first_day_of_week).to_i - day_of_week = User.current.today.cwday - days_since = 7 - day_of_week - first_day_of_week - relative_date_clause(db_table, db_field, days_since, days_since + 7, is_custom_filter) - when "nm" - # = next month - date = User.current.today.next_month - date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter) - when "=" - sql_for_field_without_more_filters(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) - when "!match" - sql = sql_for_match_operators(field, operator, value, db_table, db_field, is_custom_filter) - else - sql_for_field_without_more_filters(field, operator, value, db_table, db_field, is_custom_filter) + def sql_for_field_with_more_filters(field, operator, value, db_table, db_field, is_custom_filter = false) + if field.include?('status_first_date_') && respond_to?(:sql_for_field_with_dit_kpi) + sql = sql_for_field_with_dit_kpi(field, operator, value, db_table, db_field, is_custom_filter) + return sql if sql end - return sql - end #def - - def sql_for_match_operators(field, operator, value, db_table, db_field, is_custom_filter=false) + + case operator + when '!*' + s = "#{db_table}.#{db_field} IS NULL" + s << " OR RTRIM(#{db_table}.#{db_field}) = ''" if is_custom_filter || %i[text + string].include?(type_for(field)) + s + when '*' + s = "#{db_table}.#{db_field} IS NOT NULL" + s << " AND RTRIM(#{db_table}.#{db_field}) <> ''" if is_custom_filter || %i[text + string].include?(type_for(field)) + s + when '^=' + sql_begins_with("#{db_table}.#{db_field}", value.first) + when '*^=' + value.first.split(' ').select do |s| + s.present? + end.map { |s| sql_begins_with("#{db_table}.#{db_field}", s) }.join(' OR ') + when '!^=' + sql_begins_with("#{db_table}.#{db_field}", value.first, false) + when '!*^=' + value.first.split(' ').select do |s| + s.present? + end.map { |s| sql_begins_with("#{db_table}.#{db_field}", s, false) }.join(' AND ') + + when '*=' + sql_one_of("#{db_table}.#{db_field}", value.first.split(' ').select { |s| s.present? }, true) + + when '!*=' + sql_one_of("#{db_table}.#{db_field}", value.first.split(' ').select { |s| s.present? }, false) + + when '$=' + sql = sql_ends_with("#{db_table}.#{db_field}", value.first) + when '*$=' + value.first.split(' ').select do |s| + s.present? + end.map { |s| sql_ends_with("#{db_table}.#{db_field}", s) }.join(' OR ') + when '!$=' + sql_ends_with("#{db_table}.#{db_field}", value.first, false) + when '!*$=' + value.first.split(' ').select do |s| + s.present? + end.map { |s| sql_ends_with("#{db_table}.#{db_field}", s, false) }.join(' AND ') + + when '*~' + value.first.split(' ').select do |s| + s.present? + end.map { |s| sql_contains("#{db_table}.#{db_field}", s) }.join(' OR ') + when '!*~' + value.first.split(' ').select do |s| + s.present? + end.map { |s| sql_contains("#{db_table}.#{db_field}", s, false) }.join(' AND ') + when '[~]' + value.first.split(' ').select do |s| + s.present? + end.map { |s| sql_contains("#{db_table}.#{db_field}", s) }.join(' AND ') + when '![~]' + value.first.split(' ').select do |s| + s.present? + end.map { |s| sql_contains("#{db_table}.#{db_field}", s, false) }.join(' OR ') + + when '<>' + relative_time_clause(db_table, db_field, 0, nil, nil, nil) + + when '>h-' + relative_time_clause(db_table, db_field, value.first.to_i * -1, 'hour', nil, nil) + when '>' + relative_date_clause(db_table, db_field, 0, nil, is_custom_filter) + + when 'nd' + # = tomorrow + relative_date_clause(db_table, db_field, 1, 1, is_custom_filter) + when 'nw' + # = next week + first_day_of_week = l(:general_first_day_of_week).to_i + day_of_week = User.current.today.cwday + days_since = 7 - day_of_week - first_day_of_week + relative_date_clause(db_table, db_field, days_since, days_since + 7, is_custom_filter) + when 'nm' + # = next month + date = User.current.today.next_month + date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter) + when '=' + sql_for_field_without_more_filters(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) + when '!match' + sql = sql_for_match_operators(field, operator, value, db_table, db_field, is_custom_filter) + else + sql_for_field_without_more_filters(field, operator, value, db_table, db_field, is_custom_filter) + end + end # def + + def sql_for_match_operators(_field, operator, value, db_table, db_field, _is_custom_filter = false) sql = '' - v = "(" + value.first.strip + ")" - + v = '(' + value.first.strip + ')' + match = true - op = "" - term = "" + 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 = "" + if (!in_bracket && '()+~!'.include?(c) && in_term) || (in_bracket && '}'.include?(c)) + sql += '(' + sql_contains("#{db_table}.#{db_field}", term, match) + ')' unless term.empty? + # reset + op = '' + term = '' in_term = false - - in_bracket = (c == "{") + + in_bracket = (c == '{') end - - if in_bracket && (!"{}".include? c) + + if in_bracket && (!'{}'.include? c) term += c in_term = true else - + case c - when "{" + when '{' in_bracket = true - when "}" + when '}' in_bracket = false - when "(" + when '(' sql += c - when ")" + when ')' sql += c - when "+" - sql += " AND " if sql.last != "(" - when "~" - sql += " OR " if sql.last != "(" - when "!" - sql += " NOT " + when '+' + sql += ' AND ' if sql.last != '(' + when '~' + sql += ' OR ' if sql.last != '(' + when '!' + sql += ' NOT ' else - if c != " " + if c != ' ' term += c in_term = true end end - + end end - - if operator.include? "!" - sql = " NOT " + sql - end - + + sql = ' NOT ' + sql if operator.include? '!' + Rails.logger.info "MATCH EXPRESSION: V=#{value.first}, SQL=#{sql}" - return sql + sql end - + def sql_for_custom_field_with_more_filters(field, operator, value, custom_field_id) db_table = CustomValue.table_name db_field = 'value' - #filter = @available_filters[field] + # filter = @available_filters[field] filter = available_filters[field] return nil unless filter - if filter[:field].format.target_class && filter[:field].format.target_class <= User - if value.delete('me') - value.push User.current.id.to_s - end + + if filter[:field].format.target_class && filter[:field].format.target_class <= User && value.delete('me') + value.push User.current.id.to_s end not_in = nil if operator == '!' @@ -317,22 +343,22 @@ def sql_for_custom_field_with_more_filters(field, operator, value, custom_field_ operator = '=' not_in = 'NOT' end - if operator == "!!" - operator = "!" - end - customized_key = "id" + operator = '!' if operator == '!!' + customized_key = 'id' customized_class = queried_class if field =~ /^(.+)\.cf_/ - assoc = $1 + assoc = ::Regexp.last_match(1) customized_key = "#{assoc}_id" - customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil + customized_class = begin + queried_class.reflect_on_association(assoc.to_sym).klass.base_class + rescue StandardError + nil + end raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class end where = sql_for_field_with_more_filters(field, operator, value, db_table, db_field, true) - if operator =~ /[<>]/ - where = "(#{where}) AND #{db_table}.#{db_field} <> ''" - end - "#{queried_table_name}.#{customized_key} #{not_in} IN (" + + where = "(#{where}) AND #{db_table}.#{db_field} <> ''" if operator =~ /[<>]/ + "#{queried_table_name}.#{customized_key} #{not_in} IN (" + "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" + " LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id}" + " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))" @@ -341,211 +367,209 @@ def sql_for_custom_field_with_more_filters(field, operator, value, custom_field_ def statement_with_more_filters # filters clauses filters_clauses = [] - + and_clauses = [] and_any_clauses = [] or_any_clauses = [] or_all_clauses = [] - and_any_op = "" - or_any_op = "" - or_all_op = "" + and_any_op = '' + or_any_op = '' + or_all_op = '' - #the AND filter start first + # 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? - operator = operator_for(field) - - # "me" value substitution - if %w(assigned_to_id author_id user_id watcher_id updated_by last_updated_by).include?(field) - if v.delete("me") + + if filters and valid? + 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? + + operator = operator_for(field) + + # "me" value substitution + if %w[assigned_to_id author_id user_id watcher_id updated_by + last_updated_by].include?(field) && v.delete('me') if User.current.logged? v.push(User.current.id.to_s) v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id' else - v.push("0") + v.push('0') end end - end - - if field == 'project_id' - if v.delete('mine') - v += User.current.memberships.map(&:project_id).map(&:to_s) - end - end - - if field =~ /^cf_(\d+)\.cf_(\d+)$/ - filters_clauses << sql_for_chained_custom_field(field, operator, v, $1, $2) - elsif field =~ /cf_(\d+)$/ - # custom field - if v.is_a?(Array) - case operator - when "==" - fc = [] - v.each do |sv| - fc << sql_for_custom_field_with_more_filters(field, "=", [sv], $1) - end - filters_clauses << fc.join(' AND ') - filters_clauses << (" NOT " + sql_for_custom_field_with_more_filters(field, "!!", v, $1)) - when "[~]" - fc = [] - v.each do |sv| - fc << sql_for_custom_field_with_more_filters(field, "=", [sv], $1) - end - filters_clauses << fc.join(' AND ') - when "![~]" - fc = [] - v.each do |sv| - fc << sql_for_custom_field_with_more_filters(field, "=", [sv], $1) - end - filters_clauses << (" NOT (" + fc.join(' AND ') + ")") - when "!!" - fc = [] - v.each do |sv| - fc << sql_for_custom_field_with_more_filters(field, "=", [sv], $1) + + v += User.current.memberships.map(&:project_id).map(&:to_s) if field == 'project_id' && v.delete('mine') + + if field =~ /^cf_(\d+)\.cf_(\d+)$/ + filters_clauses << sql_for_chained_custom_field(field, operator, v, ::Regexp.last_match(1), + ::Regexp.last_match(2)) + elsif field =~ /cf_(\d+)$/ + # custom field + if v.is_a?(Array) + case operator + when '==' + fc = [] + v.each do |sv| + fc << sql_for_custom_field_with_more_filters(field, '=', [sv], ::Regexp.last_match(1)) + end + filters_clauses << fc.join(' AND ') + filters_clauses << (' NOT ' + sql_for_custom_field_with_more_filters(field, '!!', v, + ::Regexp.last_match(1))) + when '[~]' + fc = [] + v.each do |sv| + fc << sql_for_custom_field_with_more_filters(field, '=', [sv], ::Regexp.last_match(1)) + end + filters_clauses << fc.join(' AND ') + when '![~]' + fc = [] + v.each do |sv| + fc << sql_for_custom_field_with_more_filters(field, '=', [sv], ::Regexp.last_match(1)) + end + filters_clauses << (' NOT (' + fc.join(' AND ') + ')') + when '!!' + fc = [] + v.each do |sv| + fc << sql_for_custom_field_with_more_filters(field, '=', [sv], ::Regexp.last_match(1)) + end + filters_clauses << (' NOT (' + fc.join(' AND ') + ' AND NOT ' + sql_for_custom_field_with_more_filters( + field, '!!', v, ::Regexp.last_match(1) + ) + ')') + else + filters_clauses << sql_for_custom_field_with_more_filters(field, operator, v, + ::Regexp.last_match(1)) end - filters_clauses << (" NOT (" + fc.join(' AND ') + " AND NOT " + sql_for_custom_field_with_more_filters(field, "!!", v, $1) + ")") else - filters_clauses << sql_for_custom_field_with_more_filters(field, operator, v, $1) + filters_clauses << sql_for_custom_field_with_more_filters(field, operator, v, ::Regexp.last_match(1)) end + elsif field =~ /^cf_(\d+)\.(.+)$/ + filters_clauses << sql_for_custom_field_attribute(field, operator, v, ::Regexp.last_match(1), + ::Regexp.last_match(2)) + elsif respond_to?(method = "sql_for_#{field.gsub('.', '_')}_field") + # specific statement + filters_clauses << send(method, field, operator, v) else - filters_clauses << sql_for_custom_field_with_more_filters(field, operator, v, $1) + # regular field + filters_clauses << ('(' + sql_for_field_with_more_filters(field, operator, v, queried_table_name, + field) + ')') end - elsif field =~ /^cf_(\d+)\.(.+)$/ - filters_clauses << sql_for_custom_field_attribute(field, operator, v, $1, $2) - elsif respond_to?(method = "sql_for_#{field.gsub('.','_')}_field") - # specific statement - filters_clauses << send(method, field, operator, v) - else - # regular field - filters_clauses << ('(' + sql_for_field_with_more_filters(field, operator, v, queried_table_name, field) + ')') end - end if filters and valid? - + end + if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn) # Excludes results for which the grouped custom field is not visible filters_clauses << c.custom_field.visibility_by_project_condition end - #now start build the full statement, project filter is allways AND + # 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 + 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 + 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 + 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 + or_all_statement = or_all_clauses.any? ? '(' + or_all_clauses.join(' AND ') + ')' : nil # then add the or_any or_any_clauses.reject!(&:blank?) - or_any_statement = or_any_clauses.any? ? "("+ or_any_clauses.join(" OR ") +")" : nil + or_any_statement = or_any_clauses.any? ? '(' + or_any_clauses.join(' OR ') + ')' : 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 - - #filters_clauses.any? ? filters_clauses.join(' AND ') : 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 + full_statement end - + # Returns a SQL LIKE statement with wildcards - def sql_begins_with(db_field, value, match=true) + def sql_begins_with(db_field, value, match = true) queried_class.send :sanitize_sql_for_conditions, - [Redmine::Database.like(db_field, '?', :match => match), "#{value}%"] - end #def - + [Redmine::Database.like(db_field, '?', match: match), "#{value}%"] + end # def + # Returns a SQL LIKE statement with wildcards - def sql_ends_with(db_field, value, match=true) + def sql_ends_with(db_field, value, match = true) queried_class.send :sanitize_sql_for_conditions, - [Redmine::Database.like(db_field, '?', :match => match), "%#{value}"] - end #def - - # Returns a SQL IN statement - def sql_one_of(db_field, values, match=true) + [Redmine::Database.like(db_field, '?', match: match), "%#{value}"] + end # def + + # Returns a SQL IN statement + def sql_one_of(db_field, values, match = true) queried_class.send :sanitize_sql_for_conditions, - "#{db_field} #{match ? '' : 'NOT'} IN (#{values.map{|v| "'#{ActiveRecord::Base.connection.quote_string(v)}'"}.join(', ')})" - end #def - + "#{db_field} #{match ? '' : 'NOT'} IN (#{values.map do |v| + "'#{ActiveRecord::Base.connection.quote_string(v)}'" + end.join(', ')})" + end # def + # Returns a SQL clause for a time field. - def relative_time_clause(table, field, from, from_epoch, to, to_epoch ) + def relative_time_clause(table, field, from, from_epoch, to, to_epoch) s = [] - from_epoch ||= "second" - to_epoch ||= "second" - if from - s << ("#{table}.#{field} > %s" % [Redmine::Database.relative_time( from, from_epoch )]) - end - if to - s << ("#{table}.#{field} <= %s" % [Redmine::Database.relative_time( to, to_epoch )]) - end + from_epoch ||= 'second' + to_epoch ||= 'second' + s << (format("#{table}.#{field} > %s", Redmine::Database.relative_time(from, from_epoch))) if from + s << (format("#{table}.#{field} <= %s", Redmine::Database.relative_time(to, to_epoch))) if to s.join(' AND ') - end #def - + end # def + # Returns a SQL clause for a time field in local time # interprets the string as User local time # User local time is converted to utc, which is database time # - def local_clock_time_clause(table, field, from, to, time_zone ) + def local_clock_time_clause(table, field, from, to, time_zone) s = [] - - if from - s << ("(#{Redmine::Database.local_clock_time_sql(table, field, time_zone)}) > '%s'" % [from]) - end - if to - s << ("(#{Redmine::Database.local_clock_time_sql(table, field, time_zone)}) <= '%s'" % [to]) - end + + s << (format("(#{Redmine::Database.local_clock_time_sql(table, field, time_zone)}) > '%s'", from)) if from + s << (format("(#{Redmine::Database.local_clock_time_sql(table, field, time_zone)}) <= '%s'", to)) if to s.join(' AND ') - end #def - + end # def + def get_timezone return User.current.time_zone_or_default_identifier if User.current - return ActiveSupport::TimeZone[RedmineApp::Application.config.time_zone] if RedmineApp::Application.config.time_zone - "Etc/UTC" - end #def - + if RedmineApp::Application.config.time_zone + return ActiveSupport::TimeZone[RedmineApp::Application.config.time_zone] + end + + 'Etc/UTC' + end # def end end end end unless Query.included_modules.include?(RedmineMoreFilters::Patches::QueryPatch) - Query.send(:include, RedmineMoreFilters::Patches::QueryPatch) + Query.include RedmineMoreFilters::Patches::QueryPatch end - - - From 4533403c5cf6a5ad9f1d285f8b7d19e3fde8801c Mon Sep 17 00:00:00 2001 From: Keks Keksov Date: Wed, 24 Jul 2024 19:53:53 +0300 Subject: [PATCH 5/6] sql_for_field chainig fix --- lib/redmine_more_filters/patches/database_patch.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/redmine_more_filters/patches/database_patch.rb b/lib/redmine_more_filters/patches/database_patch.rb index 673dd10..1d370f0 100644 --- a/lib/redmine_more_filters/patches/database_patch.rb +++ b/lib/redmine_more_filters/patches/database_patch.rb @@ -87,8 +87,8 @@ def query_db_timezone if postgresql? "SELECT current_setting('TIMEZONE');" elsif mysql? - #"SELECT @@system_time_zone;" - "SELECT 'UTC';" + "SELECT @@system_time_zone;" + #"SELECT 'UTC';" elsif sqlserver? "EXEC MASTER.dbo.xp_regread 'HKEY_LOCAL_MACHINE', 'SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation','TimeZoneKeyName'" else From 28f1aae07c078f263920c0562d220990c9d5bcb1 Mon Sep 17 00:00:00 2001 From: Keks Keksov Date: Fri, 26 Jul 2024 20:50:12 +0300 Subject: [PATCH 6/6] 3.1.6, 3.1.7 status_first_date, status_total_duration fix 6 --- .../patches/database_patch.rb | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/lib/redmine_more_filters/patches/database_patch.rb b/lib/redmine_more_filters/patches/database_patch.rb index 1d370f0..98708a9 100644 --- a/lib/redmine_more_filters/patches/database_patch.rb +++ b/lib/redmine_more_filters/patches/database_patch.rb @@ -25,22 +25,22 @@ module DatabasePatch def self.included(base) base.send(:include, InstanceMethods) base.send(:extend, ClassMethods) - + base.class_eval do unloadable - + end #base end #self - + module InstanceMethods end - + module ClassMethods # Returns true if the database is a SQLServer def sqlserver? (ActiveRecord::Base.connection.adapter_name =~ /SQLServer/i).present? end - + # Returns a SQL statement for relative time def relative_time( num, epoch ) if postgresql? @@ -51,10 +51,10 @@ def relative_time( num, epoch ) "(DATEADD(#{epoch}, #{num}, CURRENT_TIMESTAMP))" end end #def - + # Returns a SQL statement for local time def local_time_sql( table_name, field, time_zone ) - + if postgresql? "#{table_name}.#{field} at time zone '#{db_timezone.downcase}' at time zone '#{time_zone.downcase}'" elsif mysql? @@ -63,43 +63,43 @@ def local_time_sql( table_name, field, time_zone ) "Tzdb.ConvertZone(#{table_name}.#{field}, '#{db_timezone}', '#{time_zone}', 1, 1)" end end #def - + # Returns a SQL statement for hour of local clock time def local_clock_time_sql( table_name, field, time_zone ) "CAST(#{local_time_sql(table_name, field, time_zone)} AS TIME)" end #def - + # Returns a SQL statement for hour of local time def hour_of_local_clock_time_sql( table_name, field, time_zone ) - if postgresql? + if postgresql? "CAST(DATE_PART('hour', #{local_clock_time_sql( table_name, field, time_zone)} ) AS INTEGER)" elsif mysql? - "CAST(hour(#{local_clock_time_sql( table_name, field, time_zone)}) AS INTEGER)" + "CAST(hour(#{local_clock_time_sql( table_name, field, time_zone)}) AS UNSIGNED)" elsif sqlserver? "CAST(DATEPART('hour', #{local_clock_time_sql( table_name, field, time_zone)}) AS INTEGER)" else nil end end #def - + def query_db_timezone - sql = + sql = if postgresql? "SELECT current_setting('TIMEZONE');" elsif mysql? - "SELECT @@system_time_zone;" - #"SELECT 'UTC';" + #"SELECT @@system_time_zone;" + "SELECT 'UTC';" elsif sqlserver? "EXEC MASTER.dbo.xp_regread 'HKEY_LOCAL_MACHINE', 'SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation','TimeZoneKeyName'" else nil end - + if sql result = ActiveRecord::Base.connection.exec_query(sql).rows[0] end - - timezone = + + timezone = if result.present? if postgresql? result[0] @@ -113,13 +113,13 @@ def query_db_timezone else nil end - + end #def - + def db_timezone @db_timezone ||= query_db_timezone end #def - + end end end @@ -128,6 +128,3 @@ def db_timezone unless Redmine::Database.included_modules.include?(RedmineMoreFilters::Patches::DatabasePatch) Redmine::Database.send(:include, RedmineMoreFilters::Patches::DatabasePatch) end - - -