]> source.dussan.org Git - redmine.git/commitdiff
New issues filter operators "has been", "has never been", and "changed from" (#38527).
authorGo MAEDA <maeda@farend.jp>
Thu, 11 May 2023 02:08:41 +0000 (02:08 +0000)
committerGo MAEDA <maeda@farend.jp>
Thu, 11 May 2023 02:08:41 +0000 (02:08 +0000)
Patch by Go MAEDA.

git-svn-id: https://svn.redmine.org/redmine/trunk@22240 e93f8b46-1217-0410-a6f0-8f06a7374b81

app/models/issue_query.rb
app/models/query.rb
config/locales/en.yml
public/javascripts/application.js
test/unit/query_test.rb

index e573f8782b40db722a82d8bc7814b51eac536d6a..a0420c9941e3b60d1e399c1d6a56c9ca9dec59f6 100644 (file)
@@ -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
index 00d99c24f6cf3144b3b5273604879478a5a04c10..f4bbfb5cdd2fa749e0289b144584f513fdbc79c5 100644 (file)
@@ -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+", "t+", "nd", "t", "ld", "nw", "w", "lw", "l2w", "nm", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
     :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
@@ -1438,6 +1443,29 @@ class Query < ActiveRecord::Base
       sql = sql_contains("#{db_table}.#{db_field}", value.first, :starts_with => 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
index a09a499f29bd69a17421f8b5e5695b9b903a5fc9..f9064c866e8859c8ac845cd0981dda8d51ba9038 100644 (file)
@@ -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
index 6d01dc2b9502109f172fe4054ae75589d4497977..a183a2777b94c993ce18434db2c0fb349e09dccf 100644 (file)
@@ -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(
index 6087616b0de5c911e7d730737ae07f84cff4b980..8c5b5e1c815754a4af14e6b796ec55cd4699c8cf 100644 (file)
@@ -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)