##// END OF EJS Templates
Filters on chained custom fields and custom field attributes (#21249)....
Jean-Philippe Lang -
r15809:f1678e4f778c
parent child
Show More
@@ -28,6 +28,8 module QueriesHelper
28 group = :label_relations
28 group = :label_relations
29 elsif field_options[:type] == :tree
29 elsif field_options[:type] == :tree
30 group = query.is_a?(IssueQuery) ? :label_relations : nil
30 group = query.is_a?(IssueQuery) ? :label_relations : nil
31 elsif field =~ /^cf_\d+\./
32 group = (field_options[:through] || field_options[:field]).try(:name)
31 elsif field =~ /^(.+)\./
33 elsif field =~ /^(.+)\./
32 # association filters
34 # association filters
33 group = "field_#{$1}".to_sym
35 group = "field_#{$1}".to_sym
@@ -48,7 +50,7 module QueriesHelper
48 end
50 end
49 s = options_for_select([[]] + ungrouped)
51 s = options_for_select([[]] + ungrouped)
50 if grouped.present?
52 if grouped.present?
51 localized_grouped = grouped.map {|k,v| [l(k), v]}
53 localized_grouped = grouped.map {|k,v| [k.is_a?(Symbol) ? l(k) : k.to_s, v]}
52 s << grouped_options_for_select(localized_grouped)
54 s << grouped_options_for_select(localized_grouped)
53 end
55 end
54 s
56 s
@@ -808,9 +808,13 class Query < ActiveRecord::Base
808 end
808 end
809 end
809 end
810
810
811 if field =~ /cf_(\d+)$/
811 if field =~ /^cf_(\d+)\.cf_(\d+)$/
812 filters_clauses << sql_for_chained_custom_field(field, operator, v, $1, $2)
813 elsif field =~ /cf_(\d+)$/
812 # custom field
814 # custom field
813 filters_clauses << sql_for_custom_field(field, operator, v, $1)
815 filters_clauses << sql_for_custom_field(field, operator, v, $1)
816 elsif field =~ /^cf_(\d+)\.(.+)$/
817 filters_clauses << sql_for_custom_field_attribute(field, operator, v, $1, $2)
814 elsif respond_to?(method = "sql_for_#{field.gsub('.','_')}_field")
818 elsif respond_to?(method = "sql_for_#{field.gsub('.','_')}_field")
815 # specific statement
819 # specific statement
816 filters_clauses << send(method, field, operator, v)
820 filters_clauses << send(method, field, operator, v)
@@ -951,6 +955,46 class Query < ActiveRecord::Base
951 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
955 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
952 end
956 end
953
957
958 def sql_for_chained_custom_field(field, operator, value, custom_field_id, chained_custom_field_id)
959 not_in = nil
960 if operator == '!'
961 # Makes ! operator work for custom fields with multiple values
962 operator = '='
963 not_in = 'NOT'
964 end
965
966 filter = available_filters[field]
967 target_class = filter[:through].format.target_class
968
969 "#{queried_table_name}.id #{not_in} IN (" +
970 "SELECT customized_id FROM #{CustomValue.table_name}" +
971 " WHERE customized_type='#{queried_class}' AND custom_field_id=#{custom_field_id}" +
972 " AND value <> '' AND CAST(value AS integer) IN (" +
973 " SELECT customized_id FROM #{CustomValue.table_name}" +
974 " WHERE customized_type='#{target_class}' AND custom_field_id=#{chained_custom_field_id}" +
975 " AND #{sql_for_field(field, operator, value, CustomValue.table_name, 'value')}))"
976
977 end
978
979 def sql_for_custom_field_attribute(field, operator, value, custom_field_id, attribute)
980 attribute = 'effective_date' if attribute == 'due_date'
981 not_in = nil
982 if operator == '!'
983 # Makes ! operator work for custom fields with multiple values
984 operator = '='
985 not_in = 'NOT'
986 end
987
988 filter = available_filters[field]
989 target_table_name = filter[:field].format.target_class.table_name
990
991 "#{queried_table_name}.id #{not_in} IN (" +
992 "SELECT customized_id FROM #{CustomValue.table_name}" +
993 " WHERE customized_type='#{queried_class}' AND custom_field_id=#{custom_field_id}" +
994 " AND value <> '' AND CAST(value AS integer) IN (" +
995 " SELECT id FROM #{target_table_name} WHERE #{sql_for_field(field, operator, value, filter[:field].format.target_class.table_name, attribute)}))"
996 end
997
954 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
998 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
955 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
999 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
956 sql = ''
1000 sql = ''
@@ -1124,10 +1168,47 class Query < ActiveRecord::Base
1124 })
1168 })
1125 end
1169 end
1126
1170
1171 # Adds filters for custom fields associated to the custom field target class
1172 # Eg. having a version custom field "Milestone" for issues and a date custom field "Release date"
1173 # for versions, it will add an issue filter on Milestone'e Release date.
1174 def add_chained_custom_field_filters(field)
1175 klass = field.format.target_class
1176 if klass
1177 CustomField.where(:is_filter => true, :type => "#{klass.name}CustomField").each do |chained|
1178 options = chained.query_filter_options(self)
1179
1180 filter_id = "cf_#{field.id}.cf_#{chained.id}"
1181 filter_name = chained.name
1182
1183 add_available_filter filter_id, options.merge({
1184 :name => l(:label_attribute_of_object, :name => chained.name, :object_name => field.name),
1185 :field => chained,
1186 :through => field
1187 })
1188 end
1189 end
1190 end
1191
1127 # Adds filters for the given custom fields scope
1192 # Adds filters for the given custom fields scope
1128 def add_custom_fields_filters(scope, assoc=nil)
1193 def add_custom_fields_filters(scope, assoc=nil)
1129 scope.visible.where(:is_filter => true).sorted.each do |field|
1194 scope.visible.where(:is_filter => true).sorted.each do |field|
1130 add_custom_field_filter(field, assoc)
1195 add_custom_field_filter(field, assoc)
1196 if assoc.nil?
1197 add_chained_custom_field_filters(field)
1198
1199 if field.format.target_class && field.format.target_class == Version
1200 add_available_filter "cf_#{field.id}.due_date",
1201 :type => :date,
1202 :field => field,
1203 :name => l(:label_attribute_of_object, :name => l(:field_effective_date), :object_name => field.name)
1204
1205 add_available_filter "cf_#{field.id}.status",
1206 :type => :list,
1207 :field => field,
1208 :name => l(:label_attribute_of_object, :name => l(:field_status), :object_name => field.name),
1209 :values => Version::VERSION_STATUSES.map{|s| [l("version_status_#{s}"), s] }
1210 end
1211 end
1131 end
1212 end
1132 end
1213 end
1133
1214
@@ -948,6 +948,7 en:
948 label_attribute_of_assigned_to: "Assignee's %{name}"
948 label_attribute_of_assigned_to: "Assignee's %{name}"
949 label_attribute_of_user: "User's %{name}"
949 label_attribute_of_user: "User's %{name}"
950 label_attribute_of_fixed_version: "Target version's %{name}"
950 label_attribute_of_fixed_version: "Target version's %{name}"
951 label_attribute_of_object: "%{object_name}'s %{name}"
951 label_cross_project_descendants: With subprojects
952 label_cross_project_descendants: With subprojects
952 label_cross_project_tree: With project tree
953 label_cross_project_tree: With project tree
953 label_cross_project_hierarchy: With project hierarchy
954 label_cross_project_hierarchy: With project hierarchy
@@ -959,6 +959,7 fr:
959 label_attribute_of_assigned_to: "%{name} de l'assignΓ©"
959 label_attribute_of_assigned_to: "%{name} de l'assignΓ©"
960 label_attribute_of_user: "%{name} de l'utilisateur"
960 label_attribute_of_user: "%{name} de l'utilisateur"
961 label_attribute_of_fixed_version: "%{name} de la version cible"
961 label_attribute_of_fixed_version: "%{name} de la version cible"
962 label_attribute_of_object: "%{name} de \"%{object_name}\""
962 label_cross_project_descendants: Avec les sous-projets
963 label_cross_project_descendants: Avec les sous-projets
963 label_cross_project_tree: Avec tout l'arbre
964 label_cross_project_tree: Avec tout l'arbre
964 label_cross_project_hierarchy: Avec toute la hiΓ©rarchie
965 label_cross_project_hierarchy: Avec toute la hiΓ©rarchie
@@ -877,6 +877,49 class QueryTest < ActiveSupport::TestCase
877 assert_equal [1, 3, 7, 8], find_issues_with_query(query).map(&:id).uniq.sort
877 assert_equal [1, 3, 7, 8], find_issues_with_query(query).map(&:id).uniq.sort
878 end
878 end
879
879
880 def test_filter_on_version_custom_field
881 field = IssueCustomField.generate!(:field_format => 'version', :is_filter => true)
882 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => '2'})
883
884 query = IssueQuery.new(:name => '_')
885 filter_name = "cf_#{field.id}"
886 assert_include filter_name, query.available_filters.keys
887
888 query.filters = {filter_name => {:operator => '=', :values => ['2']}}
889 issues = find_issues_with_query(query)
890 assert_equal [issue.id], issues.map(&:id).sort
891 end
892
893 def test_filter_on_attribute_of_version_custom_field
894 field = IssueCustomField.generate!(:field_format => 'version', :is_filter => true)
895 version = Version.generate!(:effective_date => '2017-01-14')
896 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => version.id.to_s})
897
898 query = IssueQuery.new(:name => '_')
899 filter_name = "cf_#{field.id}.due_date"
900 assert_include filter_name, query.available_filters.keys
901
902 query.filters = {filter_name => {:operator => '=', :values => ['2017-01-14']}}
903 issues = find_issues_with_query(query)
904 assert_equal [issue.id], issues.map(&:id).sort
905 end
906
907 def test_filter_on_custom_field_of_version_custom_field
908 field = IssueCustomField.generate!(:field_format => 'version', :is_filter => true)
909 attr = VersionCustomField.generate!(:field_format => 'string', :is_filter => true)
910
911 version = Version.generate!(:custom_field_values => {attr.id.to_s => 'ABC'})
912 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => version.id.to_s})
913
914 query = IssueQuery.new(:name => '_')
915 filter_name = "cf_#{field.id}.cf_#{attr.id}"
916 assert_include filter_name, query.available_filters.keys
917
918 query.filters = {filter_name => {:operator => '=', :values => ['ABC']}}
919 issues = find_issues_with_query(query)
920 assert_equal [issue.id], issues.map(&:id).sort
921 end
922
880 def test_filter_on_relations_with_a_specific_issue
923 def test_filter_on_relations_with_a_specific_issue
881 IssueRelation.delete_all
924 IssueRelation.delete_all
882 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
925 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
General Comments 0
You need to be logged in to leave comments. Login now