Commit 51252912 authored by jplang's avatar jplang

Filters on chained custom fields and custom field attributes (#21249).

git-svn-id: https://svn.redmine.org/redmine/trunk@16191 e93f8b46-1217-0410-a6f0-8f06a7374b81
parent 0579f0e4
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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))
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment