mirror of
https://github.com/redmine/redmine.git
synced 2025-11-11 15:56:03 +01:00
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
This commit is contained in:
@@ -28,6 +28,8 @@ module QueriesHelper
|
|||||||
group = :label_relations
|
group = :label_relations
|
||||||
elsif field_options[:type] == :tree
|
elsif field_options[:type] == :tree
|
||||||
group = query.is_a?(IssueQuery) ? :label_relations : nil
|
group = query.is_a?(IssueQuery) ? :label_relations : nil
|
||||||
|
elsif field =~ /^cf_\d+\./
|
||||||
|
group = (field_options[:through] || field_options[:field]).try(:name)
|
||||||
elsif field =~ /^(.+)\./
|
elsif field =~ /^(.+)\./
|
||||||
# association filters
|
# association filters
|
||||||
group = "field_#{$1}".to_sym
|
group = "field_#{$1}".to_sym
|
||||||
@@ -48,7 +50,7 @@ module QueriesHelper
|
|||||||
end
|
end
|
||||||
s = options_for_select([[]] + ungrouped)
|
s = options_for_select([[]] + ungrouped)
|
||||||
if grouped.present?
|
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)
|
s << grouped_options_for_select(localized_grouped)
|
||||||
end
|
end
|
||||||
s
|
s
|
||||||
|
|||||||
@@ -808,9 +808,13 @@ class Query < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
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
|
# custom field
|
||||||
filters_clauses << sql_for_custom_field(field, operator, v, $1)
|
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")
|
elsif respond_to?(method = "sql_for_#{field.gsub('.','_')}_field")
|
||||||
# specific statement
|
# specific statement
|
||||||
filters_clauses << send(method, field, operator, v)
|
filters_clauses << send(method, field, operator, v)
|
||||||
@@ -951,6 +955,46 @@ class Query < ActiveRecord::Base
|
|||||||
" WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
|
" WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
|
||||||
end
|
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+
|
# 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)
|
def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
|
||||||
sql = ''
|
sql = ''
|
||||||
@@ -1124,10 +1168,47 @@ class Query < ActiveRecord::Base
|
|||||||
})
|
})
|
||||||
end
|
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
|
# Adds filters for the given custom fields scope
|
||||||
def add_custom_fields_filters(scope, assoc=nil)
|
def add_custom_fields_filters(scope, assoc=nil)
|
||||||
scope.visible.where(:is_filter => true).sorted.each do |field|
|
scope.visible.where(:is_filter => true).sorted.each do |field|
|
||||||
add_custom_field_filter(field, assoc)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -948,6 +948,7 @@ en:
|
|||||||
label_attribute_of_assigned_to: "Assignee's %{name}"
|
label_attribute_of_assigned_to: "Assignee's %{name}"
|
||||||
label_attribute_of_user: "User's %{name}"
|
label_attribute_of_user: "User's %{name}"
|
||||||
label_attribute_of_fixed_version: "Target version'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_descendants: With subprojects
|
||||||
label_cross_project_tree: With project tree
|
label_cross_project_tree: With project tree
|
||||||
label_cross_project_hierarchy: With project hierarchy
|
label_cross_project_hierarchy: With project hierarchy
|
||||||
|
|||||||
@@ -959,6 +959,7 @@ fr:
|
|||||||
label_attribute_of_assigned_to: "%{name} de l'assigné"
|
label_attribute_of_assigned_to: "%{name} de l'assigné"
|
||||||
label_attribute_of_user: "%{name} de l'utilisateur"
|
label_attribute_of_user: "%{name} de l'utilisateur"
|
||||||
label_attribute_of_fixed_version: "%{name} de la version cible"
|
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_descendants: Avec les sous-projets
|
||||||
label_cross_project_tree: Avec tout l'arbre
|
label_cross_project_tree: Avec tout l'arbre
|
||||||
label_cross_project_hierarchy: Avec toute la hiérarchie
|
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
|
assert_equal [1, 3, 7, 8], find_issues_with_query(query).map(&:id).uniq.sort
|
||||||
end
|
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
|
def test_filter_on_relations_with_a_specific_issue
|
||||||
IssueRelation.delete_all
|
IssueRelation.delete_all
|
||||||
IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
|
IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
|
||||||
|
|||||||
Reference in New Issue
Block a user