]> source.dussan.org Git - redmine.git/commitdiff
Filters on chained custom fields and custom field attributes (#21249).
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Sat, 14 Jan 2017 10:52:38 +0000 (10:52 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Sat, 14 Jan 2017 10:52:38 +0000 (10:52 +0000)
git-svn-id: http://svn.redmine.org/redmine/trunk@16191 e93f8b46-1217-0410-a6f0-8f06a7374b81

app/helpers/queries_helper.rb
app/models/query.rb
config/locales/en.yml
config/locales/fr.yml
test/unit/query_test.rb

index 66793e493948ecd3c956de2388b0a37713c31595..570ccb3687fa63ba5e14032de3de613d5a8532de 100644 (file)
@@ -28,6 +28,8 @@ module QueriesHelper
         group = :label_relations
       elsif field_options[:type] == :tree
         group = query.is_a?(IssueQuery) ? :label_relations : nil
+      elsif field =~ /^cf_\d+\./
+        group = (field_options[:through] || field_options[:field]).try(:name)
       elsif field =~ /^(.+)\./
         # association filters
         group = "field_#{$1}".to_sym
@@ -48,7 +50,7 @@ module QueriesHelper
     end
     s = options_for_select([[]] + ungrouped)
     if grouped.present?
-      localized_grouped = grouped.map {|k,v| [l(k), v]}
+      localized_grouped = grouped.map {|k,v| [k.is_a?(Symbol) ? l(k) : k.to_s, v]}
       s << grouped_options_for_select(localized_grouped)
     end
     s
index fea958631157b4570eb64ed2f7547f95f082144c..1fe405deba5dd39eb0eb5f7d7785673815c81ba5 100644 (file)
@@ -808,9 +808,13 @@ class Query < ActiveRecord::Base
         end
       end
 
-      if field =~ /cf_(\d+)$/
+      if field =~ /^cf_(\d+)\.cf_(\d+)$/
+        filters_clauses << sql_for_chained_custom_field(field, operator, v, $1, $2)
+      elsif field =~ /cf_(\d+)$/
         # custom field
         filters_clauses << sql_for_custom_field(field, operator, v, $1)
+      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)
@@ -951,6 +955,46 @@ class Query < ActiveRecord::Base
       " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
   end
 
+  def sql_for_chained_custom_field(field, operator, value, custom_field_id, chained_custom_field_id)
+    not_in = nil
+    if operator == '!'
+      # Makes ! operator work for custom fields with multiple values
+      operator = '='
+      not_in = 'NOT'
+    end
+
+    filter = available_filters[field]
+    target_class = filter[:through].format.target_class
+
+    "#{queried_table_name}.id #{not_in} IN (" +
+      "SELECT customized_id FROM #{CustomValue.table_name}" +
+      " WHERE customized_type='#{queried_class}' AND custom_field_id=#{custom_field_id}" +
+      "  AND value <> '' AND CAST(value AS integer) IN (" +
+      "  SELECT customized_id FROM #{CustomValue.table_name}" +
+      "  WHERE customized_type='#{target_class}' AND custom_field_id=#{chained_custom_field_id}" +
+      "  AND #{sql_for_field(field, operator, value, CustomValue.table_name, 'value')}))"
+    
+  end
+
+  def sql_for_custom_field_attribute(field, operator, value, custom_field_id, attribute)
+    attribute = 'effective_date' if attribute == 'due_date'
+    not_in = nil
+    if operator == '!'
+      # Makes ! operator work for custom fields with multiple values
+      operator = '='
+      not_in = 'NOT'
+    end
+
+    filter = available_filters[field]
+    target_table_name = filter[:field].format.target_class.table_name
+
+    "#{queried_table_name}.id #{not_in} IN (" +
+      "SELECT customized_id FROM #{CustomValue.table_name}" +
+      " WHERE customized_type='#{queried_class}' AND custom_field_id=#{custom_field_id}" +
+      "  AND value <> '' AND CAST(value AS integer) IN (" +
+      "  SELECT id FROM #{target_table_name} WHERE #{sql_for_field(field, operator, value, filter[:field].format.target_class.table_name, attribute)}))"
+  end
+
   # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
   def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
     sql = ''
@@ -1124,10 +1168,47 @@ class Query < ActiveRecord::Base
     })
   end
 
+  # Adds filters for custom fields associated to the custom field target class
+  # Eg. having a version custom field "Milestone" for issues and a date custom field "Release date"
+  # for versions, it will add an issue filter on Milestone'e Release date.
+  def add_chained_custom_field_filters(field)
+    klass = field.format.target_class
+    if klass
+      CustomField.where(:is_filter => true, :type => "#{klass.name}CustomField").each do |chained|
+        options = chained.query_filter_options(self)
+
+        filter_id = "cf_#{field.id}.cf_#{chained.id}"
+        filter_name = chained.name
+
+        add_available_filter filter_id, options.merge({
+          :name => l(:label_attribute_of_object, :name => chained.name, :object_name => field.name),
+          :field => chained,
+          :through => field
+        })
+      end
+    end
+  end
+
   # Adds filters for the given custom fields scope
   def add_custom_fields_filters(scope, assoc=nil)
     scope.visible.where(:is_filter => true).sorted.each do |field|
       add_custom_field_filter(field, assoc)
+      if assoc.nil?
+        add_chained_custom_field_filters(field)
+
+        if field.format.target_class && field.format.target_class == Version
+          add_available_filter "cf_#{field.id}.due_date",
+            :type => :date,
+            :field => field,
+            :name => l(:label_attribute_of_object, :name => l(:field_effective_date), :object_name => field.name)
+
+          add_available_filter "cf_#{field.id}.status",
+            :type => :list,
+            :field => field,
+            :name => l(:label_attribute_of_object, :name => l(:field_status), :object_name => field.name),
+            :values => Version::VERSION_STATUSES.map{|s| [l("version_status_#{s}"), s] }
+        end
+      end
     end
   end
 
index d6201b9c7d67a2a01c4c095e111c9de89b3388f1..b643ae5771c19ae152221db9eadb78a193aa9302 100644 (file)
@@ -948,6 +948,7 @@ en:
   label_attribute_of_assigned_to: "Assignee's %{name}"
   label_attribute_of_user: "User's %{name}"
   label_attribute_of_fixed_version: "Target version's %{name}"
+  label_attribute_of_object: "%{object_name}'s %{name}"
   label_cross_project_descendants: With subprojects
   label_cross_project_tree: With project tree
   label_cross_project_hierarchy: With project hierarchy
index 555dd6e01cbb30c52d0621ce276ff97260c153a6..610dc156bec4e4ad46e9a7519efa1eb8045275a0 100644 (file)
@@ -959,6 +959,7 @@ fr:
   label_attribute_of_assigned_to: "%{name} de l'assigné"
   label_attribute_of_user: "%{name} de l'utilisateur"
   label_attribute_of_fixed_version: "%{name} de la version cible"
+  label_attribute_of_object: "%{name} de \"%{object_name}\""
   label_cross_project_descendants: Avec les sous-projets
   label_cross_project_tree: Avec tout l'arbre
   label_cross_project_hierarchy: Avec toute la hiérarchie
index 8c1c3b43cb55d75f74ec0b5ac39178f51f9dc6ff..f559756f8de2ca5ac103ca420c2cc778fe8723d5 100644 (file)
@@ -877,6 +877,49 @@ class QueryTest < ActiveSupport::TestCase
     assert_equal [1, 3, 7, 8], find_issues_with_query(query).map(&:id).uniq.sort
   end
 
+  def test_filter_on_version_custom_field
+    field = IssueCustomField.generate!(:field_format => 'version', :is_filter => true)
+    issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => '2'})
+
+    query = IssueQuery.new(:name => '_')
+    filter_name = "cf_#{field.id}"
+    assert_include filter_name, query.available_filters.keys
+
+    query.filters = {filter_name => {:operator => '=', :values => ['2']}}
+    issues = find_issues_with_query(query)
+    assert_equal [issue.id], issues.map(&:id).sort
+  end
+
+  def test_filter_on_attribute_of_version_custom_field
+    field = IssueCustomField.generate!(:field_format => 'version', :is_filter => true)
+    version = Version.generate!(:effective_date => '2017-01-14')
+    issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => version.id.to_s})
+
+    query = IssueQuery.new(:name => '_')
+    filter_name = "cf_#{field.id}.due_date"
+    assert_include filter_name, query.available_filters.keys
+
+    query.filters = {filter_name => {:operator => '=', :values => ['2017-01-14']}}
+    issues = find_issues_with_query(query)
+    assert_equal [issue.id], issues.map(&:id).sort
+  end
+
+  def test_filter_on_custom_field_of_version_custom_field
+    field = IssueCustomField.generate!(:field_format => 'version', :is_filter => true)
+    attr = VersionCustomField.generate!(:field_format => 'string', :is_filter => true)
+
+    version = Version.generate!(:custom_field_values => {attr.id.to_s => 'ABC'})
+    issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => version.id.to_s})
+
+    query = IssueQuery.new(:name => '_')
+    filter_name = "cf_#{field.id}.cf_#{attr.id}"
+    assert_include filter_name, query.available_filters.keys
+
+    query.filters = {filter_name => {:operator => '=', :values => ['ABC']}}
+    issues = find_issues_with_query(query)
+    assert_equal [issue.id], issues.map(&:id).sort
+  end
+
   def test_filter_on_relations_with_a_specific_issue
     IssueRelation.delete_all
     IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))