summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJean-Philippe Lang <jp_lang@yahoo.fr>2017-01-14 10:52:38 +0000
committerJean-Philippe Lang <jp_lang@yahoo.fr>2017-01-14 10:52:38 +0000
commitf1678e4f778c111ec97528eb57672e2d8d01e37c (patch)
tree51647ec750effa364e90f8879b93b8e50dead183
parentcb15f0df380031ec44c53c71755bd9926c17aa32 (diff)
downloadredmine-f1678e4f778c111ec97528eb57672e2d8d01e37c.tar.gz
redmine-f1678e4f778c111ec97528eb57672e2d8d01e37c.zip
Filters on chained custom fields and custom field attributes (#21249).
git-svn-id: http://svn.redmine.org/redmine/trunk@16191 e93f8b46-1217-0410-a6f0-8f06a7374b81
-rw-r--r--app/helpers/queries_helper.rb4
-rw-r--r--app/models/query.rb83
-rw-r--r--config/locales/en.yml1
-rw-r--r--config/locales/fr.yml1
-rw-r--r--test/unit/query_test.rb43
5 files changed, 130 insertions, 2 deletions
diff --git a/app/helpers/queries_helper.rb b/app/helpers/queries_helper.rb
index 66793e493..570ccb368 100644
--- a/app/helpers/queries_helper.rb
+++ b/app/helpers/queries_helper.rb
@@ -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
diff --git a/app/models/query.rb b/app/models/query.rb
index fea958631..1fe405deb 100644
--- a/app/models/query.rb
+++ b/app/models/query.rb
@@ -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
diff --git a/config/locales/en.yml b/config/locales/en.yml
index d6201b9c7..b643ae577 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -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
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 555dd6e01..610dc156b 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -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
diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb
index 8c1c3b43c..f559756f8 100644
--- a/test/unit/query_test.rb
+++ b/test/unit/query_test.rb
@@ -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))