From 2caf5eaf973f82d3a9177f63378a642aec8b7d3b Mon Sep 17 00:00:00 2001 From: Go MAEDA Date: Thu, 11 May 2023 02:08:41 +0000 Subject: [PATCH] New issues filter operators "has been", "has never been", and "changed from" (#38527). Patch by Go MAEDA. git-svn-id: https://svn.redmine.org/redmine/trunk@22240 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/issue_query.rb | 10 ++--- app/models/query.rb | 30 +++++++++++++- config/locales/en.yml | 3 ++ public/javascripts/application.js | 2 + test/unit/query_test.rb | 67 +++++++++++++++++++++++++++++++ 5 files changed, 106 insertions(+), 6 deletions(-) diff --git a/app/models/issue_query.rb b/app/models/issue_query.rb index e573f8782..a0420c994 100644 --- a/app/models/issue_query.rb +++ b/app/models/issue_query.rb @@ -155,11 +155,11 @@ class IssueQuery < Query ) if project.nil? add_available_filter( "tracker_id", - :type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s]} + :type => :list_with_history, :values => trackers.collect{|s| [s.name, s.id.to_s]} ) add_available_filter( "priority_id", - :type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s]} + :type => :list_with_history, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s]} ) add_available_filter( "author_id", @@ -167,7 +167,7 @@ class IssueQuery < Query ) add_available_filter( "assigned_to_id", - :type => :list_optional, :values => lambda {assigned_to_values} + :type => :list_optional_with_history, :values => lambda {assigned_to_values} ) add_available_filter( "member_of_group", @@ -179,7 +179,7 @@ class IssueQuery < Query ) add_available_filter( "fixed_version_id", - :type => :list_optional, :values => lambda {fixed_version_values} + :type => :list_optional_with_history, :values => lambda {fixed_version_values} ) add_available_filter( "fixed_version.due_date", @@ -194,7 +194,7 @@ class IssueQuery < Query ) add_available_filter( "category_id", - :type => :list_optional, + :type => :list_optional_with_history, :values => lambda {project.issue_categories.collect{|s| [s.name, s.id.to_s]}} ) if project add_available_filter "subject", :type => :text diff --git a/app/models/query.rb b/app/models/query.rb index 00d99c24f..f4bbfb5cd 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -314,13 +314,18 @@ class Query < ActiveRecord::Base "!p" => :label_no_issues_in_project, "*o" => :label_any_open_issues, "!o" => :label_no_open_issues, + "ev" => :label_has_been, # "ev" stands for "ever" + "!ev" => :label_has_never_been, + "cf" => :label_changed_from } class_attribute :operators_by_filter_type self.operators_by_filter_type = { :list => [ "=", "!" ], - :list_status => [ "o", "=", "!", "c", "*" ], + :list_with_history => [ "=", "!", "ev", "!ev", "cf" ], + :list_status => [ "o", "=", "!", "ev", "!ev", "cf", "c", "*" ], :list_optional => [ "=", "!", "!*", "*" ], + :list_optional_with_history => [ "=", "!", "ev", "!ev", "cf", "!*", "*" ], :list_subprojects => [ "*", "!*", "=", "!" ], :date => [ "=", ">=", "<=", "><", "t+", ">t-", " [ "=", ">=", "<=", "><", ">t-", " true) when "$" sql = sql_contains("#{db_table}.#{db_field}", value.first, :ends_with => true) + when "ev", "!ev", "cf" + # has been, has never been, changed from + if queried_class == Issue && value.present? + neg = (operator.start_with?('!') ? 'NOT' : '') + subquery = + "SELECT 1 FROM #{Journal.table_name}" + + " INNER JOIN #{JournalDetail.table_name} ON #{Journal.table_name}.id = #{JournalDetail.table_name}.journal_id" + + " WHERE (#{Journal.visible_notes_condition(User.current, :skip_pre_condition => true)}" + + " AND #{Journal.table_name}.journalized_type = 'Issue'" + + " AND #{Journal.table_name}.journalized_id = #{db_table}.id" + + " AND #{JournalDetail.table_name}.property = 'attr'" + + " AND #{JournalDetail.table_name}.prop_key = '#{db_field}'" + + " AND " + + queried_class.send(:sanitize_sql_for_conditions, ["#{JournalDetail.table_name}.old_value IN (?)", value]) + + ")" + if %w[ev !ev].include?(operator) + subquery << + " OR " + queried_class.send(:sanitize_sql_for_conditions, ["#{db_table}.#{db_field} IN (?)", value]) + end + sql = "#{neg} EXISTS (#{subquery})" + else + sql = '1=0' + end else raise QueryError, "Unknown query operator #{operator}" end diff --git a/config/locales/en.yml b/config/locales/en.yml index a09a499f2..f9064c866 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -818,6 +818,9 @@ en: label_no_issues_in_project: no issues in project label_any_open_issues: any open issues label_no_open_issues: no open issues + label_has_been: has been + label_has_never_been: has never been + label_changed_from: changed from label_day_plural: days label_repository: Repository label_repository_new: New repository diff --git a/public/javascripts/application.js b/public/javascripts/application.js index 6d01dc2b9..a183a2777 100644 --- a/public/javascripts/application.js +++ b/public/javascripts/application.js @@ -180,7 +180,9 @@ function buildFilterRow(field, operator, values) { switch (filterOptions['type']) { case "list": + case "list_with_history": case "list_optional": + case "list_optional_with_history": case "list_status": case "list_subprojects": tr.find('td.values').append( diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb index 6087616b0..8c5b5e1c8 100644 --- a/test/unit/query_test.rb +++ b/test/unit/query_test.rb @@ -797,6 +797,73 @@ class QueryTest < ActiveSupport::TestCase assert_equal [2, 14], result.map(&:id).sort end + def test_operator_changed_from + User.current = User.find(1) + issue1 = Issue.find(2) + issue1.init_journal(User.current) + issue1.update(status_id: 1) # Assigned (2) -> New + issue2 = Issue.find(8) + issue2.init_journal(User.current) + issue2.update(status_id: 2) # Closed (5) -> Assigned + + query = IssueQuery.new( + :name => '_', + :filters => { + 'status_id' => { + :operator => 'cf', + :values => [2, 5] # Assigned, Closed + } + } + ) + result = find_issues_with_query(query) + assert_equal( + [[2, 'New'], [8, 'Assigned']], + result.sort_by(&:id).map {|issue| [issue.id, issue.status.name]} + ) + end + + def test_operator_has_been + User.current = User.find(1) + issue = Issue.find(8) + issue.init_journal(User.current) + issue.update(status_id: 2) # Closed (5) -> Assigned + + query = IssueQuery.new( + :name => '_', + :filters => { + 'status_id' => { + :operator => 'ev', + :values => [5] # Closed + } + } + ) + result = find_issues_with_query(query) + assert_equal( + [[8, 'Assigned'], [11, 'Closed'], [12, 'Closed']], + result.sort_by(&:id).map {|issue| [issue.id, issue.status.name]} + ) + end + + def test_operator_has_never_been + User.current = User.find(1) + issue = Issue.find(8) + issue.init_journal(User.current) + issue.update(status_id: 2) # Closed (5) -> Assigned + + query = IssueQuery.new( + :name => '_', + :filters => { + 'status_id' => { + :operator => '!ev', + :values => [5] # Closed + } + } + ) + result = find_issues_with_query(query) + expected = Issue.all.order(:id).ids - [8, 11, 12] + assert_equal expected, result.map(&:id).sort + end + def test_range_for_this_week_with_week_starting_on_monday I18n.locale = :fr assert_equal '1', I18n.t(:general_first_day_of_week) -- 2.39.5