diff options
author | Jean-Philippe Lang <jp_lang@yahoo.fr> | 2017-01-14 10:52:38 +0000 |
---|---|---|
committer | Jean-Philippe Lang <jp_lang@yahoo.fr> | 2017-01-14 10:52:38 +0000 |
commit | f1678e4f778c111ec97528eb57672e2d8d01e37c (patch) | |
tree | 51647ec750effa364e90f8879b93b8e50dead183 | |
parent | cb15f0df380031ec44c53c71755bd9926c17aa32 (diff) | |
download | redmine-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.rb | 4 | ||||
-rw-r--r-- | app/models/query.rb | 83 | ||||
-rw-r--r-- | config/locales/en.yml | 1 | ||||
-rw-r--r-- | config/locales/fr.yml | 1 | ||||
-rw-r--r-- | test/unit/query_test.rb | 43 |
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)) |