##// END OF EJS Templates
Refactor: use an ordered hash to store available filters and remove :order option (#13154)....
Jean-Philippe Lang -
r11142:0be82ea2c45e
parent child
Show More
@@ -1,160 +1,139
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module QueriesHelper
20 module QueriesHelper
21 def filters_options_for_select(query)
21 def filters_options_for_select(query)
22 options_for_select(filters_options(query))
22 options_for_select(filters_options(query))
23 end
23 end
24
24
25 def filters_options(query)
25 def filters_options(query)
26 options = [[]]
26 options = [[]]
27 sorted_options = query.available_filters.sort do |a, b|
27 options += query.available_filters.map do |field, field_options|
28 ord = 0
29 if !(a[1][:order] == 20 && b[1][:order] == 20)
30 ord = a[1][:order] <=> b[1][:order]
31 else
32 cn = (CustomField::CUSTOM_FIELDS_NAMES.index(a[1][:field].class.name) <=>
33 CustomField::CUSTOM_FIELDS_NAMES.index(b[1][:field].class.name))
34 if cn != 0
35 ord = cn
36 else
37 f = (a[1][:field] <=> b[1][:field])
38 if f != 0
39 ord = f
40 else
41 # assigned_to or author
42 ord = (a[0] <=> b[0])
43 end
44 end
45 end
46 ord
47 end
48 options += sorted_options.map do |field, field_options|
49 [field_options[:name], field]
28 [field_options[:name], field]
50 end
29 end
51 end
30 end
52
31
53 def available_block_columns_tags(query)
32 def available_block_columns_tags(query)
54 tags = ''.html_safe
33 tags = ''.html_safe
55 query.available_block_columns.each do |column|
34 query.available_block_columns.each do |column|
56 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column)) + " #{column.caption}", :class => 'inline')
35 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column)) + " #{column.caption}", :class => 'inline')
57 end
36 end
58 tags
37 tags
59 end
38 end
60
39
61 def column_header(column)
40 def column_header(column)
62 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
41 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
63 :default_order => column.default_order) :
42 :default_order => column.default_order) :
64 content_tag('th', h(column.caption))
43 content_tag('th', h(column.caption))
65 end
44 end
66
45
67 def column_content(column, issue)
46 def column_content(column, issue)
68 value = column.value(issue)
47 value = column.value(issue)
69 if value.is_a?(Array)
48 if value.is_a?(Array)
70 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
49 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
71 else
50 else
72 column_value(column, issue, value)
51 column_value(column, issue, value)
73 end
52 end
74 end
53 end
75
54
76 def column_value(column, issue, value)
55 def column_value(column, issue, value)
77 case value.class.name
56 case value.class.name
78 when 'String'
57 when 'String'
79 if column.name == :subject
58 if column.name == :subject
80 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
59 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
81 elsif column.name == :description
60 elsif column.name == :description
82 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
61 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
83 else
62 else
84 h(value)
63 h(value)
85 end
64 end
86 when 'Time'
65 when 'Time'
87 format_time(value)
66 format_time(value)
88 when 'Date'
67 when 'Date'
89 format_date(value)
68 format_date(value)
90 when 'Fixnum'
69 when 'Fixnum'
91 if column.name == :done_ratio
70 if column.name == :done_ratio
92 progress_bar(value, :width => '80px')
71 progress_bar(value, :width => '80px')
93 else
72 else
94 value.to_s
73 value.to_s
95 end
74 end
96 when 'Float'
75 when 'Float'
97 sprintf "%.2f", value
76 sprintf "%.2f", value
98 when 'User'
77 when 'User'
99 link_to_user value
78 link_to_user value
100 when 'Project'
79 when 'Project'
101 link_to_project value
80 link_to_project value
102 when 'Version'
81 when 'Version'
103 link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
82 link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
104 when 'TrueClass'
83 when 'TrueClass'
105 l(:general_text_Yes)
84 l(:general_text_Yes)
106 when 'FalseClass'
85 when 'FalseClass'
107 l(:general_text_No)
86 l(:general_text_No)
108 when 'Issue'
87 when 'Issue'
109 value.visible? ? link_to_issue(value) : "##{value.id}"
88 value.visible? ? link_to_issue(value) : "##{value.id}"
110 when 'IssueRelation'
89 when 'IssueRelation'
111 other = value.other_issue(issue)
90 other = value.other_issue(issue)
112 content_tag('span',
91 content_tag('span',
113 (l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe,
92 (l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe,
114 :class => value.css_classes_for(issue))
93 :class => value.css_classes_for(issue))
115 else
94 else
116 h(value)
95 h(value)
117 end
96 end
118 end
97 end
119
98
120 # Retrieve query from session or build a new query
99 # Retrieve query from session or build a new query
121 def retrieve_query
100 def retrieve_query
122 if !params[:query_id].blank?
101 if !params[:query_id].blank?
123 cond = "project_id IS NULL"
102 cond = "project_id IS NULL"
124 cond << " OR project_id = #{@project.id}" if @project
103 cond << " OR project_id = #{@project.id}" if @project
125 @query = IssueQuery.find(params[:query_id], :conditions => cond)
104 @query = IssueQuery.find(params[:query_id], :conditions => cond)
126 raise ::Unauthorized unless @query.visible?
105 raise ::Unauthorized unless @query.visible?
127 @query.project = @project
106 @query.project = @project
128 session[:query] = {:id => @query.id, :project_id => @query.project_id}
107 session[:query] = {:id => @query.id, :project_id => @query.project_id}
129 sort_clear
108 sort_clear
130 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
109 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
131 # Give it a name, required to be valid
110 # Give it a name, required to be valid
132 @query = IssueQuery.new(:name => "_")
111 @query = IssueQuery.new(:name => "_")
133 @query.project = @project
112 @query.project = @project
134 @query.build_from_params(params)
113 @query.build_from_params(params)
135 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
114 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
136 else
115 else
137 # retrieve from session
116 # retrieve from session
138 @query = IssueQuery.find_by_id(session[:query][:id]) if session[:query][:id]
117 @query = IssueQuery.find_by_id(session[:query][:id]) if session[:query][:id]
139 @query ||= IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
118 @query ||= IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
140 @query.project = @project
119 @query.project = @project
141 end
120 end
142 end
121 end
143
122
144 def retrieve_query_from_session
123 def retrieve_query_from_session
145 if session[:query]
124 if session[:query]
146 if session[:query][:id]
125 if session[:query][:id]
147 @query = IssueQuery.find_by_id(session[:query][:id])
126 @query = IssueQuery.find_by_id(session[:query][:id])
148 return unless @query
127 return unless @query
149 else
128 else
150 @query = IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
129 @query = IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
151 end
130 end
152 if session[:query].has_key?(:project_id)
131 if session[:query].has_key?(:project_id)
153 @query.project_id = session[:query][:project_id]
132 @query.project_id = session[:query][:project_id]
154 else
133 else
155 @query.project = @project
134 @query.project = @project
156 end
135 end
157 @query
136 @query
158 end
137 end
159 end
138 end
160 end
139 end
@@ -1,420 +1,406
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssueQuery < Query
18 class IssueQuery < Query
19
19
20 self.queried_class = Issue
20 self.queried_class = Issue
21
21
22 self.available_columns = [
22 self.available_columns = [
23 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
23 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
24 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
24 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
25 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
25 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
26 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
26 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
27 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
27 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
28 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
28 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
29 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
29 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
30 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
30 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
31 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
31 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
32 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
32 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
33 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
33 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
34 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
34 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
35 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
35 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
36 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
36 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
37 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
37 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
38 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
38 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
39 QueryColumn.new(:relations, :caption => :label_related_issues),
39 QueryColumn.new(:relations, :caption => :label_related_issues),
40 QueryColumn.new(:description, :inline => false)
40 QueryColumn.new(:description, :inline => false)
41 ]
41 ]
42
42
43 scope :visible, lambda {|*args|
43 scope :visible, lambda {|*args|
44 user = args.shift || User.current
44 user = args.shift || User.current
45 base = Project.allowed_to_condition(user, :view_issues, *args)
45 base = Project.allowed_to_condition(user, :view_issues, *args)
46 user_id = user.logged? ? user.id : 0
46 user_id = user.logged? ? user.id : 0
47
47
48 includes(:project).where("(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id)
48 includes(:project).where("(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id)
49 }
49 }
50
50
51 def initialize(attributes=nil, *args)
51 def initialize(attributes=nil, *args)
52 super attributes
52 super attributes
53 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
53 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
54 end
54 end
55
55
56 # Returns true if the query is visible to +user+ or the current user.
56 # Returns true if the query is visible to +user+ or the current user.
57 def visible?(user=User.current)
57 def visible?(user=User.current)
58 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
58 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
59 end
59 end
60
60
61 def available_filters
61 def initialize_available_filters
62 return @available_filters if @available_filters
63 @available_filters = {
64 "status_id" => {
65 :type => :list_status, :order => 0,
66 :values => IssueStatus.sorted.all.collect{|s| [s.name, s.id.to_s] }
67 },
68 "tracker_id" => {
69 :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] }
70 },
71 "priority_id" => {
72 :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
73 },
74 "subject" => { :type => :text, :order => 8 },
75 "created_on" => { :type => :date_past, :order => 9 },
76 "updated_on" => { :type => :date_past, :order => 10 },
77 "start_date" => { :type => :date, :order => 11 },
78 "due_date" => { :type => :date, :order => 12 },
79 "estimated_hours" => { :type => :float, :order => 13 },
80 "done_ratio" => { :type => :integer, :order => 14 }
81 }
82 IssueRelation::TYPES.each do |relation_type, options|
83 @available_filters[relation_type] = {
84 :type => :relation, :order => @available_filters.size + 100,
85 :label => options[:name]
86 }
87 end
88 principals = []
62 principals = []
63 subprojects = []
64 versions = []
65 categories = []
66 issue_custom_fields = []
67
89 if project
68 if project
90 principals += project.principals.sort
69 principals += project.principals.sort
91 unless project.leaf?
70 unless project.leaf?
92 subprojects = project.descendants.visible.all
71 subprojects = project.descendants.visible.all
93 if subprojects.any?
72 principals += Principal.member_of(subprojects)
94 @available_filters["subproject_id"] = {
95 :type => :list_subprojects, :order => 13,
96 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
97 }
98 principals += Principal.member_of(subprojects)
99 end
100 end
73 end
74 versions = project.shared_versions.all
75 categories = project.issue_categories.all
76 issue_custom_fields = project.all_issue_custom_fields
101 else
77 else
102 if all_projects.any?
78 if all_projects.any?
103 # members of visible projects
104 principals += Principal.member_of(all_projects)
79 principals += Principal.member_of(all_projects)
105 # project filter
106 project_values = []
107 if User.current.logged? && User.current.memberships.any?
108 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
109 end
110 project_values += all_projects_values
111 @available_filters["project_id"] = {
112 :type => :list, :order => 1, :values => project_values
113 } unless project_values.empty?
114 end
80 end
81 versions = Version.visible.find_all_by_sharing('system')
82 issue_custom_fields = IssueCustomField.where(:is_filter => true, :is_for_all => true).all
115 end
83 end
116 principals.uniq!
84 principals.uniq!
117 principals.sort!
85 principals.sort!
118 users = principals.select {|p| p.is_a?(User)}
86 users = principals.select {|p| p.is_a?(User)}
119
87
120 assigned_to_values = []
88
121 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
89 add_available_filter "status_id",
122 assigned_to_values += (Setting.issue_group_assignment? ?
90 :type => :list_status, :values => IssueStatus.sorted.all.collect{|s| [s.name, s.id.to_s] }
123 principals : users).collect{|s| [s.name, s.id.to_s] }
91
124 @available_filters["assigned_to_id"] = {
92 if project.nil?
125 :type => :list_optional, :order => 4, :values => assigned_to_values
93 project_values = []
126 } unless assigned_to_values.empty?
94 if User.current.logged? && User.current.memberships.any?
95 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
96 end
97 project_values += all_projects_values
98 add_available_filter("project_id",
99 :type => :list, :values => project_values
100 ) unless project_values.empty?
101 end
102
103 add_available_filter "tracker_id",
104 :type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] }
105 add_available_filter "priority_id",
106 :type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
127
107
128 author_values = []
108 author_values = []
129 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
109 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
130 author_values += users.collect{|s| [s.name, s.id.to_s] }
110 author_values += users.collect{|s| [s.name, s.id.to_s] }
131 @available_filters["author_id"] = {
111 add_available_filter("author_id",
132 :type => :list, :order => 5, :values => author_values
112 :type => :list, :values => author_values
133 } unless author_values.empty?
113 ) unless author_values.empty?
114
115 assigned_to_values = []
116 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
117 assigned_to_values += (Setting.issue_group_assignment? ?
118 principals : users).collect{|s| [s.name, s.id.to_s] }
119 add_available_filter("assigned_to_id",
120 :type => :list_optional, :values => assigned_to_values
121 ) unless assigned_to_values.empty?
134
122
135 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
123 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
136 @available_filters["member_of_group"] = {
124 add_available_filter("member_of_group",
137 :type => :list_optional, :order => 6, :values => group_values
125 :type => :list_optional, :values => group_values
138 } unless group_values.empty?
126 ) unless group_values.empty?
139
127
140 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
128 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
141 @available_filters["assigned_to_role"] = {
129 add_available_filter("assigned_to_role",
142 :type => :list_optional, :order => 7, :values => role_values
130 :type => :list_optional, :values => role_values
143 } unless role_values.empty?
131 ) unless role_values.empty?
144
132
145 if User.current.logged?
133 if versions.any?
146 @available_filters["watcher_id"] = {
134 add_available_filter "fixed_version_id",
147 :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]]
135 :type => :list_optional,
148 }
136 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
149 end
137 end
150
138
151 if project
139 if categories.any?
152 # project specific filters
140 add_available_filter "category_id",
153 categories = project.issue_categories.all
141 :type => :list_optional,
154 unless categories.empty?
142 :values => categories.collect{|s| [s.name, s.id.to_s] }
155 @available_filters["category_id"] = {
156 :type => :list_optional, :order => 6,
157 :values => categories.collect{|s| [s.name, s.id.to_s] }
158 }
159 end
160 versions = project.shared_versions.all
161 unless versions.empty?
162 @available_filters["fixed_version_id"] = {
163 :type => :list_optional, :order => 7,
164 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
165 }
166 end
167 add_custom_fields_filters(project.all_issue_custom_fields)
168 else
169 # global filters for cross project issue list
170 system_shared_versions = Version.visible.find_all_by_sharing('system')
171 unless system_shared_versions.empty?
172 @available_filters["fixed_version_id"] = {
173 :type => :list_optional, :order => 7,
174 :values => system_shared_versions.sort.collect{|s|
175 ["#{s.project.name} - #{s.name}", s.id.to_s]
176 }
177 }
178 end
179 add_custom_fields_filters(IssueCustomField.where(:is_filter => true, :is_for_all => true).all)
180 end
143 end
181 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
144
145 add_available_filter "subject", :type => :text
146 add_available_filter "created_on", :type => :date_past
147 add_available_filter "updated_on", :type => :date_past
148 add_available_filter "start_date", :type => :date
149 add_available_filter "due_date", :type => :date
150 add_available_filter "estimated_hours", :type => :float
151 add_available_filter "done_ratio", :type => :integer
152
182 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
153 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
183 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
154 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
184 @available_filters["is_private"] = {
155 add_available_filter "is_private",
185 :type => :list, :order => 16,
156 :type => :list,
186 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
157 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
187 }
188 end
158 end
159
160 if User.current.logged?
161 add_available_filter "watcher_id",
162 :type => :list, :values => [["<< #{l(:label_me)} >>", "me"]]
163 end
164
165 if subprojects.any?
166 add_available_filter "subproject_id",
167 :type => :list_subprojects,
168 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
169 end
170
171 add_custom_fields_filters(issue_custom_fields)
172
173 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
174
175 IssueRelation::TYPES.each do |relation_type, options|
176 add_available_filter relation_type, :type => :relation, :label => options[:name]
177 end
178
189 Tracker.disabled_core_fields(trackers).each {|field|
179 Tracker.disabled_core_fields(trackers).each {|field|
190 @available_filters.delete field
180 delete_available_filter field
191 }
181 }
192 @available_filters.each do |field, options|
193 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
194 end
195 @available_filters
196 end
182 end
197
183
198 def available_columns
184 def available_columns
199 return @available_columns if @available_columns
185 return @available_columns if @available_columns
200 @available_columns = self.class.available_columns.dup
186 @available_columns = self.class.available_columns.dup
201 @available_columns += (project ?
187 @available_columns += (project ?
202 project.all_issue_custom_fields :
188 project.all_issue_custom_fields :
203 IssueCustomField.all
189 IssueCustomField.all
204 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
190 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
205
191
206 if User.current.allowed_to?(:view_time_entries, project, :global => true)
192 if User.current.allowed_to?(:view_time_entries, project, :global => true)
207 index = nil
193 index = nil
208 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
194 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
209 index = (index ? index + 1 : -1)
195 index = (index ? index + 1 : -1)
210 # insert the column after estimated_hours or at the end
196 # insert the column after estimated_hours or at the end
211 @available_columns.insert index, QueryColumn.new(:spent_hours,
197 @available_columns.insert index, QueryColumn.new(:spent_hours,
212 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id), 0)",
198 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id), 0)",
213 :default_order => 'desc',
199 :default_order => 'desc',
214 :caption => :label_spent_time
200 :caption => :label_spent_time
215 )
201 )
216 end
202 end
217
203
218 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
204 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
219 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
205 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
220 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
206 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
221 end
207 end
222
208
223 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
209 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
224 @available_columns.reject! {|column|
210 @available_columns.reject! {|column|
225 disabled_fields.include?(column.name.to_s)
211 disabled_fields.include?(column.name.to_s)
226 }
212 }
227
213
228 @available_columns
214 @available_columns
229 end
215 end
230
216
231 def sortable_columns
217 def sortable_columns
232 {'id' => "#{Issue.table_name}.id"}.merge(super)
218 {'id' => "#{Issue.table_name}.id"}.merge(super)
233 end
219 end
234
220
235 def default_columns_names
221 def default_columns_names
236 @default_columns_names ||= begin
222 @default_columns_names ||= begin
237 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
223 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
238
224
239 project.present? ? default_columns : [:project] | default_columns
225 project.present? ? default_columns : [:project] | default_columns
240 end
226 end
241 end
227 end
242
228
243 # Returns the issue count
229 # Returns the issue count
244 def issue_count
230 def issue_count
245 Issue.visible.count(:include => [:status, :project], :conditions => statement)
231 Issue.visible.count(:include => [:status, :project], :conditions => statement)
246 rescue ::ActiveRecord::StatementInvalid => e
232 rescue ::ActiveRecord::StatementInvalid => e
247 raise StatementInvalid.new(e.message)
233 raise StatementInvalid.new(e.message)
248 end
234 end
249
235
250 # Returns the issue count by group or nil if query is not grouped
236 # Returns the issue count by group or nil if query is not grouped
251 def issue_count_by_group
237 def issue_count_by_group
252 r = nil
238 r = nil
253 if grouped?
239 if grouped?
254 begin
240 begin
255 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
241 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
256 r = Issue.visible.count(:joins => joins_for_order_statement(group_by_statement), :group => group_by_statement, :include => [:status, :project], :conditions => statement)
242 r = Issue.visible.count(:joins => joins_for_order_statement(group_by_statement), :group => group_by_statement, :include => [:status, :project], :conditions => statement)
257 rescue ActiveRecord::RecordNotFound
243 rescue ActiveRecord::RecordNotFound
258 r = {nil => issue_count}
244 r = {nil => issue_count}
259 end
245 end
260 c = group_by_column
246 c = group_by_column
261 if c.is_a?(QueryCustomFieldColumn)
247 if c.is_a?(QueryCustomFieldColumn)
262 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
248 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
263 end
249 end
264 end
250 end
265 r
251 r
266 rescue ::ActiveRecord::StatementInvalid => e
252 rescue ::ActiveRecord::StatementInvalid => e
267 raise StatementInvalid.new(e.message)
253 raise StatementInvalid.new(e.message)
268 end
254 end
269
255
270 # Returns the issues
256 # Returns the issues
271 # Valid options are :order, :offset, :limit, :include, :conditions
257 # Valid options are :order, :offset, :limit, :include, :conditions
272 def issues(options={})
258 def issues(options={})
273 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
259 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
274
260
275 issues = Issue.visible.where(options[:conditions]).all(
261 issues = Issue.visible.where(options[:conditions]).all(
276 :include => ([:status, :project] + (options[:include] || [])).uniq,
262 :include => ([:status, :project] + (options[:include] || [])).uniq,
277 :conditions => statement,
263 :conditions => statement,
278 :order => order_option,
264 :order => order_option,
279 :joins => joins_for_order_statement(order_option.join(',')),
265 :joins => joins_for_order_statement(order_option.join(',')),
280 :limit => options[:limit],
266 :limit => options[:limit],
281 :offset => options[:offset]
267 :offset => options[:offset]
282 )
268 )
283
269
284 if has_column?(:spent_hours)
270 if has_column?(:spent_hours)
285 Issue.load_visible_spent_hours(issues)
271 Issue.load_visible_spent_hours(issues)
286 end
272 end
287 if has_column?(:relations)
273 if has_column?(:relations)
288 Issue.load_visible_relations(issues)
274 Issue.load_visible_relations(issues)
289 end
275 end
290 issues
276 issues
291 rescue ::ActiveRecord::StatementInvalid => e
277 rescue ::ActiveRecord::StatementInvalid => e
292 raise StatementInvalid.new(e.message)
278 raise StatementInvalid.new(e.message)
293 end
279 end
294
280
295 # Returns the issues ids
281 # Returns the issues ids
296 def issue_ids(options={})
282 def issue_ids(options={})
297 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
283 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
298
284
299 Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
285 Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
300 :conditions => statement,
286 :conditions => statement,
301 :order => order_option,
287 :order => order_option,
302 :joins => joins_for_order_statement(order_option.join(',')),
288 :joins => joins_for_order_statement(order_option.join(',')),
303 :limit => options[:limit],
289 :limit => options[:limit],
304 :offset => options[:offset]).find_ids
290 :offset => options[:offset]).find_ids
305 rescue ::ActiveRecord::StatementInvalid => e
291 rescue ::ActiveRecord::StatementInvalid => e
306 raise StatementInvalid.new(e.message)
292 raise StatementInvalid.new(e.message)
307 end
293 end
308
294
309 # Returns the journals
295 # Returns the journals
310 # Valid options are :order, :offset, :limit
296 # Valid options are :order, :offset, :limit
311 def journals(options={})
297 def journals(options={})
312 Journal.visible.all(
298 Journal.visible.all(
313 :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
299 :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
314 :conditions => statement,
300 :conditions => statement,
315 :order => options[:order],
301 :order => options[:order],
316 :limit => options[:limit],
302 :limit => options[:limit],
317 :offset => options[:offset]
303 :offset => options[:offset]
318 )
304 )
319 rescue ::ActiveRecord::StatementInvalid => e
305 rescue ::ActiveRecord::StatementInvalid => e
320 raise StatementInvalid.new(e.message)
306 raise StatementInvalid.new(e.message)
321 end
307 end
322
308
323 # Returns the versions
309 # Returns the versions
324 # Valid options are :conditions
310 # Valid options are :conditions
325 def versions(options={})
311 def versions(options={})
326 Version.visible.where(options[:conditions]).all(
312 Version.visible.where(options[:conditions]).all(
327 :include => :project,
313 :include => :project,
328 :conditions => project_statement
314 :conditions => project_statement
329 )
315 )
330 rescue ::ActiveRecord::StatementInvalid => e
316 rescue ::ActiveRecord::StatementInvalid => e
331 raise StatementInvalid.new(e.message)
317 raise StatementInvalid.new(e.message)
332 end
318 end
333
319
334 def sql_for_watcher_id_field(field, operator, value)
320 def sql_for_watcher_id_field(field, operator, value)
335 db_table = Watcher.table_name
321 db_table = Watcher.table_name
336 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
322 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
337 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
323 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
338 end
324 end
339
325
340 def sql_for_member_of_group_field(field, operator, value)
326 def sql_for_member_of_group_field(field, operator, value)
341 if operator == '*' # Any group
327 if operator == '*' # Any group
342 groups = Group.all
328 groups = Group.all
343 operator = '=' # Override the operator since we want to find by assigned_to
329 operator = '=' # Override the operator since we want to find by assigned_to
344 elsif operator == "!*"
330 elsif operator == "!*"
345 groups = Group.all
331 groups = Group.all
346 operator = '!' # Override the operator since we want to find by assigned_to
332 operator = '!' # Override the operator since we want to find by assigned_to
347 else
333 else
348 groups = Group.find_all_by_id(value)
334 groups = Group.find_all_by_id(value)
349 end
335 end
350 groups ||= []
336 groups ||= []
351
337
352 members_of_groups = groups.inject([]) {|user_ids, group|
338 members_of_groups = groups.inject([]) {|user_ids, group|
353 user_ids + group.user_ids + [group.id]
339 user_ids + group.user_ids + [group.id]
354 }.uniq.compact.sort.collect(&:to_s)
340 }.uniq.compact.sort.collect(&:to_s)
355
341
356 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
342 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
357 end
343 end
358
344
359 def sql_for_assigned_to_role_field(field, operator, value)
345 def sql_for_assigned_to_role_field(field, operator, value)
360 case operator
346 case operator
361 when "*", "!*" # Member / Not member
347 when "*", "!*" # Member / Not member
362 sw = operator == "!*" ? 'NOT' : ''
348 sw = operator == "!*" ? 'NOT' : ''
363 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
349 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
364 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
350 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
365 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
351 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
366 when "=", "!"
352 when "=", "!"
367 role_cond = value.any? ?
353 role_cond = value.any? ?
368 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
354 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
369 "1=0"
355 "1=0"
370
356
371 sw = operator == "!" ? 'NOT' : ''
357 sw = operator == "!" ? 'NOT' : ''
372 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
358 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
373 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
359 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
374 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
360 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
375 end
361 end
376 end
362 end
377
363
378 def sql_for_is_private_field(field, operator, value)
364 def sql_for_is_private_field(field, operator, value)
379 op = (operator == "=" ? 'IN' : 'NOT IN')
365 op = (operator == "=" ? 'IN' : 'NOT IN')
380 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
366 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
381
367
382 "#{Issue.table_name}.is_private #{op} (#{va})"
368 "#{Issue.table_name}.is_private #{op} (#{va})"
383 end
369 end
384
370
385 def sql_for_relations(field, operator, value, options={})
371 def sql_for_relations(field, operator, value, options={})
386 relation_options = IssueRelation::TYPES[field]
372 relation_options = IssueRelation::TYPES[field]
387 return relation_options unless relation_options
373 return relation_options unless relation_options
388
374
389 relation_type = field
375 relation_type = field
390 join_column, target_join_column = "issue_from_id", "issue_to_id"
376 join_column, target_join_column = "issue_from_id", "issue_to_id"
391 if relation_options[:reverse] || options[:reverse]
377 if relation_options[:reverse] || options[:reverse]
392 relation_type = relation_options[:reverse] || relation_type
378 relation_type = relation_options[:reverse] || relation_type
393 join_column, target_join_column = target_join_column, join_column
379 join_column, target_join_column = target_join_column, join_column
394 end
380 end
395
381
396 sql = case operator
382 sql = case operator
397 when "*", "!*"
383 when "*", "!*"
398 op = (operator == "*" ? 'IN' : 'NOT IN')
384 op = (operator == "*" ? 'IN' : 'NOT IN')
399 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')"
385 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')"
400 when "=", "!"
386 when "=", "!"
401 op = (operator == "=" ? 'IN' : 'NOT IN')
387 op = (operator == "=" ? 'IN' : 'NOT IN')
402 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
388 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
403 when "=p", "=!p", "!p"
389 when "=p", "=!p", "!p"
404 op = (operator == "!p" ? 'NOT IN' : 'IN')
390 op = (operator == "!p" ? 'NOT IN' : 'IN')
405 comp = (operator == "=!p" ? '<>' : '=')
391 comp = (operator == "=!p" ? '<>' : '=')
406 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
392 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
407 end
393 end
408
394
409 if relation_options[:sym] == field && !options[:reverse]
395 if relation_options[:sym] == field && !options[:reverse]
410 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
396 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
411 sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
397 sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
412 else
398 else
413 sql
399 sql
414 end
400 end
415 end
401 end
416
402
417 IssueRelation::TYPES.keys.each do |relation_type|
403 IssueRelation::TYPES.keys.each do |relation_type|
418 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
404 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
419 end
405 end
420 end
406 end
@@ -1,792 +1,822
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :groupable, :default_order
19 attr_accessor :name, :sortable, :groupable, :default_order
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.groupable = options[:groupable] || false
25 self.groupable = options[:groupable] || false
26 if groupable == true
26 if groupable == true
27 self.groupable = name.to_s
27 self.groupable = name.to_s
28 end
28 end
29 self.default_order = options[:default_order]
29 self.default_order = options[:default_order]
30 @inline = options.key?(:inline) ? options[:inline] : true
30 @inline = options.key?(:inline) ? options[:inline] : true
31 @caption_key = options[:caption] || "field_#{name}"
31 @caption_key = options[:caption] || "field_#{name}"
32 end
32 end
33
33
34 def caption
34 def caption
35 l(@caption_key)
35 l(@caption_key)
36 end
36 end
37
37
38 # Returns true if the column is sortable, otherwise false
38 # Returns true if the column is sortable, otherwise false
39 def sortable?
39 def sortable?
40 !@sortable.nil?
40 !@sortable.nil?
41 end
41 end
42
42
43 def sortable
43 def sortable
44 @sortable.is_a?(Proc) ? @sortable.call : @sortable
44 @sortable.is_a?(Proc) ? @sortable.call : @sortable
45 end
45 end
46
46
47 def inline?
47 def inline?
48 @inline
48 @inline
49 end
49 end
50
50
51 def value(object)
51 def value(object)
52 object.send name
52 object.send name
53 end
53 end
54
54
55 def css_classes
55 def css_classes
56 name
56 name
57 end
57 end
58 end
58 end
59
59
60 class QueryCustomFieldColumn < QueryColumn
60 class QueryCustomFieldColumn < QueryColumn
61
61
62 def initialize(custom_field)
62 def initialize(custom_field)
63 self.name = "cf_#{custom_field.id}".to_sym
63 self.name = "cf_#{custom_field.id}".to_sym
64 self.sortable = custom_field.order_statement || false
64 self.sortable = custom_field.order_statement || false
65 self.groupable = custom_field.group_statement || false
65 self.groupable = custom_field.group_statement || false
66 @inline = true
66 @inline = true
67 @cf = custom_field
67 @cf = custom_field
68 end
68 end
69
69
70 def caption
70 def caption
71 @cf.name
71 @cf.name
72 end
72 end
73
73
74 def custom_field
74 def custom_field
75 @cf
75 @cf
76 end
76 end
77
77
78 def value(object)
78 def value(object)
79 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
79 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
80 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
80 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
81 end
81 end
82
82
83 def css_classes
83 def css_classes
84 @css_classes ||= "#{name} #{@cf.field_format}"
84 @css_classes ||= "#{name} #{@cf.field_format}"
85 end
85 end
86 end
86 end
87
87
88 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
88 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
89
89
90 def initialize(association, custom_field)
90 def initialize(association, custom_field)
91 super(custom_field)
91 super(custom_field)
92 self.name = "#{association}.cf_#{custom_field.id}".to_sym
92 self.name = "#{association}.cf_#{custom_field.id}".to_sym
93 # TODO: support sorting/grouping by association custom field
93 # TODO: support sorting/grouping by association custom field
94 self.sortable = false
94 self.sortable = false
95 self.groupable = false
95 self.groupable = false
96 @association = association
96 @association = association
97 end
97 end
98
98
99 def value(object)
99 def value(object)
100 if assoc = object.send(@association)
100 if assoc = object.send(@association)
101 super(assoc)
101 super(assoc)
102 end
102 end
103 end
103 end
104
104
105 def css_classes
105 def css_classes
106 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
106 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
107 end
107 end
108 end
108 end
109
109
110 class Query < ActiveRecord::Base
110 class Query < ActiveRecord::Base
111 class StatementInvalid < ::ActiveRecord::StatementInvalid
111 class StatementInvalid < ::ActiveRecord::StatementInvalid
112 end
112 end
113
113
114 belongs_to :project
114 belongs_to :project
115 belongs_to :user
115 belongs_to :user
116 serialize :filters
116 serialize :filters
117 serialize :column_names
117 serialize :column_names
118 serialize :sort_criteria, Array
118 serialize :sort_criteria, Array
119
119
120 attr_protected :project_id, :user_id
120 attr_protected :project_id, :user_id
121
121
122 validates_presence_of :name
122 validates_presence_of :name
123 validates_length_of :name, :maximum => 255
123 validates_length_of :name, :maximum => 255
124 validate :validate_query_filters
124 validate :validate_query_filters
125
125
126 class_attribute :operators
126 class_attribute :operators
127 self.operators = {
127 self.operators = {
128 "=" => :label_equals,
128 "=" => :label_equals,
129 "!" => :label_not_equals,
129 "!" => :label_not_equals,
130 "o" => :label_open_issues,
130 "o" => :label_open_issues,
131 "c" => :label_closed_issues,
131 "c" => :label_closed_issues,
132 "!*" => :label_none,
132 "!*" => :label_none,
133 "*" => :label_any,
133 "*" => :label_any,
134 ">=" => :label_greater_or_equal,
134 ">=" => :label_greater_or_equal,
135 "<=" => :label_less_or_equal,
135 "<=" => :label_less_or_equal,
136 "><" => :label_between,
136 "><" => :label_between,
137 "<t+" => :label_in_less_than,
137 "<t+" => :label_in_less_than,
138 ">t+" => :label_in_more_than,
138 ">t+" => :label_in_more_than,
139 "><t+"=> :label_in_the_next_days,
139 "><t+"=> :label_in_the_next_days,
140 "t+" => :label_in,
140 "t+" => :label_in,
141 "t" => :label_today,
141 "t" => :label_today,
142 "ld" => :label_yesterday,
142 "ld" => :label_yesterday,
143 "w" => :label_this_week,
143 "w" => :label_this_week,
144 "lw" => :label_last_week,
144 "lw" => :label_last_week,
145 "l2w" => [:label_last_n_weeks, {:count => 2}],
145 "l2w" => [:label_last_n_weeks, {:count => 2}],
146 "m" => :label_this_month,
146 "m" => :label_this_month,
147 "lm" => :label_last_month,
147 "lm" => :label_last_month,
148 "y" => :label_this_year,
148 "y" => :label_this_year,
149 ">t-" => :label_less_than_ago,
149 ">t-" => :label_less_than_ago,
150 "<t-" => :label_more_than_ago,
150 "<t-" => :label_more_than_ago,
151 "><t-"=> :label_in_the_past_days,
151 "><t-"=> :label_in_the_past_days,
152 "t-" => :label_ago,
152 "t-" => :label_ago,
153 "~" => :label_contains,
153 "~" => :label_contains,
154 "!~" => :label_not_contains,
154 "!~" => :label_not_contains,
155 "=p" => :label_any_issues_in_project,
155 "=p" => :label_any_issues_in_project,
156 "=!p" => :label_any_issues_not_in_project,
156 "=!p" => :label_any_issues_not_in_project,
157 "!p" => :label_no_issues_in_project
157 "!p" => :label_no_issues_in_project
158 }
158 }
159
159
160 class_attribute :operators_by_filter_type
160 class_attribute :operators_by_filter_type
161 self.operators_by_filter_type = {
161 self.operators_by_filter_type = {
162 :list => [ "=", "!" ],
162 :list => [ "=", "!" ],
163 :list_status => [ "o", "=", "!", "c", "*" ],
163 :list_status => [ "o", "=", "!", "c", "*" ],
164 :list_optional => [ "=", "!", "!*", "*" ],
164 :list_optional => [ "=", "!", "!*", "*" ],
165 :list_subprojects => [ "*", "!*", "=" ],
165 :list_subprojects => [ "*", "!*", "=" ],
166 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
166 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
167 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
167 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
168 :string => [ "=", "~", "!", "!~", "!*", "*" ],
168 :string => [ "=", "~", "!", "!~", "!*", "*" ],
169 :text => [ "~", "!~", "!*", "*" ],
169 :text => [ "~", "!~", "!*", "*" ],
170 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
170 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
171 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
171 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
172 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]
172 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]
173 }
173 }
174
174
175 class_attribute :available_columns
175 class_attribute :available_columns
176 self.available_columns = []
176 self.available_columns = []
177
177
178 class_attribute :queried_class
178 class_attribute :queried_class
179
179
180 def queried_table_name
180 def queried_table_name
181 @queried_table_name ||= self.class.queried_class.table_name
181 @queried_table_name ||= self.class.queried_class.table_name
182 end
182 end
183
183
184 def initialize(attributes=nil, *args)
184 def initialize(attributes=nil, *args)
185 super attributes
185 super attributes
186 @is_for_all = project.nil?
186 @is_for_all = project.nil?
187 end
187 end
188
188
189 # Builds the query from the given params
189 # Builds the query from the given params
190 def build_from_params(params)
190 def build_from_params(params)
191 if params[:fields] || params[:f]
191 if params[:fields] || params[:f]
192 self.filters = {}
192 self.filters = {}
193 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
193 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
194 else
194 else
195 available_filters.keys.each do |field|
195 available_filters.keys.each do |field|
196 add_short_filter(field, params[field]) if params[field]
196 add_short_filter(field, params[field]) if params[field]
197 end
197 end
198 end
198 end
199 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
199 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
200 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
200 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
201 self
201 self
202 end
202 end
203
203
204 # Builds a new query from the given params and attributes
204 # Builds a new query from the given params and attributes
205 def self.build_from_params(params, attributes={})
205 def self.build_from_params(params, attributes={})
206 new(attributes).build_from_params(params)
206 new(attributes).build_from_params(params)
207 end
207 end
208
208
209 def validate_query_filters
209 def validate_query_filters
210 filters.each_key do |field|
210 filters.each_key do |field|
211 if values_for(field)
211 if values_for(field)
212 case type_for(field)
212 case type_for(field)
213 when :integer
213 when :integer
214 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
214 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
215 when :float
215 when :float
216 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
216 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
217 when :date, :date_past
217 when :date, :date_past
218 case operator_for(field)
218 case operator_for(field)
219 when "=", ">=", "<=", "><"
219 when "=", ">=", "<=", "><"
220 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
220 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
221 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
221 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
222 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
222 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
223 end
223 end
224 end
224 end
225 end
225 end
226
226
227 add_filter_error(field, :blank) unless
227 add_filter_error(field, :blank) unless
228 # filter requires one or more values
228 # filter requires one or more values
229 (values_for(field) and !values_for(field).first.blank?) or
229 (values_for(field) and !values_for(field).first.blank?) or
230 # filter doesn't require any value
230 # filter doesn't require any value
231 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y"].include? operator_for(field)
231 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y"].include? operator_for(field)
232 end if filters
232 end if filters
233 end
233 end
234
234
235 def add_filter_error(field, message)
235 def add_filter_error(field, message)
236 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
236 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
237 errors.add(:base, m)
237 errors.add(:base, m)
238 end
238 end
239
239
240 def editable_by?(user)
240 def editable_by?(user)
241 return false unless user
241 return false unless user
242 # Admin can edit them all and regular users can edit their private queries
242 # Admin can edit them all and regular users can edit their private queries
243 return true if user.admin? || (!is_public && self.user_id == user.id)
243 return true if user.admin? || (!is_public && self.user_id == user.id)
244 # Members can not edit public queries that are for all project (only admin is allowed to)
244 # Members can not edit public queries that are for all project (only admin is allowed to)
245 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
245 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
246 end
246 end
247
247
248 def trackers
248 def trackers
249 @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers
249 @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers
250 end
250 end
251
251
252 # Returns a hash of localized labels for all filter operators
252 # Returns a hash of localized labels for all filter operators
253 def self.operators_labels
253 def self.operators_labels
254 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
254 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
255 end
255 end
256
256
257 # Returns a representation of the available filters for JSON serialization
257 # Returns a representation of the available filters for JSON serialization
258 def available_filters_as_json
258 def available_filters_as_json
259 json = {}
259 json = {}
260 available_filters.each do |field, options|
260 available_filters.each do |field, options|
261 json[field] = options.slice(:type, :name, :values).stringify_keys
261 json[field] = options.slice(:type, :name, :values).stringify_keys
262 end
262 end
263 json
263 json
264 end
264 end
265
265
266 def all_projects
266 def all_projects
267 @all_projects ||= Project.visible.all
267 @all_projects ||= Project.visible.all
268 end
268 end
269
269
270 def all_projects_values
270 def all_projects_values
271 return @all_projects_values if @all_projects_values
271 return @all_projects_values if @all_projects_values
272
272
273 values = []
273 values = []
274 Project.project_tree(all_projects) do |p, level|
274 Project.project_tree(all_projects) do |p, level|
275 prefix = (level > 0 ? ('--' * level + ' ') : '')
275 prefix = (level > 0 ? ('--' * level + ' ') : '')
276 values << ["#{prefix}#{p.name}", p.id.to_s]
276 values << ["#{prefix}#{p.name}", p.id.to_s]
277 end
277 end
278 @all_projects_values = values
278 @all_projects_values = values
279 end
279 end
280
280
281 # Adds available filters
282 def initialize_available_filters
283 # implemented by sub-classes
284 end
285 protected :initialize_available_filters
286
287 # Adds an available filter
288 def add_available_filter(field, options)
289 @available_filters ||= ActiveSupport::OrderedHash.new
290 @available_filters[field] = options
291 @available_filters
292 end
293
294 # Removes an available filter
295 def delete_available_filter(field)
296 if @available_filters
297 @available_filters.delete(field)
298 end
299 end
300
301 # Return a hash of available filters
302 def available_filters
303 unless @available_filters
304 initialize_available_filters
305 @available_filters.each do |field, options|
306 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
307 end
308 end
309 @available_filters
310 end
311
281 def add_filter(field, operator, values=nil)
312 def add_filter(field, operator, values=nil)
282 # values must be an array
313 # values must be an array
283 return unless values.nil? || values.is_a?(Array)
314 return unless values.nil? || values.is_a?(Array)
284 # check if field is defined as an available filter
315 # check if field is defined as an available filter
285 if available_filters.has_key? field
316 if available_filters.has_key? field
286 filter_options = available_filters[field]
317 filter_options = available_filters[field]
287 filters[field] = {:operator => operator, :values => (values || [''])}
318 filters[field] = {:operator => operator, :values => (values || [''])}
288 end
319 end
289 end
320 end
290
321
291 def add_short_filter(field, expression)
322 def add_short_filter(field, expression)
292 return unless expression && available_filters.has_key?(field)
323 return unless expression && available_filters.has_key?(field)
293 field_type = available_filters[field][:type]
324 field_type = available_filters[field][:type]
294 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
325 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
295 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
326 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
296 values = $1
327 values = $1
297 add_filter field, operator, values.present? ? values.split('|') : ['']
328 add_filter field, operator, values.present? ? values.split('|') : ['']
298 end || add_filter(field, '=', expression.split('|'))
329 end || add_filter(field, '=', expression.split('|'))
299 end
330 end
300
331
301 # Add multiple filters using +add_filter+
332 # Add multiple filters using +add_filter+
302 def add_filters(fields, operators, values)
333 def add_filters(fields, operators, values)
303 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
334 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
304 fields.each do |field|
335 fields.each do |field|
305 add_filter(field, operators[field], values && values[field])
336 add_filter(field, operators[field], values && values[field])
306 end
337 end
307 end
338 end
308 end
339 end
309
340
310 def has_filter?(field)
341 def has_filter?(field)
311 filters and filters[field]
342 filters and filters[field]
312 end
343 end
313
344
314 def type_for(field)
345 def type_for(field)
315 available_filters[field][:type] if available_filters.has_key?(field)
346 available_filters[field][:type] if available_filters.has_key?(field)
316 end
347 end
317
348
318 def operator_for(field)
349 def operator_for(field)
319 has_filter?(field) ? filters[field][:operator] : nil
350 has_filter?(field) ? filters[field][:operator] : nil
320 end
351 end
321
352
322 def values_for(field)
353 def values_for(field)
323 has_filter?(field) ? filters[field][:values] : nil
354 has_filter?(field) ? filters[field][:values] : nil
324 end
355 end
325
356
326 def value_for(field, index=0)
357 def value_for(field, index=0)
327 (values_for(field) || [])[index]
358 (values_for(field) || [])[index]
328 end
359 end
329
360
330 def label_for(field)
361 def label_for(field)
331 label = available_filters[field][:name] if available_filters.has_key?(field)
362 label = available_filters[field][:name] if available_filters.has_key?(field)
332 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
363 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
333 end
364 end
334
365
335 def self.add_available_column(column)
366 def self.add_available_column(column)
336 self.available_columns << (column) if column.is_a?(QueryColumn)
367 self.available_columns << (column) if column.is_a?(QueryColumn)
337 end
368 end
338
369
339 # Returns an array of columns that can be used to group the results
370 # Returns an array of columns that can be used to group the results
340 def groupable_columns
371 def groupable_columns
341 available_columns.select {|c| c.groupable}
372 available_columns.select {|c| c.groupable}
342 end
373 end
343
374
344 # Returns a Hash of columns and the key for sorting
375 # Returns a Hash of columns and the key for sorting
345 def sortable_columns
376 def sortable_columns
346 available_columns.inject({}) {|h, column|
377 available_columns.inject({}) {|h, column|
347 h[column.name.to_s] = column.sortable
378 h[column.name.to_s] = column.sortable
348 h
379 h
349 }
380 }
350 end
381 end
351
382
352 def columns
383 def columns
353 # preserve the column_names order
384 # preserve the column_names order
354 (has_default_columns? ? default_columns_names : column_names).collect do |name|
385 (has_default_columns? ? default_columns_names : column_names).collect do |name|
355 available_columns.find { |col| col.name == name }
386 available_columns.find { |col| col.name == name }
356 end.compact
387 end.compact
357 end
388 end
358
389
359 def inline_columns
390 def inline_columns
360 columns.select(&:inline?)
391 columns.select(&:inline?)
361 end
392 end
362
393
363 def block_columns
394 def block_columns
364 columns.reject(&:inline?)
395 columns.reject(&:inline?)
365 end
396 end
366
397
367 def available_inline_columns
398 def available_inline_columns
368 available_columns.select(&:inline?)
399 available_columns.select(&:inline?)
369 end
400 end
370
401
371 def available_block_columns
402 def available_block_columns
372 available_columns.reject(&:inline?)
403 available_columns.reject(&:inline?)
373 end
404 end
374
405
375 def default_columns_names
406 def default_columns_names
376 []
407 []
377 end
408 end
378
409
379 def column_names=(names)
410 def column_names=(names)
380 if names
411 if names
381 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
412 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
382 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
413 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
383 # Set column_names to nil if default columns
414 # Set column_names to nil if default columns
384 if names == default_columns_names
415 if names == default_columns_names
385 names = nil
416 names = nil
386 end
417 end
387 end
418 end
388 write_attribute(:column_names, names)
419 write_attribute(:column_names, names)
389 end
420 end
390
421
391 def has_column?(column)
422 def has_column?(column)
392 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
423 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
393 end
424 end
394
425
395 def has_default_columns?
426 def has_default_columns?
396 column_names.nil? || column_names.empty?
427 column_names.nil? || column_names.empty?
397 end
428 end
398
429
399 def sort_criteria=(arg)
430 def sort_criteria=(arg)
400 c = []
431 c = []
401 if arg.is_a?(Hash)
432 if arg.is_a?(Hash)
402 arg = arg.keys.sort.collect {|k| arg[k]}
433 arg = arg.keys.sort.collect {|k| arg[k]}
403 end
434 end
404 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
435 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
405 write_attribute(:sort_criteria, c)
436 write_attribute(:sort_criteria, c)
406 end
437 end
407
438
408 def sort_criteria
439 def sort_criteria
409 read_attribute(:sort_criteria) || []
440 read_attribute(:sort_criteria) || []
410 end
441 end
411
442
412 def sort_criteria_key(arg)
443 def sort_criteria_key(arg)
413 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
444 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
414 end
445 end
415
446
416 def sort_criteria_order(arg)
447 def sort_criteria_order(arg)
417 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
448 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
418 end
449 end
419
450
420 def sort_criteria_order_for(key)
451 def sort_criteria_order_for(key)
421 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
452 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
422 end
453 end
423
454
424 # Returns the SQL sort order that should be prepended for grouping
455 # Returns the SQL sort order that should be prepended for grouping
425 def group_by_sort_order
456 def group_by_sort_order
426 if grouped? && (column = group_by_column)
457 if grouped? && (column = group_by_column)
427 order = sort_criteria_order_for(column.name) || column.default_order
458 order = sort_criteria_order_for(column.name) || column.default_order
428 column.sortable.is_a?(Array) ?
459 column.sortable.is_a?(Array) ?
429 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
460 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
430 "#{column.sortable} #{order}"
461 "#{column.sortable} #{order}"
431 end
462 end
432 end
463 end
433
464
434 # Returns true if the query is a grouped query
465 # Returns true if the query is a grouped query
435 def grouped?
466 def grouped?
436 !group_by_column.nil?
467 !group_by_column.nil?
437 end
468 end
438
469
439 def group_by_column
470 def group_by_column
440 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
471 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
441 end
472 end
442
473
443 def group_by_statement
474 def group_by_statement
444 group_by_column.try(:groupable)
475 group_by_column.try(:groupable)
445 end
476 end
446
477
447 def project_statement
478 def project_statement
448 project_clauses = []
479 project_clauses = []
449 if project && !project.descendants.active.empty?
480 if project && !project.descendants.active.empty?
450 ids = [project.id]
481 ids = [project.id]
451 if has_filter?("subproject_id")
482 if has_filter?("subproject_id")
452 case operator_for("subproject_id")
483 case operator_for("subproject_id")
453 when '='
484 when '='
454 # include the selected subprojects
485 # include the selected subprojects
455 ids += values_for("subproject_id").each(&:to_i)
486 ids += values_for("subproject_id").each(&:to_i)
456 when '!*'
487 when '!*'
457 # main project only
488 # main project only
458 else
489 else
459 # all subprojects
490 # all subprojects
460 ids += project.descendants.collect(&:id)
491 ids += project.descendants.collect(&:id)
461 end
492 end
462 elsif Setting.display_subprojects_issues?
493 elsif Setting.display_subprojects_issues?
463 ids += project.descendants.collect(&:id)
494 ids += project.descendants.collect(&:id)
464 end
495 end
465 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
496 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
466 elsif project
497 elsif project
467 project_clauses << "#{Project.table_name}.id = %d" % project.id
498 project_clauses << "#{Project.table_name}.id = %d" % project.id
468 end
499 end
469 project_clauses.any? ? project_clauses.join(' AND ') : nil
500 project_clauses.any? ? project_clauses.join(' AND ') : nil
470 end
501 end
471
502
472 def statement
503 def statement
473 # filters clauses
504 # filters clauses
474 filters_clauses = []
505 filters_clauses = []
475 filters.each_key do |field|
506 filters.each_key do |field|
476 next if field == "subproject_id"
507 next if field == "subproject_id"
477 v = values_for(field).clone
508 v = values_for(field).clone
478 next unless v and !v.empty?
509 next unless v and !v.empty?
479 operator = operator_for(field)
510 operator = operator_for(field)
480
511
481 # "me" value subsitution
512 # "me" value subsitution
482 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
513 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
483 if v.delete("me")
514 if v.delete("me")
484 if User.current.logged?
515 if User.current.logged?
485 v.push(User.current.id.to_s)
516 v.push(User.current.id.to_s)
486 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
517 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
487 else
518 else
488 v.push("0")
519 v.push("0")
489 end
520 end
490 end
521 end
491 end
522 end
492
523
493 if field == 'project_id'
524 if field == 'project_id'
494 if v.delete('mine')
525 if v.delete('mine')
495 v += User.current.memberships.map(&:project_id).map(&:to_s)
526 v += User.current.memberships.map(&:project_id).map(&:to_s)
496 end
527 end
497 end
528 end
498
529
499 if field =~ /cf_(\d+)$/
530 if field =~ /cf_(\d+)$/
500 # custom field
531 # custom field
501 filters_clauses << sql_for_custom_field(field, operator, v, $1)
532 filters_clauses << sql_for_custom_field(field, operator, v, $1)
502 elsif respond_to?("sql_for_#{field}_field")
533 elsif respond_to?("sql_for_#{field}_field")
503 # specific statement
534 # specific statement
504 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
535 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
505 else
536 else
506 # regular field
537 # regular field
507 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
538 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
508 end
539 end
509 end if filters and valid?
540 end if filters and valid?
510
541
511 filters_clauses << project_statement
542 filters_clauses << project_statement
512 filters_clauses.reject!(&:blank?)
543 filters_clauses.reject!(&:blank?)
513
544
514 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
545 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
515 end
546 end
516
547
517 private
548 private
518
549
519 def sql_for_custom_field(field, operator, value, custom_field_id)
550 def sql_for_custom_field(field, operator, value, custom_field_id)
520 db_table = CustomValue.table_name
551 db_table = CustomValue.table_name
521 db_field = 'value'
552 db_field = 'value'
522 filter = @available_filters[field]
553 filter = @available_filters[field]
523 return nil unless filter
554 return nil unless filter
524 if filter[:format] == 'user'
555 if filter[:format] == 'user'
525 if value.delete('me')
556 if value.delete('me')
526 value.push User.current.id.to_s
557 value.push User.current.id.to_s
527 end
558 end
528 end
559 end
529 not_in = nil
560 not_in = nil
530 if operator == '!'
561 if operator == '!'
531 # Makes ! operator work for custom fields with multiple values
562 # Makes ! operator work for custom fields with multiple values
532 operator = '='
563 operator = '='
533 not_in = 'NOT'
564 not_in = 'NOT'
534 end
565 end
535 customized_key = "id"
566 customized_key = "id"
536 customized_class = queried_class
567 customized_class = queried_class
537 if field =~ /^(.+)\.cf_/
568 if field =~ /^(.+)\.cf_/
538 assoc = $1
569 assoc = $1
539 customized_key = "#{assoc}_id"
570 customized_key = "#{assoc}_id"
540 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
571 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
541 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
572 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
542 end
573 end
543 "#{queried_table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " +
574 "#{queried_table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " +
544 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
575 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
545 end
576 end
546
577
547 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
578 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
548 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
579 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
549 sql = ''
580 sql = ''
550 case operator
581 case operator
551 when "="
582 when "="
552 if value.any?
583 if value.any?
553 case type_for(field)
584 case type_for(field)
554 when :date, :date_past
585 when :date, :date_past
555 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
586 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
556 when :integer
587 when :integer
557 if is_custom_filter
588 if is_custom_filter
558 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) = #{value.first.to_i})"
589 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) = #{value.first.to_i})"
559 else
590 else
560 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
591 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
561 end
592 end
562 when :float
593 when :float
563 if is_custom_filter
594 if is_custom_filter
564 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
595 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
565 else
596 else
566 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
597 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
567 end
598 end
568 else
599 else
569 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
600 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
570 end
601 end
571 else
602 else
572 # IN an empty set
603 # IN an empty set
573 sql = "1=0"
604 sql = "1=0"
574 end
605 end
575 when "!"
606 when "!"
576 if value.any?
607 if value.any?
577 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
608 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
578 else
609 else
579 # NOT IN an empty set
610 # NOT IN an empty set
580 sql = "1=1"
611 sql = "1=1"
581 end
612 end
582 when "!*"
613 when "!*"
583 sql = "#{db_table}.#{db_field} IS NULL"
614 sql = "#{db_table}.#{db_field} IS NULL"
584 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
615 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
585 when "*"
616 when "*"
586 sql = "#{db_table}.#{db_field} IS NOT NULL"
617 sql = "#{db_table}.#{db_field} IS NOT NULL"
587 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
618 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
588 when ">="
619 when ">="
589 if [:date, :date_past].include?(type_for(field))
620 if [:date, :date_past].include?(type_for(field))
590 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
621 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
591 else
622 else
592 if is_custom_filter
623 if is_custom_filter
593 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})"
624 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})"
594 else
625 else
595 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
626 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
596 end
627 end
597 end
628 end
598 when "<="
629 when "<="
599 if [:date, :date_past].include?(type_for(field))
630 if [:date, :date_past].include?(type_for(field))
600 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
631 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
601 else
632 else
602 if is_custom_filter
633 if is_custom_filter
603 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})"
634 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})"
604 else
635 else
605 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
636 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
606 end
637 end
607 end
638 end
608 when "><"
639 when "><"
609 if [:date, :date_past].include?(type_for(field))
640 if [:date, :date_past].include?(type_for(field))
610 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
641 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
611 else
642 else
612 if is_custom_filter
643 if is_custom_filter
613 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
644 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
614 else
645 else
615 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
646 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
616 end
647 end
617 end
648 end
618 when "o"
649 when "o"
619 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
650 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
620 when "c"
651 when "c"
621 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
652 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
622 when "><t-"
653 when "><t-"
623 # between today - n days and today
654 # between today - n days and today
624 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
655 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
625 when ">t-"
656 when ">t-"
626 # >= today - n days
657 # >= today - n days
627 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
658 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
628 when "<t-"
659 when "<t-"
629 # <= today - n days
660 # <= today - n days
630 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
661 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
631 when "t-"
662 when "t-"
632 # = n days in past
663 # = n days in past
633 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
664 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
634 when "><t+"
665 when "><t+"
635 # between today and today + n days
666 # between today and today + n days
636 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
667 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
637 when ">t+"
668 when ">t+"
638 # >= today + n days
669 # >= today + n days
639 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
670 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
640 when "<t+"
671 when "<t+"
641 # <= today + n days
672 # <= today + n days
642 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
673 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
643 when "t+"
674 when "t+"
644 # = today + n days
675 # = today + n days
645 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
676 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
646 when "t"
677 when "t"
647 # = today
678 # = today
648 sql = relative_date_clause(db_table, db_field, 0, 0)
679 sql = relative_date_clause(db_table, db_field, 0, 0)
649 when "ld"
680 when "ld"
650 # = yesterday
681 # = yesterday
651 sql = relative_date_clause(db_table, db_field, -1, -1)
682 sql = relative_date_clause(db_table, db_field, -1, -1)
652 when "w"
683 when "w"
653 # = this week
684 # = this week
654 first_day_of_week = l(:general_first_day_of_week).to_i
685 first_day_of_week = l(:general_first_day_of_week).to_i
655 day_of_week = Date.today.cwday
686 day_of_week = Date.today.cwday
656 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
687 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
657 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
688 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
658 when "lw"
689 when "lw"
659 # = last week
690 # = last week
660 first_day_of_week = l(:general_first_day_of_week).to_i
691 first_day_of_week = l(:general_first_day_of_week).to_i
661 day_of_week = Date.today.cwday
692 day_of_week = Date.today.cwday
662 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
693 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
663 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1)
694 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1)
664 when "l2w"
695 when "l2w"
665 # = last 2 weeks
696 # = last 2 weeks
666 first_day_of_week = l(:general_first_day_of_week).to_i
697 first_day_of_week = l(:general_first_day_of_week).to_i
667 day_of_week = Date.today.cwday
698 day_of_week = Date.today.cwday
668 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
699 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
669 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1)
700 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1)
670 when "m"
701 when "m"
671 # = this month
702 # = this month
672 date = Date.today
703 date = Date.today
673 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month)
704 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month)
674 when "lm"
705 when "lm"
675 # = last month
706 # = last month
676 date = Date.today.prev_month
707 date = Date.today.prev_month
677 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month)
708 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month)
678 when "y"
709 when "y"
679 # = this year
710 # = this year
680 date = Date.today
711 date = Date.today
681 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year)
712 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year)
682 when "~"
713 when "~"
683 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
714 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
684 when "!~"
715 when "!~"
685 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
716 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
686 else
717 else
687 raise "Unknown query operator #{operator}"
718 raise "Unknown query operator #{operator}"
688 end
719 end
689
720
690 return sql
721 return sql
691 end
722 end
692
723
693 def add_custom_fields_filters(custom_fields, assoc=nil)
724 def add_custom_fields_filters(custom_fields, assoc=nil)
694 return unless custom_fields.present?
725 return unless custom_fields.present?
695 @available_filters ||= {}
696
726
697 custom_fields.select(&:is_filter?).each do |field|
727 custom_fields.select(&:is_filter?).sort.each do |field|
698 case field.field_format
728 case field.field_format
699 when "text"
729 when "text"
700 options = { :type => :text, :order => 20 }
730 options = { :type => :text }
701 when "list"
731 when "list"
702 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
732 options = { :type => :list_optional, :values => field.possible_values }
703 when "date"
733 when "date"
704 options = { :type => :date, :order => 20 }
734 options = { :type => :date }
705 when "bool"
735 when "bool"
706 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
736 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]] }
707 when "int"
737 when "int"
708 options = { :type => :integer, :order => 20 }
738 options = { :type => :integer }
709 when "float"
739 when "float"
710 options = { :type => :float, :order => 20 }
740 options = { :type => :float }
711 when "user", "version"
741 when "user", "version"
712 next unless project
742 next unless project
713 values = field.possible_values_options(project)
743 values = field.possible_values_options(project)
714 if User.current.logged? && field.field_format == 'user'
744 if User.current.logged? && field.field_format == 'user'
715 values.unshift ["<< #{l(:label_me)} >>", "me"]
745 values.unshift ["<< #{l(:label_me)} >>", "me"]
716 end
746 end
717 options = { :type => :list_optional, :values => values, :order => 20}
747 options = { :type => :list_optional, :values => values }
718 else
748 else
719 options = { :type => :string, :order => 20 }
749 options = { :type => :string }
720 end
750 end
721 filter_id = "cf_#{field.id}"
751 filter_id = "cf_#{field.id}"
722 filter_name = field.name
752 filter_name = field.name
723 if assoc.present?
753 if assoc.present?
724 filter_id = "#{assoc}.#{filter_id}"
754 filter_id = "#{assoc}.#{filter_id}"
725 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
755 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
726 end
756 end
727 @available_filters[filter_id] = options.merge({
757 add_available_filter filter_id, options.merge({
728 :name => filter_name,
758 :name => filter_name,
729 :format => field.field_format,
759 :format => field.field_format,
730 :field => field
760 :field => field
731 })
761 })
732 end
762 end
733 end
763 end
734
764
735 def add_associations_custom_fields_filters(*associations)
765 def add_associations_custom_fields_filters(*associations)
736 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
766 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
737 associations.each do |assoc|
767 associations.each do |assoc|
738 association_klass = queried_class.reflect_on_association(assoc).klass
768 association_klass = queried_class.reflect_on_association(assoc).klass
739 fields_by_class.each do |field_class, fields|
769 fields_by_class.each do |field_class, fields|
740 if field_class.customized_class <= association_klass
770 if field_class.customized_class <= association_klass
741 add_custom_fields_filters(fields, assoc)
771 add_custom_fields_filters(fields, assoc)
742 end
772 end
743 end
773 end
744 end
774 end
745 end
775 end
746
776
747 # Returns a SQL clause for a date or datetime field.
777 # Returns a SQL clause for a date or datetime field.
748 def date_clause(table, field, from, to)
778 def date_clause(table, field, from, to)
749 s = []
779 s = []
750 if from
780 if from
751 from_yesterday = from - 1
781 from_yesterday = from - 1
752 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
782 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
753 if self.class.default_timezone == :utc
783 if self.class.default_timezone == :utc
754 from_yesterday_time = from_yesterday_time.utc
784 from_yesterday_time = from_yesterday_time.utc
755 end
785 end
756 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
786 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
757 end
787 end
758 if to
788 if to
759 to_time = Time.local(to.year, to.month, to.day)
789 to_time = Time.local(to.year, to.month, to.day)
760 if self.class.default_timezone == :utc
790 if self.class.default_timezone == :utc
761 to_time = to_time.utc
791 to_time = to_time.utc
762 end
792 end
763 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
793 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
764 end
794 end
765 s.join(' AND ')
795 s.join(' AND ')
766 end
796 end
767
797
768 # Returns a SQL clause for a date or datetime field using relative dates.
798 # Returns a SQL clause for a date or datetime field using relative dates.
769 def relative_date_clause(table, field, days_from, days_to)
799 def relative_date_clause(table, field, days_from, days_to)
770 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
800 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
771 end
801 end
772
802
773 # Additional joins required for the given sort options
803 # Additional joins required for the given sort options
774 def joins_for_order_statement(order_options)
804 def joins_for_order_statement(order_options)
775 joins = []
805 joins = []
776
806
777 if order_options
807 if order_options
778 if order_options.include?('authors')
808 if order_options.include?('authors')
779 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
809 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
780 end
810 end
781 order_options.scan(/cf_\d+/).uniq.each do |name|
811 order_options.scan(/cf_\d+/).uniq.each do |name|
782 column = available_columns.detect {|c| c.name.to_s == name}
812 column = available_columns.detect {|c| c.name.to_s == name}
783 join = column && column.custom_field.join_for_order_statement
813 join = column && column.custom_field.join_for_order_statement
784 if join
814 if join
785 joins << join
815 joins << join
786 end
816 end
787 end
817 end
788 end
818 end
789
819
790 joins.any? ? joins.join(' ') : nil
820 joins.any? ? joins.join(' ') : nil
791 end
821 end
792 end
822 end
@@ -1,123 +1,115
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class TimeEntryQuery < Query
18 class TimeEntryQuery < Query
19
19
20 self.queried_class = TimeEntry
20 self.queried_class = TimeEntry
21
21
22 self.available_columns = [
22 self.available_columns = [
23 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
23 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
24 QueryColumn.new(:spent_on, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"], :default_order => 'desc', :groupable => true),
24 QueryColumn.new(:spent_on, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"], :default_order => 'desc', :groupable => true),
25 QueryColumn.new(:user, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
25 QueryColumn.new(:user, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
26 QueryColumn.new(:activity, :sortable => "#{TimeEntryActivity.table_name}.position", :groupable => true),
26 QueryColumn.new(:activity, :sortable => "#{TimeEntryActivity.table_name}.position", :groupable => true),
27 QueryColumn.new(:issue, :sortable => "#{Issue.table_name}.id"),
27 QueryColumn.new(:issue, :sortable => "#{Issue.table_name}.id"),
28 QueryColumn.new(:comments),
28 QueryColumn.new(:comments),
29 QueryColumn.new(:hours, :sortable => "#{TimeEntry.table_name}.hours"),
29 QueryColumn.new(:hours, :sortable => "#{TimeEntry.table_name}.hours"),
30 ]
30 ]
31
31
32 def initialize(attributes=nil, *args)
32 def initialize(attributes=nil, *args)
33 super attributes
33 super attributes
34 self.filters ||= {}
34 self.filters ||= {}
35 add_filter('spent_on', '*') unless filters.present?
35 add_filter('spent_on', '*') unless filters.present?
36 end
36 end
37
37
38 def available_filters
38 def initialize_available_filters
39 return @available_filters if @available_filters
39 add_available_filter "spent_on", :type => :date_past
40 @available_filters = {
41 "spent_on" => { :type => :date_past, :order => 0 },
42 "comments" => { :type => :text, :order => 5 },
43 "hours" => { :type => :float, :order => 6 }
44 }
45
40
46 principals = []
41 principals = []
47 if project
42 if project
48 principals += project.principals.sort
43 principals += project.principals.sort
49 unless project.leaf?
44 unless project.leaf?
50 subprojects = project.descendants.visible.all
45 subprojects = project.descendants.visible.all
51 if subprojects.any?
46 if subprojects.any?
52 @available_filters["subproject_id"] = {
47 add_available_filter "subproject_id",
53 :type => :list_subprojects, :order => 1,
48 :type => :list_subprojects,
54 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
49 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
55 }
56 principals += Principal.member_of(subprojects)
50 principals += Principal.member_of(subprojects)
57 end
51 end
58 end
52 end
59 else
53 else
60 if all_projects.any?
54 if all_projects.any?
61 # members of visible projects
55 # members of visible projects
62 principals += Principal.member_of(all_projects)
56 principals += Principal.member_of(all_projects)
63 # project filter
57 # project filter
64 project_values = []
58 project_values = []
65 if User.current.logged? && User.current.memberships.any?
59 if User.current.logged? && User.current.memberships.any?
66 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
60 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
67 end
61 end
68 project_values += all_projects_values
62 project_values += all_projects_values
69 @available_filters["project_id"] = {
63 add_available_filter("project_id",
70 :type => :list, :order => 1, :values => project_values
64 :type => :list, :values => project_values
71 } unless project_values.empty?
65 ) unless project_values.empty?
72 end
66 end
73 end
67 end
74 principals.uniq!
68 principals.uniq!
75 principals.sort!
69 principals.sort!
76 users = principals.select {|p| p.is_a?(User)}
70 users = principals.select {|p| p.is_a?(User)}
77
71
78 users_values = []
72 users_values = []
79 users_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
73 users_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
80 users_values += users.collect{|s| [s.name, s.id.to_s] }
74 users_values += users.collect{|s| [s.name, s.id.to_s] }
81 @available_filters["user_id"] = {
75 add_available_filter("user_id",
82 :type => :list_optional, :order => 2, :values => users_values
76 :type => :list_optional, :values => users_values
83 } unless users_values.empty?
77 ) unless users_values.empty?
84
78
85 activities = (project ? project.activities : TimeEntryActivity.shared.active)
79 activities = (project ? project.activities : TimeEntryActivity.shared.active)
86 @available_filters["activity_id"] = {
80 add_available_filter("activity_id",
87 :type => :list, :order => 3, :values => activities.map {|a| [a.name, a.id.to_s]}
81 :type => :list, :values => activities.map {|a| [a.name, a.id.to_s]}
88 } unless activities.empty?
82 ) unless activities.empty?
83
84 add_available_filter "comments", :type => :text
85 add_available_filter "hours", :type => :float
89
86
90 add_custom_fields_filters(TimeEntryCustomField.where(:is_filter => true).all)
87 add_custom_fields_filters(TimeEntryCustomField.where(:is_filter => true).all)
91 add_associations_custom_fields_filters :project, :issue, :user
88 add_associations_custom_fields_filters :project, :issue, :user
92
93 @available_filters.each do |field, options|
94 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
95 end
96 @available_filters
97 end
89 end
98
90
99 def available_columns
91 def available_columns
100 return @available_columns if @available_columns
92 return @available_columns if @available_columns
101 @available_columns = self.class.available_columns.dup
93 @available_columns = self.class.available_columns.dup
102 @available_columns += TimeEntryCustomField.all.map {|cf| QueryCustomFieldColumn.new(cf) }
94 @available_columns += TimeEntryCustomField.all.map {|cf| QueryCustomFieldColumn.new(cf) }
103 @available_columns += IssueCustomField.all.map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf) }
95 @available_columns += IssueCustomField.all.map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf) }
104 @available_columns
96 @available_columns
105 end
97 end
106
98
107 def default_columns_names
99 def default_columns_names
108 @default_columns_names ||= [:project, :spent_on, :user, :activity, :issue, :comments, :hours]
100 @default_columns_names ||= [:project, :spent_on, :user, :activity, :issue, :comments, :hours]
109 end
101 end
110
102
111 # Accepts :from/:to params as shortcut filters
103 # Accepts :from/:to params as shortcut filters
112 def build_from_params(params)
104 def build_from_params(params)
113 super
105 super
114 if params[:from].present? && params[:to].present?
106 if params[:from].present? && params[:to].present?
115 add_filter('spent_on', '><', [params[:from], params[:to]])
107 add_filter('spent_on', '><', [params[:from], params[:to]])
116 elsif params[:from].present?
108 elsif params[:from].present?
117 add_filter('spent_on', '>=', [params[:from]])
109 add_filter('spent_on', '>=', [params[:from]])
118 elsif params[:to].present?
110 elsif params[:to].present?
119 add_filter('spent_on', '<=', [params[:to]])
111 add_filter('spent_on', '<=', [params[:to]])
120 end
112 end
121 self
113 self
122 end
114 end
123 end
115 end
@@ -1,65 +1,64
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class QueriesHelperTest < ActionView::TestCase
20 class QueriesHelperTest < ActionView::TestCase
21 include QueriesHelper
21 include QueriesHelper
22 include Redmine::I18n
22 include Redmine::I18n
23
23
24 fixtures :projects, :enabled_modules, :users, :members,
24 fixtures :projects, :enabled_modules, :users, :members,
25 :member_roles, :roles, :trackers, :issue_statuses,
25 :member_roles, :roles, :trackers, :issue_statuses,
26 :issue_categories, :enumerations, :issues,
26 :issue_categories, :enumerations, :issues,
27 :watchers, :custom_fields, :custom_values, :versions,
27 :watchers, :custom_fields, :custom_values, :versions,
28 :queries,
28 :queries,
29 :projects_trackers,
29 :projects_trackers,
30 :custom_fields_trackers
30 :custom_fields_trackers
31
31
32 def test_order
32 def test_filters_options_should_be_ordered
33 User.current = User.find_by_login('admin')
33 User.current = User.find_by_login('admin')
34 query = IssueQuery.new(:project => nil, :name => '_')
34 query = IssueQuery.new(:project => nil, :name => '_')
35 assert_equal 30, query.available_filters.size
35 assert_equal 30, query.available_filters.size
36 fo = filters_options(query)
36 fo = filters_options(query)
37 assert_equal 31, fo.size
37 assert_equal 31, fo.size
38 assert_equal [], fo[0]
38 assert_equal [], fo[0]
39 assert_equal "status_id", fo[1][1]
39 assert_equal "status_id", fo[1][1]
40 assert_equal "project_id", fo[2][1]
40 assert_equal "project_id", fo[2][1]
41 assert_equal "tracker_id", fo[3][1]
41 assert_equal "tracker_id", fo[3][1]
42 assert_equal "priority_id", fo[4][1]
42 assert_equal "priority_id", fo[4][1]
43 assert_equal "watcher_id", fo[17][1]
43 assert_equal "is_private", fo[17][1]
44 assert_equal "is_private", fo[18][1]
44 assert_equal "watcher_id", fo[18][1]
45 end
45 end
46
46
47 def test_order_custom_fields
47 def test_filters_options_should_be_ordered_with_custom_fields
48 set_language_if_valid 'en'
48 set_language_if_valid 'en'
49 field = UserCustomField.new(
49 field = UserCustomField.create!(
50 :name => 'order test', :field_format => 'string',
50 :name => 'order test', :field_format => 'string',
51 :is_for_all => true, :is_filter => true
51 :is_for_all => true, :is_filter => true
52 )
52 )
53 assert field.save
54 User.current = User.find_by_login('admin')
53 User.current = User.find_by_login('admin')
55 query = IssueQuery.new(:project => nil, :name => '_')
54 query = IssueQuery.new(:project => nil, :name => '_')
56 assert_equal 32, query.available_filters.size
55 assert_equal 32, query.available_filters.size
57 fo = filters_options(query)
56 fo = filters_options(query)
58 assert_equal 33, fo.size
57 assert_equal 33, fo.size
59 assert_equal "Searchable field", fo[19][0]
58 assert_equal "Searchable field", fo[19][0]
60 assert_equal "Database", fo[20][0]
59 assert_equal "Database", fo[20][0]
61 assert_equal "Project's Development status", fo[21][0]
60 assert_equal "Project's Development status", fo[21][0]
62 assert_equal "Assignee's order test", fo[22][0]
61 assert_equal "Author's order test", fo[22][0]
63 assert_equal "Author's order test", fo[23][0]
62 assert_equal "Assignee's order test", fo[23][0]
64 end
63 end
65 end
64 end
@@ -1,1211 +1,1216
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class QueryTest < ActiveSupport::TestCase
20 class QueryTest < ActiveSupport::TestCase
21 include Redmine::I18n
21 include Redmine::I18n
22
22
23 fixtures :projects, :enabled_modules, :users, :members,
23 fixtures :projects, :enabled_modules, :users, :members,
24 :member_roles, :roles, :trackers, :issue_statuses,
24 :member_roles, :roles, :trackers, :issue_statuses,
25 :issue_categories, :enumerations, :issues,
25 :issue_categories, :enumerations, :issues,
26 :watchers, :custom_fields, :custom_values, :versions,
26 :watchers, :custom_fields, :custom_values, :versions,
27 :queries,
27 :queries,
28 :projects_trackers,
28 :projects_trackers,
29 :custom_fields_trackers
29 :custom_fields_trackers
30
30
31 def test_available_filters_should_be_ordered
32 query = IssueQuery.new
33 assert_equal 0, query.available_filters.keys.index('status_id')
34 end
35
31 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
36 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
32 query = IssueQuery.new(:project => nil, :name => '_')
37 query = IssueQuery.new(:project => nil, :name => '_')
33 assert query.available_filters.has_key?('cf_1')
38 assert query.available_filters.has_key?('cf_1')
34 assert !query.available_filters.has_key?('cf_3')
39 assert !query.available_filters.has_key?('cf_3')
35 end
40 end
36
41
37 def test_system_shared_versions_should_be_available_in_global_queries
42 def test_system_shared_versions_should_be_available_in_global_queries
38 Version.find(2).update_attribute :sharing, 'system'
43 Version.find(2).update_attribute :sharing, 'system'
39 query = IssueQuery.new(:project => nil, :name => '_')
44 query = IssueQuery.new(:project => nil, :name => '_')
40 assert query.available_filters.has_key?('fixed_version_id')
45 assert query.available_filters.has_key?('fixed_version_id')
41 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
46 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
42 end
47 end
43
48
44 def test_project_filter_in_global_queries
49 def test_project_filter_in_global_queries
45 query = IssueQuery.new(:project => nil, :name => '_')
50 query = IssueQuery.new(:project => nil, :name => '_')
46 project_filter = query.available_filters["project_id"]
51 project_filter = query.available_filters["project_id"]
47 assert_not_nil project_filter
52 assert_not_nil project_filter
48 project_ids = project_filter[:values].map{|p| p[1]}
53 project_ids = project_filter[:values].map{|p| p[1]}
49 assert project_ids.include?("1") #public project
54 assert project_ids.include?("1") #public project
50 assert !project_ids.include?("2") #private project user cannot see
55 assert !project_ids.include?("2") #private project user cannot see
51 end
56 end
52
57
53 def find_issues_with_query(query)
58 def find_issues_with_query(query)
54 Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
59 Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
55 query.statement
60 query.statement
56 ).all
61 ).all
57 end
62 end
58
63
59 def assert_find_issues_with_query_is_successful(query)
64 def assert_find_issues_with_query_is_successful(query)
60 assert_nothing_raised do
65 assert_nothing_raised do
61 find_issues_with_query(query)
66 find_issues_with_query(query)
62 end
67 end
63 end
68 end
64
69
65 def assert_query_statement_includes(query, condition)
70 def assert_query_statement_includes(query, condition)
66 assert_include condition, query.statement
71 assert_include condition, query.statement
67 end
72 end
68
73
69 def assert_query_result(expected, query)
74 def assert_query_result(expected, query)
70 assert_nothing_raised do
75 assert_nothing_raised do
71 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
76 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
72 assert_equal expected.size, query.issue_count
77 assert_equal expected.size, query.issue_count
73 end
78 end
74 end
79 end
75
80
76 def test_query_should_allow_shared_versions_for_a_project_query
81 def test_query_should_allow_shared_versions_for_a_project_query
77 subproject_version = Version.find(4)
82 subproject_version = Version.find(4)
78 query = IssueQuery.new(:project => Project.find(1), :name => '_')
83 query = IssueQuery.new(:project => Project.find(1), :name => '_')
79 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
84 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
80
85
81 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
86 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
82 end
87 end
83
88
84 def test_query_with_multiple_custom_fields
89 def test_query_with_multiple_custom_fields
85 query = IssueQuery.find(1)
90 query = IssueQuery.find(1)
86 assert query.valid?
91 assert query.valid?
87 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
92 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
88 issues = find_issues_with_query(query)
93 issues = find_issues_with_query(query)
89 assert_equal 1, issues.length
94 assert_equal 1, issues.length
90 assert_equal Issue.find(3), issues.first
95 assert_equal Issue.find(3), issues.first
91 end
96 end
92
97
93 def test_operator_none
98 def test_operator_none
94 query = IssueQuery.new(:project => Project.find(1), :name => '_')
99 query = IssueQuery.new(:project => Project.find(1), :name => '_')
95 query.add_filter('fixed_version_id', '!*', [''])
100 query.add_filter('fixed_version_id', '!*', [''])
96 query.add_filter('cf_1', '!*', [''])
101 query.add_filter('cf_1', '!*', [''])
97 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
102 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
98 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
103 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
99 find_issues_with_query(query)
104 find_issues_with_query(query)
100 end
105 end
101
106
102 def test_operator_none_for_integer
107 def test_operator_none_for_integer
103 query = IssueQuery.new(:project => Project.find(1), :name => '_')
108 query = IssueQuery.new(:project => Project.find(1), :name => '_')
104 query.add_filter('estimated_hours', '!*', [''])
109 query.add_filter('estimated_hours', '!*', [''])
105 issues = find_issues_with_query(query)
110 issues = find_issues_with_query(query)
106 assert !issues.empty?
111 assert !issues.empty?
107 assert issues.all? {|i| !i.estimated_hours}
112 assert issues.all? {|i| !i.estimated_hours}
108 end
113 end
109
114
110 def test_operator_none_for_date
115 def test_operator_none_for_date
111 query = IssueQuery.new(:project => Project.find(1), :name => '_')
116 query = IssueQuery.new(:project => Project.find(1), :name => '_')
112 query.add_filter('start_date', '!*', [''])
117 query.add_filter('start_date', '!*', [''])
113 issues = find_issues_with_query(query)
118 issues = find_issues_with_query(query)
114 assert !issues.empty?
119 assert !issues.empty?
115 assert issues.all? {|i| i.start_date.nil?}
120 assert issues.all? {|i| i.start_date.nil?}
116 end
121 end
117
122
118 def test_operator_none_for_string_custom_field
123 def test_operator_none_for_string_custom_field
119 query = IssueQuery.new(:project => Project.find(1), :name => '_')
124 query = IssueQuery.new(:project => Project.find(1), :name => '_')
120 query.add_filter('cf_2', '!*', [''])
125 query.add_filter('cf_2', '!*', [''])
121 assert query.has_filter?('cf_2')
126 assert query.has_filter?('cf_2')
122 issues = find_issues_with_query(query)
127 issues = find_issues_with_query(query)
123 assert !issues.empty?
128 assert !issues.empty?
124 assert issues.all? {|i| i.custom_field_value(2).blank?}
129 assert issues.all? {|i| i.custom_field_value(2).blank?}
125 end
130 end
126
131
127 def test_operator_all
132 def test_operator_all
128 query = IssueQuery.new(:project => Project.find(1), :name => '_')
133 query = IssueQuery.new(:project => Project.find(1), :name => '_')
129 query.add_filter('fixed_version_id', '*', [''])
134 query.add_filter('fixed_version_id', '*', [''])
130 query.add_filter('cf_1', '*', [''])
135 query.add_filter('cf_1', '*', [''])
131 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
136 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
132 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
137 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
133 find_issues_with_query(query)
138 find_issues_with_query(query)
134 end
139 end
135
140
136 def test_operator_all_for_date
141 def test_operator_all_for_date
137 query = IssueQuery.new(:project => Project.find(1), :name => '_')
142 query = IssueQuery.new(:project => Project.find(1), :name => '_')
138 query.add_filter('start_date', '*', [''])
143 query.add_filter('start_date', '*', [''])
139 issues = find_issues_with_query(query)
144 issues = find_issues_with_query(query)
140 assert !issues.empty?
145 assert !issues.empty?
141 assert issues.all? {|i| i.start_date.present?}
146 assert issues.all? {|i| i.start_date.present?}
142 end
147 end
143
148
144 def test_operator_all_for_string_custom_field
149 def test_operator_all_for_string_custom_field
145 query = IssueQuery.new(:project => Project.find(1), :name => '_')
150 query = IssueQuery.new(:project => Project.find(1), :name => '_')
146 query.add_filter('cf_2', '*', [''])
151 query.add_filter('cf_2', '*', [''])
147 assert query.has_filter?('cf_2')
152 assert query.has_filter?('cf_2')
148 issues = find_issues_with_query(query)
153 issues = find_issues_with_query(query)
149 assert !issues.empty?
154 assert !issues.empty?
150 assert issues.all? {|i| i.custom_field_value(2).present?}
155 assert issues.all? {|i| i.custom_field_value(2).present?}
151 end
156 end
152
157
153 def test_numeric_filter_should_not_accept_non_numeric_values
158 def test_numeric_filter_should_not_accept_non_numeric_values
154 query = IssueQuery.new(:name => '_')
159 query = IssueQuery.new(:name => '_')
155 query.add_filter('estimated_hours', '=', ['a'])
160 query.add_filter('estimated_hours', '=', ['a'])
156
161
157 assert query.has_filter?('estimated_hours')
162 assert query.has_filter?('estimated_hours')
158 assert !query.valid?
163 assert !query.valid?
159 end
164 end
160
165
161 def test_operator_is_on_float
166 def test_operator_is_on_float
162 Issue.update_all("estimated_hours = 171.2", "id=2")
167 Issue.update_all("estimated_hours = 171.2", "id=2")
163
168
164 query = IssueQuery.new(:name => '_')
169 query = IssueQuery.new(:name => '_')
165 query.add_filter('estimated_hours', '=', ['171.20'])
170 query.add_filter('estimated_hours', '=', ['171.20'])
166 issues = find_issues_with_query(query)
171 issues = find_issues_with_query(query)
167 assert_equal 1, issues.size
172 assert_equal 1, issues.size
168 assert_equal 2, issues.first.id
173 assert_equal 2, issues.first.id
169 end
174 end
170
175
171 def test_operator_is_on_integer_custom_field
176 def test_operator_is_on_integer_custom_field
172 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
177 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
173 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
178 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
174 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
179 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
175 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
180 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
176
181
177 query = IssueQuery.new(:name => '_')
182 query = IssueQuery.new(:name => '_')
178 query.add_filter("cf_#{f.id}", '=', ['12'])
183 query.add_filter("cf_#{f.id}", '=', ['12'])
179 issues = find_issues_with_query(query)
184 issues = find_issues_with_query(query)
180 assert_equal 1, issues.size
185 assert_equal 1, issues.size
181 assert_equal 2, issues.first.id
186 assert_equal 2, issues.first.id
182 end
187 end
183
188
184 def test_operator_is_on_integer_custom_field_should_accept_negative_value
189 def test_operator_is_on_integer_custom_field_should_accept_negative_value
185 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
190 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
186 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
191 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
187 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12')
192 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12')
188 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
193 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
189
194
190 query = IssueQuery.new(:name => '_')
195 query = IssueQuery.new(:name => '_')
191 query.add_filter("cf_#{f.id}", '=', ['-12'])
196 query.add_filter("cf_#{f.id}", '=', ['-12'])
192 assert query.valid?
197 assert query.valid?
193 issues = find_issues_with_query(query)
198 issues = find_issues_with_query(query)
194 assert_equal 1, issues.size
199 assert_equal 1, issues.size
195 assert_equal 2, issues.first.id
200 assert_equal 2, issues.first.id
196 end
201 end
197
202
198 def test_operator_is_on_float_custom_field
203 def test_operator_is_on_float_custom_field
199 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
204 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
200 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
205 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
201 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
206 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
202 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
207 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
203
208
204 query = IssueQuery.new(:name => '_')
209 query = IssueQuery.new(:name => '_')
205 query.add_filter("cf_#{f.id}", '=', ['12.7'])
210 query.add_filter("cf_#{f.id}", '=', ['12.7'])
206 issues = find_issues_with_query(query)
211 issues = find_issues_with_query(query)
207 assert_equal 1, issues.size
212 assert_equal 1, issues.size
208 assert_equal 2, issues.first.id
213 assert_equal 2, issues.first.id
209 end
214 end
210
215
211 def test_operator_is_on_float_custom_field_should_accept_negative_value
216 def test_operator_is_on_float_custom_field_should_accept_negative_value
212 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
217 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
213 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
218 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
214 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7')
219 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7')
215 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
220 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
216
221
217 query = IssueQuery.new(:name => '_')
222 query = IssueQuery.new(:name => '_')
218 query.add_filter("cf_#{f.id}", '=', ['-12.7'])
223 query.add_filter("cf_#{f.id}", '=', ['-12.7'])
219 assert query.valid?
224 assert query.valid?
220 issues = find_issues_with_query(query)
225 issues = find_issues_with_query(query)
221 assert_equal 1, issues.size
226 assert_equal 1, issues.size
222 assert_equal 2, issues.first.id
227 assert_equal 2, issues.first.id
223 end
228 end
224
229
225 def test_operator_is_on_multi_list_custom_field
230 def test_operator_is_on_multi_list_custom_field
226 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
231 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
227 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
232 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
228 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
233 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
229 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
234 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
230 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
235 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
231
236
232 query = IssueQuery.new(:name => '_')
237 query = IssueQuery.new(:name => '_')
233 query.add_filter("cf_#{f.id}", '=', ['value1'])
238 query.add_filter("cf_#{f.id}", '=', ['value1'])
234 issues = find_issues_with_query(query)
239 issues = find_issues_with_query(query)
235 assert_equal [1, 3], issues.map(&:id).sort
240 assert_equal [1, 3], issues.map(&:id).sort
236
241
237 query = IssueQuery.new(:name => '_')
242 query = IssueQuery.new(:name => '_')
238 query.add_filter("cf_#{f.id}", '=', ['value2'])
243 query.add_filter("cf_#{f.id}", '=', ['value2'])
239 issues = find_issues_with_query(query)
244 issues = find_issues_with_query(query)
240 assert_equal [1], issues.map(&:id).sort
245 assert_equal [1], issues.map(&:id).sort
241 end
246 end
242
247
243 def test_operator_is_not_on_multi_list_custom_field
248 def test_operator_is_not_on_multi_list_custom_field
244 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
249 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
245 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
250 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
246 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
251 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
247 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
252 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
248 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
253 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
249
254
250 query = IssueQuery.new(:name => '_')
255 query = IssueQuery.new(:name => '_')
251 query.add_filter("cf_#{f.id}", '!', ['value1'])
256 query.add_filter("cf_#{f.id}", '!', ['value1'])
252 issues = find_issues_with_query(query)
257 issues = find_issues_with_query(query)
253 assert !issues.map(&:id).include?(1)
258 assert !issues.map(&:id).include?(1)
254 assert !issues.map(&:id).include?(3)
259 assert !issues.map(&:id).include?(3)
255
260
256 query = IssueQuery.new(:name => '_')
261 query = IssueQuery.new(:name => '_')
257 query.add_filter("cf_#{f.id}", '!', ['value2'])
262 query.add_filter("cf_#{f.id}", '!', ['value2'])
258 issues = find_issues_with_query(query)
263 issues = find_issues_with_query(query)
259 assert !issues.map(&:id).include?(1)
264 assert !issues.map(&:id).include?(1)
260 assert issues.map(&:id).include?(3)
265 assert issues.map(&:id).include?(3)
261 end
266 end
262
267
263 def test_operator_is_on_is_private_field
268 def test_operator_is_on_is_private_field
264 # is_private filter only available for those who can set issues private
269 # is_private filter only available for those who can set issues private
265 User.current = User.find(2)
270 User.current = User.find(2)
266
271
267 query = IssueQuery.new(:name => '_')
272 query = IssueQuery.new(:name => '_')
268 assert query.available_filters.key?('is_private')
273 assert query.available_filters.key?('is_private')
269
274
270 query.add_filter("is_private", '=', ['1'])
275 query.add_filter("is_private", '=', ['1'])
271 issues = find_issues_with_query(query)
276 issues = find_issues_with_query(query)
272 assert issues.any?
277 assert issues.any?
273 assert_nil issues.detect {|issue| !issue.is_private?}
278 assert_nil issues.detect {|issue| !issue.is_private?}
274 ensure
279 ensure
275 User.current = nil
280 User.current = nil
276 end
281 end
277
282
278 def test_operator_is_not_on_is_private_field
283 def test_operator_is_not_on_is_private_field
279 # is_private filter only available for those who can set issues private
284 # is_private filter only available for those who can set issues private
280 User.current = User.find(2)
285 User.current = User.find(2)
281
286
282 query = IssueQuery.new(:name => '_')
287 query = IssueQuery.new(:name => '_')
283 assert query.available_filters.key?('is_private')
288 assert query.available_filters.key?('is_private')
284
289
285 query.add_filter("is_private", '!', ['1'])
290 query.add_filter("is_private", '!', ['1'])
286 issues = find_issues_with_query(query)
291 issues = find_issues_with_query(query)
287 assert issues.any?
292 assert issues.any?
288 assert_nil issues.detect {|issue| issue.is_private?}
293 assert_nil issues.detect {|issue| issue.is_private?}
289 ensure
294 ensure
290 User.current = nil
295 User.current = nil
291 end
296 end
292
297
293 def test_operator_greater_than
298 def test_operator_greater_than
294 query = IssueQuery.new(:project => Project.find(1), :name => '_')
299 query = IssueQuery.new(:project => Project.find(1), :name => '_')
295 query.add_filter('done_ratio', '>=', ['40'])
300 query.add_filter('done_ratio', '>=', ['40'])
296 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
301 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
297 find_issues_with_query(query)
302 find_issues_with_query(query)
298 end
303 end
299
304
300 def test_operator_greater_than_a_float
305 def test_operator_greater_than_a_float
301 query = IssueQuery.new(:project => Project.find(1), :name => '_')
306 query = IssueQuery.new(:project => Project.find(1), :name => '_')
302 query.add_filter('estimated_hours', '>=', ['40.5'])
307 query.add_filter('estimated_hours', '>=', ['40.5'])
303 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
308 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
304 find_issues_with_query(query)
309 find_issues_with_query(query)
305 end
310 end
306
311
307 def test_operator_greater_than_on_int_custom_field
312 def test_operator_greater_than_on_int_custom_field
308 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
313 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
309 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
314 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
310 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
315 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
311 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
316 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
312
317
313 query = IssueQuery.new(:project => Project.find(1), :name => '_')
318 query = IssueQuery.new(:project => Project.find(1), :name => '_')
314 query.add_filter("cf_#{f.id}", '>=', ['8'])
319 query.add_filter("cf_#{f.id}", '>=', ['8'])
315 issues = find_issues_with_query(query)
320 issues = find_issues_with_query(query)
316 assert_equal 1, issues.size
321 assert_equal 1, issues.size
317 assert_equal 2, issues.first.id
322 assert_equal 2, issues.first.id
318 end
323 end
319
324
320 def test_operator_lesser_than
325 def test_operator_lesser_than
321 query = IssueQuery.new(:project => Project.find(1), :name => '_')
326 query = IssueQuery.new(:project => Project.find(1), :name => '_')
322 query.add_filter('done_ratio', '<=', ['30'])
327 query.add_filter('done_ratio', '<=', ['30'])
323 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
328 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
324 find_issues_with_query(query)
329 find_issues_with_query(query)
325 end
330 end
326
331
327 def test_operator_lesser_than_on_custom_field
332 def test_operator_lesser_than_on_custom_field
328 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
333 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
329 query = IssueQuery.new(:project => Project.find(1), :name => '_')
334 query = IssueQuery.new(:project => Project.find(1), :name => '_')
330 query.add_filter("cf_#{f.id}", '<=', ['30'])
335 query.add_filter("cf_#{f.id}", '<=', ['30'])
331 assert_match /CAST.+ <= 30\.0/, query.statement
336 assert_match /CAST.+ <= 30\.0/, query.statement
332 find_issues_with_query(query)
337 find_issues_with_query(query)
333 end
338 end
334
339
335 def test_operator_between
340 def test_operator_between
336 query = IssueQuery.new(:project => Project.find(1), :name => '_')
341 query = IssueQuery.new(:project => Project.find(1), :name => '_')
337 query.add_filter('done_ratio', '><', ['30', '40'])
342 query.add_filter('done_ratio', '><', ['30', '40'])
338 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
343 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
339 find_issues_with_query(query)
344 find_issues_with_query(query)
340 end
345 end
341
346
342 def test_operator_between_on_custom_field
347 def test_operator_between_on_custom_field
343 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
348 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
344 query = IssueQuery.new(:project => Project.find(1), :name => '_')
349 query = IssueQuery.new(:project => Project.find(1), :name => '_')
345 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
350 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
346 assert_match /CAST.+ BETWEEN 30.0 AND 40.0/, query.statement
351 assert_match /CAST.+ BETWEEN 30.0 AND 40.0/, query.statement
347 find_issues_with_query(query)
352 find_issues_with_query(query)
348 end
353 end
349
354
350 def test_date_filter_should_not_accept_non_date_values
355 def test_date_filter_should_not_accept_non_date_values
351 query = IssueQuery.new(:name => '_')
356 query = IssueQuery.new(:name => '_')
352 query.add_filter('created_on', '=', ['a'])
357 query.add_filter('created_on', '=', ['a'])
353
358
354 assert query.has_filter?('created_on')
359 assert query.has_filter?('created_on')
355 assert !query.valid?
360 assert !query.valid?
356 end
361 end
357
362
358 def test_date_filter_should_not_accept_invalid_date_values
363 def test_date_filter_should_not_accept_invalid_date_values
359 query = IssueQuery.new(:name => '_')
364 query = IssueQuery.new(:name => '_')
360 query.add_filter('created_on', '=', ['2011-01-34'])
365 query.add_filter('created_on', '=', ['2011-01-34'])
361
366
362 assert query.has_filter?('created_on')
367 assert query.has_filter?('created_on')
363 assert !query.valid?
368 assert !query.valid?
364 end
369 end
365
370
366 def test_relative_date_filter_should_not_accept_non_integer_values
371 def test_relative_date_filter_should_not_accept_non_integer_values
367 query = IssueQuery.new(:name => '_')
372 query = IssueQuery.new(:name => '_')
368 query.add_filter('created_on', '>t-', ['a'])
373 query.add_filter('created_on', '>t-', ['a'])
369
374
370 assert query.has_filter?('created_on')
375 assert query.has_filter?('created_on')
371 assert !query.valid?
376 assert !query.valid?
372 end
377 end
373
378
374 def test_operator_date_equals
379 def test_operator_date_equals
375 query = IssueQuery.new(:name => '_')
380 query = IssueQuery.new(:name => '_')
376 query.add_filter('due_date', '=', ['2011-07-10'])
381 query.add_filter('due_date', '=', ['2011-07-10'])
377 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
382 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
378 find_issues_with_query(query)
383 find_issues_with_query(query)
379 end
384 end
380
385
381 def test_operator_date_lesser_than
386 def test_operator_date_lesser_than
382 query = IssueQuery.new(:name => '_')
387 query = IssueQuery.new(:name => '_')
383 query.add_filter('due_date', '<=', ['2011-07-10'])
388 query.add_filter('due_date', '<=', ['2011-07-10'])
384 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
389 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
385 find_issues_with_query(query)
390 find_issues_with_query(query)
386 end
391 end
387
392
388 def test_operator_date_greater_than
393 def test_operator_date_greater_than
389 query = IssueQuery.new(:name => '_')
394 query = IssueQuery.new(:name => '_')
390 query.add_filter('due_date', '>=', ['2011-07-10'])
395 query.add_filter('due_date', '>=', ['2011-07-10'])
391 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
396 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
392 find_issues_with_query(query)
397 find_issues_with_query(query)
393 end
398 end
394
399
395 def test_operator_date_between
400 def test_operator_date_between
396 query = IssueQuery.new(:name => '_')
401 query = IssueQuery.new(:name => '_')
397 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
402 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
398 assert_match /issues\.due_date > '2011-06-22 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
403 assert_match /issues\.due_date > '2011-06-22 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
399 find_issues_with_query(query)
404 find_issues_with_query(query)
400 end
405 end
401
406
402 def test_operator_in_more_than
407 def test_operator_in_more_than
403 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
408 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
404 query = IssueQuery.new(:project => Project.find(1), :name => '_')
409 query = IssueQuery.new(:project => Project.find(1), :name => '_')
405 query.add_filter('due_date', '>t+', ['15'])
410 query.add_filter('due_date', '>t+', ['15'])
406 issues = find_issues_with_query(query)
411 issues = find_issues_with_query(query)
407 assert !issues.empty?
412 assert !issues.empty?
408 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
413 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
409 end
414 end
410
415
411 def test_operator_in_less_than
416 def test_operator_in_less_than
412 query = IssueQuery.new(:project => Project.find(1), :name => '_')
417 query = IssueQuery.new(:project => Project.find(1), :name => '_')
413 query.add_filter('due_date', '<t+', ['15'])
418 query.add_filter('due_date', '<t+', ['15'])
414 issues = find_issues_with_query(query)
419 issues = find_issues_with_query(query)
415 assert !issues.empty?
420 assert !issues.empty?
416 issues.each {|issue| assert(issue.due_date <= (Date.today + 15))}
421 issues.each {|issue| assert(issue.due_date <= (Date.today + 15))}
417 end
422 end
418
423
419 def test_operator_in_the_next_days
424 def test_operator_in_the_next_days
420 query = IssueQuery.new(:project => Project.find(1), :name => '_')
425 query = IssueQuery.new(:project => Project.find(1), :name => '_')
421 query.add_filter('due_date', '><t+', ['15'])
426 query.add_filter('due_date', '><t+', ['15'])
422 issues = find_issues_with_query(query)
427 issues = find_issues_with_query(query)
423 assert !issues.empty?
428 assert !issues.empty?
424 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
429 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
425 end
430 end
426
431
427 def test_operator_less_than_ago
432 def test_operator_less_than_ago
428 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
433 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
429 query = IssueQuery.new(:project => Project.find(1), :name => '_')
434 query = IssueQuery.new(:project => Project.find(1), :name => '_')
430 query.add_filter('due_date', '>t-', ['3'])
435 query.add_filter('due_date', '>t-', ['3'])
431 issues = find_issues_with_query(query)
436 issues = find_issues_with_query(query)
432 assert !issues.empty?
437 assert !issues.empty?
433 issues.each {|issue| assert(issue.due_date >= (Date.today - 3))}
438 issues.each {|issue| assert(issue.due_date >= (Date.today - 3))}
434 end
439 end
435
440
436 def test_operator_in_the_past_days
441 def test_operator_in_the_past_days
437 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
442 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
438 query = IssueQuery.new(:project => Project.find(1), :name => '_')
443 query = IssueQuery.new(:project => Project.find(1), :name => '_')
439 query.add_filter('due_date', '><t-', ['3'])
444 query.add_filter('due_date', '><t-', ['3'])
440 issues = find_issues_with_query(query)
445 issues = find_issues_with_query(query)
441 assert !issues.empty?
446 assert !issues.empty?
442 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
447 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
443 end
448 end
444
449
445 def test_operator_more_than_ago
450 def test_operator_more_than_ago
446 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
451 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
447 query = IssueQuery.new(:project => Project.find(1), :name => '_')
452 query = IssueQuery.new(:project => Project.find(1), :name => '_')
448 query.add_filter('due_date', '<t-', ['10'])
453 query.add_filter('due_date', '<t-', ['10'])
449 assert query.statement.include?("#{Issue.table_name}.due_date <=")
454 assert query.statement.include?("#{Issue.table_name}.due_date <=")
450 issues = find_issues_with_query(query)
455 issues = find_issues_with_query(query)
451 assert !issues.empty?
456 assert !issues.empty?
452 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
457 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
453 end
458 end
454
459
455 def test_operator_in
460 def test_operator_in
456 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
461 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
457 query = IssueQuery.new(:project => Project.find(1), :name => '_')
462 query = IssueQuery.new(:project => Project.find(1), :name => '_')
458 query.add_filter('due_date', 't+', ['2'])
463 query.add_filter('due_date', 't+', ['2'])
459 issues = find_issues_with_query(query)
464 issues = find_issues_with_query(query)
460 assert !issues.empty?
465 assert !issues.empty?
461 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
466 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
462 end
467 end
463
468
464 def test_operator_ago
469 def test_operator_ago
465 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
470 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
466 query = IssueQuery.new(:project => Project.find(1), :name => '_')
471 query = IssueQuery.new(:project => Project.find(1), :name => '_')
467 query.add_filter('due_date', 't-', ['3'])
472 query.add_filter('due_date', 't-', ['3'])
468 issues = find_issues_with_query(query)
473 issues = find_issues_with_query(query)
469 assert !issues.empty?
474 assert !issues.empty?
470 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
475 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
471 end
476 end
472
477
473 def test_operator_today
478 def test_operator_today
474 query = IssueQuery.new(:project => Project.find(1), :name => '_')
479 query = IssueQuery.new(:project => Project.find(1), :name => '_')
475 query.add_filter('due_date', 't', [''])
480 query.add_filter('due_date', 't', [''])
476 issues = find_issues_with_query(query)
481 issues = find_issues_with_query(query)
477 assert !issues.empty?
482 assert !issues.empty?
478 issues.each {|issue| assert_equal Date.today, issue.due_date}
483 issues.each {|issue| assert_equal Date.today, issue.due_date}
479 end
484 end
480
485
481 def test_operator_this_week_on_date
486 def test_operator_this_week_on_date
482 query = IssueQuery.new(:project => Project.find(1), :name => '_')
487 query = IssueQuery.new(:project => Project.find(1), :name => '_')
483 query.add_filter('due_date', 'w', [''])
488 query.add_filter('due_date', 'w', [''])
484 find_issues_with_query(query)
489 find_issues_with_query(query)
485 end
490 end
486
491
487 def test_operator_this_week_on_datetime
492 def test_operator_this_week_on_datetime
488 query = IssueQuery.new(:project => Project.find(1), :name => '_')
493 query = IssueQuery.new(:project => Project.find(1), :name => '_')
489 query.add_filter('created_on', 'w', [''])
494 query.add_filter('created_on', 'w', [''])
490 find_issues_with_query(query)
495 find_issues_with_query(query)
491 end
496 end
492
497
493 def test_operator_contains
498 def test_operator_contains
494 query = IssueQuery.new(:project => Project.find(1), :name => '_')
499 query = IssueQuery.new(:project => Project.find(1), :name => '_')
495 query.add_filter('subject', '~', ['uNable'])
500 query.add_filter('subject', '~', ['uNable'])
496 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
501 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
497 result = find_issues_with_query(query)
502 result = find_issues_with_query(query)
498 assert result.empty?
503 assert result.empty?
499 result.each {|issue| assert issue.subject.downcase.include?('unable') }
504 result.each {|issue| assert issue.subject.downcase.include?('unable') }
500 end
505 end
501
506
502 def test_range_for_this_week_with_week_starting_on_monday
507 def test_range_for_this_week_with_week_starting_on_monday
503 I18n.locale = :fr
508 I18n.locale = :fr
504 assert_equal '1', I18n.t(:general_first_day_of_week)
509 assert_equal '1', I18n.t(:general_first_day_of_week)
505
510
506 Date.stubs(:today).returns(Date.parse('2011-04-29'))
511 Date.stubs(:today).returns(Date.parse('2011-04-29'))
507
512
508 query = IssueQuery.new(:project => Project.find(1), :name => '_')
513 query = IssueQuery.new(:project => Project.find(1), :name => '_')
509 query.add_filter('due_date', 'w', [''])
514 query.add_filter('due_date', 'w', [''])
510 assert query.statement.match(/issues\.due_date > '2011-04-24 23:59:59(\.9+)?' AND issues\.due_date <= '2011-05-01 23:59:59(\.9+)?/), "range not found in #{query.statement}"
515 assert query.statement.match(/issues\.due_date > '2011-04-24 23:59:59(\.9+)?' AND issues\.due_date <= '2011-05-01 23:59:59(\.9+)?/), "range not found in #{query.statement}"
511 I18n.locale = :en
516 I18n.locale = :en
512 end
517 end
513
518
514 def test_range_for_this_week_with_week_starting_on_sunday
519 def test_range_for_this_week_with_week_starting_on_sunday
515 I18n.locale = :en
520 I18n.locale = :en
516 assert_equal '7', I18n.t(:general_first_day_of_week)
521 assert_equal '7', I18n.t(:general_first_day_of_week)
517
522
518 Date.stubs(:today).returns(Date.parse('2011-04-29'))
523 Date.stubs(:today).returns(Date.parse('2011-04-29'))
519
524
520 query = IssueQuery.new(:project => Project.find(1), :name => '_')
525 query = IssueQuery.new(:project => Project.find(1), :name => '_')
521 query.add_filter('due_date', 'w', [''])
526 query.add_filter('due_date', 'w', [''])
522 assert query.statement.match(/issues\.due_date > '2011-04-23 23:59:59(\.9+)?' AND issues\.due_date <= '2011-04-30 23:59:59(\.9+)?/), "range not found in #{query.statement}"
527 assert query.statement.match(/issues\.due_date > '2011-04-23 23:59:59(\.9+)?' AND issues\.due_date <= '2011-04-30 23:59:59(\.9+)?/), "range not found in #{query.statement}"
523 end
528 end
524
529
525 def test_operator_does_not_contains
530 def test_operator_does_not_contains
526 query = IssueQuery.new(:project => Project.find(1), :name => '_')
531 query = IssueQuery.new(:project => Project.find(1), :name => '_')
527 query.add_filter('subject', '!~', ['uNable'])
532 query.add_filter('subject', '!~', ['uNable'])
528 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
533 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
529 find_issues_with_query(query)
534 find_issues_with_query(query)
530 end
535 end
531
536
532 def test_filter_assigned_to_me
537 def test_filter_assigned_to_me
533 user = User.find(2)
538 user = User.find(2)
534 group = Group.find(10)
539 group = Group.find(10)
535 User.current = user
540 User.current = user
536 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
541 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
537 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
542 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
538 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11))
543 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11))
539 group.users << user
544 group.users << user
540
545
541 query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
546 query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
542 result = query.issues
547 result = query.issues
543 assert_equal Issue.visible.all(:conditions => {:assigned_to_id => ([2] + user.reload.group_ids)}).sort_by(&:id), result.sort_by(&:id)
548 assert_equal Issue.visible.all(:conditions => {:assigned_to_id => ([2] + user.reload.group_ids)}).sort_by(&:id), result.sort_by(&:id)
544
549
545 assert result.include?(i1)
550 assert result.include?(i1)
546 assert result.include?(i2)
551 assert result.include?(i2)
547 assert !result.include?(i3)
552 assert !result.include?(i3)
548 end
553 end
549
554
550 def test_user_custom_field_filtered_on_me
555 def test_user_custom_field_filtered_on_me
551 User.current = User.find(2)
556 User.current = User.find(2)
552 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
557 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
553 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
558 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
554 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
559 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
555
560
556 query = IssueQuery.new(:name => '_', :project => Project.find(1))
561 query = IssueQuery.new(:name => '_', :project => Project.find(1))
557 filter = query.available_filters["cf_#{cf.id}"]
562 filter = query.available_filters["cf_#{cf.id}"]
558 assert_not_nil filter
563 assert_not_nil filter
559 assert_include 'me', filter[:values].map{|v| v[1]}
564 assert_include 'me', filter[:values].map{|v| v[1]}
560
565
561 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
566 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
562 result = query.issues
567 result = query.issues
563 assert_equal 1, result.size
568 assert_equal 1, result.size
564 assert_equal issue1, result.first
569 assert_equal issue1, result.first
565 end
570 end
566
571
567 def test_filter_my_projects
572 def test_filter_my_projects
568 User.current = User.find(2)
573 User.current = User.find(2)
569 query = IssueQuery.new(:name => '_')
574 query = IssueQuery.new(:name => '_')
570 filter = query.available_filters['project_id']
575 filter = query.available_filters['project_id']
571 assert_not_nil filter
576 assert_not_nil filter
572 assert_include 'mine', filter[:values].map{|v| v[1]}
577 assert_include 'mine', filter[:values].map{|v| v[1]}
573
578
574 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
579 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
575 result = query.issues
580 result = query.issues
576 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
581 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
577 end
582 end
578
583
579 def test_filter_watched_issues
584 def test_filter_watched_issues
580 User.current = User.find(1)
585 User.current = User.find(1)
581 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
586 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
582 result = find_issues_with_query(query)
587 result = find_issues_with_query(query)
583 assert_not_nil result
588 assert_not_nil result
584 assert !result.empty?
589 assert !result.empty?
585 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
590 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
586 User.current = nil
591 User.current = nil
587 end
592 end
588
593
589 def test_filter_unwatched_issues
594 def test_filter_unwatched_issues
590 User.current = User.find(1)
595 User.current = User.find(1)
591 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
596 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
592 result = find_issues_with_query(query)
597 result = find_issues_with_query(query)
593 assert_not_nil result
598 assert_not_nil result
594 assert !result.empty?
599 assert !result.empty?
595 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
600 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
596 User.current = nil
601 User.current = nil
597 end
602 end
598
603
599 def test_filter_on_project_custom_field
604 def test_filter_on_project_custom_field
600 field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
605 field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
601 CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
606 CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
602 CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo')
607 CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo')
603
608
604 query = IssueQuery.new(:name => '_')
609 query = IssueQuery.new(:name => '_')
605 filter_name = "project.cf_#{field.id}"
610 filter_name = "project.cf_#{field.id}"
606 assert_include filter_name, query.available_filters.keys
611 assert_include filter_name, query.available_filters.keys
607 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
612 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
608 assert_equal [3, 5], find_issues_with_query(query).map(&:project_id).uniq.sort
613 assert_equal [3, 5], find_issues_with_query(query).map(&:project_id).uniq.sort
609 end
614 end
610
615
611 def test_filter_on_author_custom_field
616 def test_filter_on_author_custom_field
612 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
617 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
613 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
618 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
614
619
615 query = IssueQuery.new(:name => '_')
620 query = IssueQuery.new(:name => '_')
616 filter_name = "author.cf_#{field.id}"
621 filter_name = "author.cf_#{field.id}"
617 assert_include filter_name, query.available_filters.keys
622 assert_include filter_name, query.available_filters.keys
618 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
623 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
619 assert_equal [3], find_issues_with_query(query).map(&:author_id).uniq.sort
624 assert_equal [3], find_issues_with_query(query).map(&:author_id).uniq.sort
620 end
625 end
621
626
622 def test_filter_on_assigned_to_custom_field
627 def test_filter_on_assigned_to_custom_field
623 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
628 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
624 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
629 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
625
630
626 query = IssueQuery.new(:name => '_')
631 query = IssueQuery.new(:name => '_')
627 filter_name = "assigned_to.cf_#{field.id}"
632 filter_name = "assigned_to.cf_#{field.id}"
628 assert_include filter_name, query.available_filters.keys
633 assert_include filter_name, query.available_filters.keys
629 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
634 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
630 assert_equal [3], find_issues_with_query(query).map(&:assigned_to_id).uniq.sort
635 assert_equal [3], find_issues_with_query(query).map(&:assigned_to_id).uniq.sort
631 end
636 end
632
637
633 def test_filter_on_fixed_version_custom_field
638 def test_filter_on_fixed_version_custom_field
634 field = VersionCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
639 field = VersionCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
635 CustomValue.create!(:custom_field => field, :customized => Version.find(2), :value => 'Foo')
640 CustomValue.create!(:custom_field => field, :customized => Version.find(2), :value => 'Foo')
636
641
637 query = IssueQuery.new(:name => '_')
642 query = IssueQuery.new(:name => '_')
638 filter_name = "fixed_version.cf_#{field.id}"
643 filter_name = "fixed_version.cf_#{field.id}"
639 assert_include filter_name, query.available_filters.keys
644 assert_include filter_name, query.available_filters.keys
640 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
645 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
641 assert_equal [2], find_issues_with_query(query).map(&:fixed_version_id).uniq.sort
646 assert_equal [2], find_issues_with_query(query).map(&:fixed_version_id).uniq.sort
642 end
647 end
643
648
644 def test_filter_on_relations_with_a_specific_issue
649 def test_filter_on_relations_with_a_specific_issue
645 IssueRelation.delete_all
650 IssueRelation.delete_all
646 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
651 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
647 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
652 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
648
653
649 query = IssueQuery.new(:name => '_')
654 query = IssueQuery.new(:name => '_')
650 query.filters = {"relates" => {:operator => '=', :values => ['1']}}
655 query.filters = {"relates" => {:operator => '=', :values => ['1']}}
651 assert_equal [2, 3], find_issues_with_query(query).map(&:id).sort
656 assert_equal [2, 3], find_issues_with_query(query).map(&:id).sort
652
657
653 query = IssueQuery.new(:name => '_')
658 query = IssueQuery.new(:name => '_')
654 query.filters = {"relates" => {:operator => '=', :values => ['2']}}
659 query.filters = {"relates" => {:operator => '=', :values => ['2']}}
655 assert_equal [1], find_issues_with_query(query).map(&:id).sort
660 assert_equal [1], find_issues_with_query(query).map(&:id).sort
656 end
661 end
657
662
658 def test_filter_on_relations_with_any_issues_in_a_project
663 def test_filter_on_relations_with_any_issues_in_a_project
659 IssueRelation.delete_all
664 IssueRelation.delete_all
660 with_settings :cross_project_issue_relations => '1' do
665 with_settings :cross_project_issue_relations => '1' do
661 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
666 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
662 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(2).issues.first)
667 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(2).issues.first)
663 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
668 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
664 end
669 end
665
670
666 query = IssueQuery.new(:name => '_')
671 query = IssueQuery.new(:name => '_')
667 query.filters = {"relates" => {:operator => '=p', :values => ['2']}}
672 query.filters = {"relates" => {:operator => '=p', :values => ['2']}}
668 assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort
673 assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort
669
674
670 query = IssueQuery.new(:name => '_')
675 query = IssueQuery.new(:name => '_')
671 query.filters = {"relates" => {:operator => '=p', :values => ['3']}}
676 query.filters = {"relates" => {:operator => '=p', :values => ['3']}}
672 assert_equal [1], find_issues_with_query(query).map(&:id).sort
677 assert_equal [1], find_issues_with_query(query).map(&:id).sort
673
678
674 query = IssueQuery.new(:name => '_')
679 query = IssueQuery.new(:name => '_')
675 query.filters = {"relates" => {:operator => '=p', :values => ['4']}}
680 query.filters = {"relates" => {:operator => '=p', :values => ['4']}}
676 assert_equal [], find_issues_with_query(query).map(&:id).sort
681 assert_equal [], find_issues_with_query(query).map(&:id).sort
677 end
682 end
678
683
679 def test_filter_on_relations_with_any_issues_not_in_a_project
684 def test_filter_on_relations_with_any_issues_not_in_a_project
680 IssueRelation.delete_all
685 IssueRelation.delete_all
681 with_settings :cross_project_issue_relations => '1' do
686 with_settings :cross_project_issue_relations => '1' do
682 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
687 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
683 #IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(1).issues.first)
688 #IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(1).issues.first)
684 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
689 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
685 end
690 end
686
691
687 query = IssueQuery.new(:name => '_')
692 query = IssueQuery.new(:name => '_')
688 query.filters = {"relates" => {:operator => '=!p', :values => ['1']}}
693 query.filters = {"relates" => {:operator => '=!p', :values => ['1']}}
689 assert_equal [1], find_issues_with_query(query).map(&:id).sort
694 assert_equal [1], find_issues_with_query(query).map(&:id).sort
690 end
695 end
691
696
692 def test_filter_on_relations_with_no_issues_in_a_project
697 def test_filter_on_relations_with_no_issues_in_a_project
693 IssueRelation.delete_all
698 IssueRelation.delete_all
694 with_settings :cross_project_issue_relations => '1' do
699 with_settings :cross_project_issue_relations => '1' do
695 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
700 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
696 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(3).issues.first)
701 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(3).issues.first)
697 IssueRelation.create!(:relation_type => "relates", :issue_to => Project.find(2).issues.first, :issue_from => Issue.find(3))
702 IssueRelation.create!(:relation_type => "relates", :issue_to => Project.find(2).issues.first, :issue_from => Issue.find(3))
698 end
703 end
699
704
700 query = IssueQuery.new(:name => '_')
705 query = IssueQuery.new(:name => '_')
701 query.filters = {"relates" => {:operator => '!p', :values => ['2']}}
706 query.filters = {"relates" => {:operator => '!p', :values => ['2']}}
702 ids = find_issues_with_query(query).map(&:id).sort
707 ids = find_issues_with_query(query).map(&:id).sort
703 assert_include 2, ids
708 assert_include 2, ids
704 assert_not_include 1, ids
709 assert_not_include 1, ids
705 assert_not_include 3, ids
710 assert_not_include 3, ids
706 end
711 end
707
712
708 def test_filter_on_relations_with_no_issues
713 def test_filter_on_relations_with_no_issues
709 IssueRelation.delete_all
714 IssueRelation.delete_all
710 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
715 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
711 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
716 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
712
717
713 query = IssueQuery.new(:name => '_')
718 query = IssueQuery.new(:name => '_')
714 query.filters = {"relates" => {:operator => '!*', :values => ['']}}
719 query.filters = {"relates" => {:operator => '!*', :values => ['']}}
715 ids = find_issues_with_query(query).map(&:id)
720 ids = find_issues_with_query(query).map(&:id)
716 assert_equal [], ids & [1, 2, 3]
721 assert_equal [], ids & [1, 2, 3]
717 assert_include 4, ids
722 assert_include 4, ids
718 end
723 end
719
724
720 def test_filter_on_relations_with_any_issues
725 def test_filter_on_relations_with_any_issues
721 IssueRelation.delete_all
726 IssueRelation.delete_all
722 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
727 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
723 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
728 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
724
729
725 query = IssueQuery.new(:name => '_')
730 query = IssueQuery.new(:name => '_')
726 query.filters = {"relates" => {:operator => '*', :values => ['']}}
731 query.filters = {"relates" => {:operator => '*', :values => ['']}}
727 assert_equal [1, 2, 3], find_issues_with_query(query).map(&:id).sort
732 assert_equal [1, 2, 3], find_issues_with_query(query).map(&:id).sort
728 end
733 end
729
734
730 def test_statement_should_be_nil_with_no_filters
735 def test_statement_should_be_nil_with_no_filters
731 q = IssueQuery.new(:name => '_')
736 q = IssueQuery.new(:name => '_')
732 q.filters = {}
737 q.filters = {}
733
738
734 assert q.valid?
739 assert q.valid?
735 assert_nil q.statement
740 assert_nil q.statement
736 end
741 end
737
742
738 def test_default_columns
743 def test_default_columns
739 q = IssueQuery.new
744 q = IssueQuery.new
740 assert q.columns.any?
745 assert q.columns.any?
741 assert q.inline_columns.any?
746 assert q.inline_columns.any?
742 assert q.block_columns.empty?
747 assert q.block_columns.empty?
743 end
748 end
744
749
745 def test_set_column_names
750 def test_set_column_names
746 q = IssueQuery.new
751 q = IssueQuery.new
747 q.column_names = ['tracker', :subject, '', 'unknonw_column']
752 q.column_names = ['tracker', :subject, '', 'unknonw_column']
748 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
753 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
749 c = q.columns.first
754 c = q.columns.first
750 assert q.has_column?(c)
755 assert q.has_column?(c)
751 end
756 end
752
757
753 def test_inline_and_block_columns
758 def test_inline_and_block_columns
754 q = IssueQuery.new
759 q = IssueQuery.new
755 q.column_names = ['subject', 'description', 'tracker']
760 q.column_names = ['subject', 'description', 'tracker']
756
761
757 assert_equal [:subject, :tracker], q.inline_columns.map(&:name)
762 assert_equal [:subject, :tracker], q.inline_columns.map(&:name)
758 assert_equal [:description], q.block_columns.map(&:name)
763 assert_equal [:description], q.block_columns.map(&:name)
759 end
764 end
760
765
761 def test_custom_field_columns_should_be_inline
766 def test_custom_field_columns_should_be_inline
762 q = IssueQuery.new
767 q = IssueQuery.new
763 columns = q.available_columns.select {|column| column.is_a? QueryCustomFieldColumn}
768 columns = q.available_columns.select {|column| column.is_a? QueryCustomFieldColumn}
764 assert columns.any?
769 assert columns.any?
765 assert_nil columns.detect {|column| !column.inline?}
770 assert_nil columns.detect {|column| !column.inline?}
766 end
771 end
767
772
768 def test_query_should_preload_spent_hours
773 def test_query_should_preload_spent_hours
769 q = IssueQuery.new(:name => '_', :column_names => [:subject, :spent_hours])
774 q = IssueQuery.new(:name => '_', :column_names => [:subject, :spent_hours])
770 assert q.has_column?(:spent_hours)
775 assert q.has_column?(:spent_hours)
771 issues = q.issues
776 issues = q.issues
772 assert_not_nil issues.first.instance_variable_get("@spent_hours")
777 assert_not_nil issues.first.instance_variable_get("@spent_hours")
773 end
778 end
774
779
775 def test_groupable_columns_should_include_custom_fields
780 def test_groupable_columns_should_include_custom_fields
776 q = IssueQuery.new
781 q = IssueQuery.new
777 column = q.groupable_columns.detect {|c| c.name == :cf_1}
782 column = q.groupable_columns.detect {|c| c.name == :cf_1}
778 assert_not_nil column
783 assert_not_nil column
779 assert_kind_of QueryCustomFieldColumn, column
784 assert_kind_of QueryCustomFieldColumn, column
780 end
785 end
781
786
782 def test_groupable_columns_should_not_include_multi_custom_fields
787 def test_groupable_columns_should_not_include_multi_custom_fields
783 field = CustomField.find(1)
788 field = CustomField.find(1)
784 field.update_attribute :multiple, true
789 field.update_attribute :multiple, true
785
790
786 q = IssueQuery.new
791 q = IssueQuery.new
787 column = q.groupable_columns.detect {|c| c.name == :cf_1}
792 column = q.groupable_columns.detect {|c| c.name == :cf_1}
788 assert_nil column
793 assert_nil column
789 end
794 end
790
795
791 def test_groupable_columns_should_include_user_custom_fields
796 def test_groupable_columns_should_include_user_custom_fields
792 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'user')
797 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'user')
793
798
794 q = IssueQuery.new
799 q = IssueQuery.new
795 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
800 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
796 end
801 end
797
802
798 def test_groupable_columns_should_include_version_custom_fields
803 def test_groupable_columns_should_include_version_custom_fields
799 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'version')
804 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'version')
800
805
801 q = IssueQuery.new
806 q = IssueQuery.new
802 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
807 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
803 end
808 end
804
809
805 def test_grouped_with_valid_column
810 def test_grouped_with_valid_column
806 q = IssueQuery.new(:group_by => 'status')
811 q = IssueQuery.new(:group_by => 'status')
807 assert q.grouped?
812 assert q.grouped?
808 assert_not_nil q.group_by_column
813 assert_not_nil q.group_by_column
809 assert_equal :status, q.group_by_column.name
814 assert_equal :status, q.group_by_column.name
810 assert_not_nil q.group_by_statement
815 assert_not_nil q.group_by_statement
811 assert_equal 'status', q.group_by_statement
816 assert_equal 'status', q.group_by_statement
812 end
817 end
813
818
814 def test_grouped_with_invalid_column
819 def test_grouped_with_invalid_column
815 q = IssueQuery.new(:group_by => 'foo')
820 q = IssueQuery.new(:group_by => 'foo')
816 assert !q.grouped?
821 assert !q.grouped?
817 assert_nil q.group_by_column
822 assert_nil q.group_by_column
818 assert_nil q.group_by_statement
823 assert_nil q.group_by_statement
819 end
824 end
820
825
821 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
826 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
822 with_settings :user_format => 'lastname_coma_firstname' do
827 with_settings :user_format => 'lastname_coma_firstname' do
823 q = IssueQuery.new
828 q = IssueQuery.new
824 assert q.sortable_columns.has_key?('assigned_to')
829 assert q.sortable_columns.has_key?('assigned_to')
825 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
830 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
826 end
831 end
827 end
832 end
828
833
829 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
834 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
830 with_settings :user_format => 'lastname_coma_firstname' do
835 with_settings :user_format => 'lastname_coma_firstname' do
831 q = IssueQuery.new
836 q = IssueQuery.new
832 assert q.sortable_columns.has_key?('author')
837 assert q.sortable_columns.has_key?('author')
833 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
838 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
834 end
839 end
835 end
840 end
836
841
837 def test_sortable_columns_should_include_custom_field
842 def test_sortable_columns_should_include_custom_field
838 q = IssueQuery.new
843 q = IssueQuery.new
839 assert q.sortable_columns['cf_1']
844 assert q.sortable_columns['cf_1']
840 end
845 end
841
846
842 def test_sortable_columns_should_not_include_multi_custom_field
847 def test_sortable_columns_should_not_include_multi_custom_field
843 field = CustomField.find(1)
848 field = CustomField.find(1)
844 field.update_attribute :multiple, true
849 field.update_attribute :multiple, true
845
850
846 q = IssueQuery.new
851 q = IssueQuery.new
847 assert !q.sortable_columns['cf_1']
852 assert !q.sortable_columns['cf_1']
848 end
853 end
849
854
850 def test_default_sort
855 def test_default_sort
851 q = IssueQuery.new
856 q = IssueQuery.new
852 assert_equal [], q.sort_criteria
857 assert_equal [], q.sort_criteria
853 end
858 end
854
859
855 def test_set_sort_criteria_with_hash
860 def test_set_sort_criteria_with_hash
856 q = IssueQuery.new
861 q = IssueQuery.new
857 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
862 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
858 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
863 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
859 end
864 end
860
865
861 def test_set_sort_criteria_with_array
866 def test_set_sort_criteria_with_array
862 q = IssueQuery.new
867 q = IssueQuery.new
863 q.sort_criteria = [['priority', 'desc'], 'tracker']
868 q.sort_criteria = [['priority', 'desc'], 'tracker']
864 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
869 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
865 end
870 end
866
871
867 def test_create_query_with_sort
872 def test_create_query_with_sort
868 q = IssueQuery.new(:name => 'Sorted')
873 q = IssueQuery.new(:name => 'Sorted')
869 q.sort_criteria = [['priority', 'desc'], 'tracker']
874 q.sort_criteria = [['priority', 'desc'], 'tracker']
870 assert q.save
875 assert q.save
871 q.reload
876 q.reload
872 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
877 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
873 end
878 end
874
879
875 def test_sort_by_string_custom_field_asc
880 def test_sort_by_string_custom_field_asc
876 q = IssueQuery.new
881 q = IssueQuery.new
877 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
882 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
878 assert c
883 assert c
879 assert c.sortable
884 assert c.sortable
880 issues = q.issues(:order => "#{c.sortable} ASC")
885 issues = q.issues(:order => "#{c.sortable} ASC")
881 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
886 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
882 assert !values.empty?
887 assert !values.empty?
883 assert_equal values.sort, values
888 assert_equal values.sort, values
884 end
889 end
885
890
886 def test_sort_by_string_custom_field_desc
891 def test_sort_by_string_custom_field_desc
887 q = IssueQuery.new
892 q = IssueQuery.new
888 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
893 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
889 assert c
894 assert c
890 assert c.sortable
895 assert c.sortable
891 issues = q.issues(:order => "#{c.sortable} DESC")
896 issues = q.issues(:order => "#{c.sortable} DESC")
892 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
897 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
893 assert !values.empty?
898 assert !values.empty?
894 assert_equal values.sort.reverse, values
899 assert_equal values.sort.reverse, values
895 end
900 end
896
901
897 def test_sort_by_float_custom_field_asc
902 def test_sort_by_float_custom_field_asc
898 q = IssueQuery.new
903 q = IssueQuery.new
899 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
904 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
900 assert c
905 assert c
901 assert c.sortable
906 assert c.sortable
902 issues = q.issues(:order => "#{c.sortable} ASC")
907 issues = q.issues(:order => "#{c.sortable} ASC")
903 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
908 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
904 assert !values.empty?
909 assert !values.empty?
905 assert_equal values.sort, values
910 assert_equal values.sort, values
906 end
911 end
907
912
908 def test_invalid_query_should_raise_query_statement_invalid_error
913 def test_invalid_query_should_raise_query_statement_invalid_error
909 q = IssueQuery.new
914 q = IssueQuery.new
910 assert_raise Query::StatementInvalid do
915 assert_raise Query::StatementInvalid do
911 q.issues(:conditions => "foo = 1")
916 q.issues(:conditions => "foo = 1")
912 end
917 end
913 end
918 end
914
919
915 def test_issue_count
920 def test_issue_count
916 q = IssueQuery.new(:name => '_')
921 q = IssueQuery.new(:name => '_')
917 issue_count = q.issue_count
922 issue_count = q.issue_count
918 assert_equal q.issues.size, issue_count
923 assert_equal q.issues.size, issue_count
919 end
924 end
920
925
921 def test_issue_count_with_archived_issues
926 def test_issue_count_with_archived_issues
922 p = Project.generate! do |project|
927 p = Project.generate! do |project|
923 project.status = Project::STATUS_ARCHIVED
928 project.status = Project::STATUS_ARCHIVED
924 end
929 end
925 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
930 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
926 assert !i.visible?
931 assert !i.visible?
927
932
928 test_issue_count
933 test_issue_count
929 end
934 end
930
935
931 def test_issue_count_by_association_group
936 def test_issue_count_by_association_group
932 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
937 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
933 count_by_group = q.issue_count_by_group
938 count_by_group = q.issue_count_by_group
934 assert_kind_of Hash, count_by_group
939 assert_kind_of Hash, count_by_group
935 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
940 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
936 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
941 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
937 assert count_by_group.has_key?(User.find(3))
942 assert count_by_group.has_key?(User.find(3))
938 end
943 end
939
944
940 def test_issue_count_by_list_custom_field_group
945 def test_issue_count_by_list_custom_field_group
941 q = IssueQuery.new(:name => '_', :group_by => 'cf_1')
946 q = IssueQuery.new(:name => '_', :group_by => 'cf_1')
942 count_by_group = q.issue_count_by_group
947 count_by_group = q.issue_count_by_group
943 assert_kind_of Hash, count_by_group
948 assert_kind_of Hash, count_by_group
944 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
949 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
945 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
950 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
946 assert count_by_group.has_key?('MySQL')
951 assert count_by_group.has_key?('MySQL')
947 end
952 end
948
953
949 def test_issue_count_by_date_custom_field_group
954 def test_issue_count_by_date_custom_field_group
950 q = IssueQuery.new(:name => '_', :group_by => 'cf_8')
955 q = IssueQuery.new(:name => '_', :group_by => 'cf_8')
951 count_by_group = q.issue_count_by_group
956 count_by_group = q.issue_count_by_group
952 assert_kind_of Hash, count_by_group
957 assert_kind_of Hash, count_by_group
953 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
958 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
954 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
959 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
955 end
960 end
956
961
957 def test_issue_count_with_nil_group_only
962 def test_issue_count_with_nil_group_only
958 Issue.update_all("assigned_to_id = NULL")
963 Issue.update_all("assigned_to_id = NULL")
959
964
960 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
965 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
961 count_by_group = q.issue_count_by_group
966 count_by_group = q.issue_count_by_group
962 assert_kind_of Hash, count_by_group
967 assert_kind_of Hash, count_by_group
963 assert_equal 1, count_by_group.keys.size
968 assert_equal 1, count_by_group.keys.size
964 assert_nil count_by_group.keys.first
969 assert_nil count_by_group.keys.first
965 end
970 end
966
971
967 def test_issue_ids
972 def test_issue_ids
968 q = IssueQuery.new(:name => '_')
973 q = IssueQuery.new(:name => '_')
969 order = "issues.subject, issues.id"
974 order = "issues.subject, issues.id"
970 issues = q.issues(:order => order)
975 issues = q.issues(:order => order)
971 assert_equal issues.map(&:id), q.issue_ids(:order => order)
976 assert_equal issues.map(&:id), q.issue_ids(:order => order)
972 end
977 end
973
978
974 def test_label_for
979 def test_label_for
975 set_language_if_valid 'en'
980 set_language_if_valid 'en'
976 q = IssueQuery.new
981 q = IssueQuery.new
977 assert_equal 'Assignee', q.label_for('assigned_to_id')
982 assert_equal 'Assignee', q.label_for('assigned_to_id')
978 end
983 end
979
984
980 def test_label_for_fr
985 def test_label_for_fr
981 set_language_if_valid 'fr'
986 set_language_if_valid 'fr'
982 q = IssueQuery.new
987 q = IssueQuery.new
983 s = "Assign\xc3\xa9 \xc3\xa0"
988 s = "Assign\xc3\xa9 \xc3\xa0"
984 s.force_encoding('UTF-8') if s.respond_to?(:force_encoding)
989 s.force_encoding('UTF-8') if s.respond_to?(:force_encoding)
985 assert_equal s, q.label_for('assigned_to_id')
990 assert_equal s, q.label_for('assigned_to_id')
986 end
991 end
987
992
988 def test_editable_by
993 def test_editable_by
989 admin = User.find(1)
994 admin = User.find(1)
990 manager = User.find(2)
995 manager = User.find(2)
991 developer = User.find(3)
996 developer = User.find(3)
992
997
993 # Public query on project 1
998 # Public query on project 1
994 q = IssueQuery.find(1)
999 q = IssueQuery.find(1)
995 assert q.editable_by?(admin)
1000 assert q.editable_by?(admin)
996 assert q.editable_by?(manager)
1001 assert q.editable_by?(manager)
997 assert !q.editable_by?(developer)
1002 assert !q.editable_by?(developer)
998
1003
999 # Private query on project 1
1004 # Private query on project 1
1000 q = IssueQuery.find(2)
1005 q = IssueQuery.find(2)
1001 assert q.editable_by?(admin)
1006 assert q.editable_by?(admin)
1002 assert !q.editable_by?(manager)
1007 assert !q.editable_by?(manager)
1003 assert q.editable_by?(developer)
1008 assert q.editable_by?(developer)
1004
1009
1005 # Private query for all projects
1010 # Private query for all projects
1006 q = IssueQuery.find(3)
1011 q = IssueQuery.find(3)
1007 assert q.editable_by?(admin)
1012 assert q.editable_by?(admin)
1008 assert !q.editable_by?(manager)
1013 assert !q.editable_by?(manager)
1009 assert q.editable_by?(developer)
1014 assert q.editable_by?(developer)
1010
1015
1011 # Public query for all projects
1016 # Public query for all projects
1012 q = IssueQuery.find(4)
1017 q = IssueQuery.find(4)
1013 assert q.editable_by?(admin)
1018 assert q.editable_by?(admin)
1014 assert !q.editable_by?(manager)
1019 assert !q.editable_by?(manager)
1015 assert !q.editable_by?(developer)
1020 assert !q.editable_by?(developer)
1016 end
1021 end
1017
1022
1018 def test_visible_scope
1023 def test_visible_scope
1019 query_ids = IssueQuery.visible(User.anonymous).map(&:id)
1024 query_ids = IssueQuery.visible(User.anonymous).map(&:id)
1020
1025
1021 assert query_ids.include?(1), 'public query on public project was not visible'
1026 assert query_ids.include?(1), 'public query on public project was not visible'
1022 assert query_ids.include?(4), 'public query for all projects was not visible'
1027 assert query_ids.include?(4), 'public query for all projects was not visible'
1023 assert !query_ids.include?(2), 'private query on public project was visible'
1028 assert !query_ids.include?(2), 'private query on public project was visible'
1024 assert !query_ids.include?(3), 'private query for all projects was visible'
1029 assert !query_ids.include?(3), 'private query for all projects was visible'
1025 assert !query_ids.include?(7), 'public query on private project was visible'
1030 assert !query_ids.include?(7), 'public query on private project was visible'
1026 end
1031 end
1027
1032
1028 test "#available_filters should include users of visible projects in cross-project view" do
1033 test "#available_filters should include users of visible projects in cross-project view" do
1029 users = IssueQuery.new.available_filters["assigned_to_id"]
1034 users = IssueQuery.new.available_filters["assigned_to_id"]
1030 assert_not_nil users
1035 assert_not_nil users
1031 assert users[:values].map{|u|u[1]}.include?("3")
1036 assert users[:values].map{|u|u[1]}.include?("3")
1032 end
1037 end
1033
1038
1034 test "#available_filters should include users of subprojects" do
1039 test "#available_filters should include users of subprojects" do
1035 user1 = User.generate!
1040 user1 = User.generate!
1036 user2 = User.generate!
1041 user2 = User.generate!
1037 project = Project.find(1)
1042 project = Project.find(1)
1038 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
1043 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
1039
1044
1040 users = IssueQuery.new(:project => project).available_filters["assigned_to_id"]
1045 users = IssueQuery.new(:project => project).available_filters["assigned_to_id"]
1041 assert_not_nil users
1046 assert_not_nil users
1042 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
1047 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
1043 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
1048 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
1044 end
1049 end
1045
1050
1046 test "#available_filters should include visible projects in cross-project view" do
1051 test "#available_filters should include visible projects in cross-project view" do
1047 projects = IssueQuery.new.available_filters["project_id"]
1052 projects = IssueQuery.new.available_filters["project_id"]
1048 assert_not_nil projects
1053 assert_not_nil projects
1049 assert projects[:values].map{|u|u[1]}.include?("1")
1054 assert projects[:values].map{|u|u[1]}.include?("1")
1050 end
1055 end
1051
1056
1052 test "#available_filters should include 'member_of_group' filter" do
1057 test "#available_filters should include 'member_of_group' filter" do
1053 query = IssueQuery.new
1058 query = IssueQuery.new
1054 assert query.available_filters.keys.include?("member_of_group")
1059 assert query.available_filters.keys.include?("member_of_group")
1055 assert_equal :list_optional, query.available_filters["member_of_group"][:type]
1060 assert_equal :list_optional, query.available_filters["member_of_group"][:type]
1056 assert query.available_filters["member_of_group"][:values].present?
1061 assert query.available_filters["member_of_group"][:values].present?
1057 assert_equal Group.all.sort.map {|g| [g.name, g.id.to_s]},
1062 assert_equal Group.all.sort.map {|g| [g.name, g.id.to_s]},
1058 query.available_filters["member_of_group"][:values].sort
1063 query.available_filters["member_of_group"][:values].sort
1059 end
1064 end
1060
1065
1061 test "#available_filters should include 'assigned_to_role' filter" do
1066 test "#available_filters should include 'assigned_to_role' filter" do
1062 query = IssueQuery.new
1067 query = IssueQuery.new
1063 assert query.available_filters.keys.include?("assigned_to_role")
1068 assert query.available_filters.keys.include?("assigned_to_role")
1064 assert_equal :list_optional, query.available_filters["assigned_to_role"][:type]
1069 assert_equal :list_optional, query.available_filters["assigned_to_role"][:type]
1065
1070
1066 assert query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
1071 assert query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
1067 assert query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
1072 assert query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
1068 assert query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
1073 assert query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
1069
1074
1070 assert ! query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
1075 assert ! query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
1071 assert ! query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
1076 assert ! query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
1072 end
1077 end
1073
1078
1074 context "#statement" do
1079 context "#statement" do
1075 context "with 'member_of_group' filter" do
1080 context "with 'member_of_group' filter" do
1076 setup do
1081 setup do
1077 Group.destroy_all # No fixtures
1082 Group.destroy_all # No fixtures
1078 @user_in_group = User.generate!
1083 @user_in_group = User.generate!
1079 @second_user_in_group = User.generate!
1084 @second_user_in_group = User.generate!
1080 @user_in_group2 = User.generate!
1085 @user_in_group2 = User.generate!
1081 @user_not_in_group = User.generate!
1086 @user_not_in_group = User.generate!
1082
1087
1083 @group = Group.generate!.reload
1088 @group = Group.generate!.reload
1084 @group.users << @user_in_group
1089 @group.users << @user_in_group
1085 @group.users << @second_user_in_group
1090 @group.users << @second_user_in_group
1086
1091
1087 @group2 = Group.generate!.reload
1092 @group2 = Group.generate!.reload
1088 @group2.users << @user_in_group2
1093 @group2.users << @user_in_group2
1089
1094
1090 end
1095 end
1091
1096
1092 should "search assigned to for users in the group" do
1097 should "search assigned to for users in the group" do
1093 @query = IssueQuery.new(:name => '_')
1098 @query = IssueQuery.new(:name => '_')
1094 @query.add_filter('member_of_group', '=', [@group.id.to_s])
1099 @query.add_filter('member_of_group', '=', [@group.id.to_s])
1095
1100
1096 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@group.id}')"
1101 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@group.id}')"
1097 assert_find_issues_with_query_is_successful @query
1102 assert_find_issues_with_query_is_successful @query
1098 end
1103 end
1099
1104
1100 should "search not assigned to any group member (none)" do
1105 should "search not assigned to any group member (none)" do
1101 @query = IssueQuery.new(:name => '_')
1106 @query = IssueQuery.new(:name => '_')
1102 @query.add_filter('member_of_group', '!*', [''])
1107 @query.add_filter('member_of_group', '!*', [''])
1103
1108
1104 # Users not in a group
1109 # Users not in a group
1105 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}','#{@group.id}','#{@group2.id}')"
1110 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}','#{@group.id}','#{@group2.id}')"
1106 assert_find_issues_with_query_is_successful @query
1111 assert_find_issues_with_query_is_successful @query
1107 end
1112 end
1108
1113
1109 should "search assigned to any group member (all)" do
1114 should "search assigned to any group member (all)" do
1110 @query = IssueQuery.new(:name => '_')
1115 @query = IssueQuery.new(:name => '_')
1111 @query.add_filter('member_of_group', '*', [''])
1116 @query.add_filter('member_of_group', '*', [''])
1112
1117
1113 # Only users in a group
1118 # Only users in a group
1114 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}','#{@group.id}','#{@group2.id}')"
1119 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}','#{@group.id}','#{@group2.id}')"
1115 assert_find_issues_with_query_is_successful @query
1120 assert_find_issues_with_query_is_successful @query
1116 end
1121 end
1117
1122
1118 should "return an empty set with = empty group" do
1123 should "return an empty set with = empty group" do
1119 @empty_group = Group.generate!
1124 @empty_group = Group.generate!
1120 @query = IssueQuery.new(:name => '_')
1125 @query = IssueQuery.new(:name => '_')
1121 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
1126 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
1122
1127
1123 assert_equal [], find_issues_with_query(@query)
1128 assert_equal [], find_issues_with_query(@query)
1124 end
1129 end
1125
1130
1126 should "return issues with ! empty group" do
1131 should "return issues with ! empty group" do
1127 @empty_group = Group.generate!
1132 @empty_group = Group.generate!
1128 @query = IssueQuery.new(:name => '_')
1133 @query = IssueQuery.new(:name => '_')
1129 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
1134 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
1130
1135
1131 assert_find_issues_with_query_is_successful @query
1136 assert_find_issues_with_query_is_successful @query
1132 end
1137 end
1133 end
1138 end
1134
1139
1135 context "with 'assigned_to_role' filter" do
1140 context "with 'assigned_to_role' filter" do
1136 setup do
1141 setup do
1137 @manager_role = Role.find_by_name('Manager')
1142 @manager_role = Role.find_by_name('Manager')
1138 @developer_role = Role.find_by_name('Developer')
1143 @developer_role = Role.find_by_name('Developer')
1139
1144
1140 @project = Project.generate!
1145 @project = Project.generate!
1141 @manager = User.generate!
1146 @manager = User.generate!
1142 @developer = User.generate!
1147 @developer = User.generate!
1143 @boss = User.generate!
1148 @boss = User.generate!
1144 @guest = User.generate!
1149 @guest = User.generate!
1145 User.add_to_project(@manager, @project, @manager_role)
1150 User.add_to_project(@manager, @project, @manager_role)
1146 User.add_to_project(@developer, @project, @developer_role)
1151 User.add_to_project(@developer, @project, @developer_role)
1147 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
1152 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
1148
1153
1149 @issue1 = Issue.generate!(:project => @project, :assigned_to_id => @manager.id)
1154 @issue1 = Issue.generate!(:project => @project, :assigned_to_id => @manager.id)
1150 @issue2 = Issue.generate!(:project => @project, :assigned_to_id => @developer.id)
1155 @issue2 = Issue.generate!(:project => @project, :assigned_to_id => @developer.id)
1151 @issue3 = Issue.generate!(:project => @project, :assigned_to_id => @boss.id)
1156 @issue3 = Issue.generate!(:project => @project, :assigned_to_id => @boss.id)
1152 @issue4 = Issue.generate!(:project => @project, :assigned_to_id => @guest.id)
1157 @issue4 = Issue.generate!(:project => @project, :assigned_to_id => @guest.id)
1153 @issue5 = Issue.generate!(:project => @project)
1158 @issue5 = Issue.generate!(:project => @project)
1154 end
1159 end
1155
1160
1156 should "search assigned to for users with the Role" do
1161 should "search assigned to for users with the Role" do
1157 @query = IssueQuery.new(:name => '_', :project => @project)
1162 @query = IssueQuery.new(:name => '_', :project => @project)
1158 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1163 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1159
1164
1160 assert_query_result [@issue1, @issue3], @query
1165 assert_query_result [@issue1, @issue3], @query
1161 end
1166 end
1162
1167
1163 should "search assigned to for users with the Role on the issue project" do
1168 should "search assigned to for users with the Role on the issue project" do
1164 other_project = Project.generate!
1169 other_project = Project.generate!
1165 User.add_to_project(@developer, other_project, @manager_role)
1170 User.add_to_project(@developer, other_project, @manager_role)
1166
1171
1167 @query = IssueQuery.new(:name => '_', :project => @project)
1172 @query = IssueQuery.new(:name => '_', :project => @project)
1168 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1173 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1169
1174
1170 assert_query_result [@issue1, @issue3], @query
1175 assert_query_result [@issue1, @issue3], @query
1171 end
1176 end
1172
1177
1173 should "return an empty set with empty role" do
1178 should "return an empty set with empty role" do
1174 @empty_role = Role.generate!
1179 @empty_role = Role.generate!
1175 @query = IssueQuery.new(:name => '_', :project => @project)
1180 @query = IssueQuery.new(:name => '_', :project => @project)
1176 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
1181 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
1177
1182
1178 assert_query_result [], @query
1183 assert_query_result [], @query
1179 end
1184 end
1180
1185
1181 should "search assigned to for users without the Role" do
1186 should "search assigned to for users without the Role" do
1182 @query = IssueQuery.new(:name => '_', :project => @project)
1187 @query = IssueQuery.new(:name => '_', :project => @project)
1183 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
1188 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
1184
1189
1185 assert_query_result [@issue2, @issue4, @issue5], @query
1190 assert_query_result [@issue2, @issue4, @issue5], @query
1186 end
1191 end
1187
1192
1188 should "search assigned to for users not assigned to any Role (none)" do
1193 should "search assigned to for users not assigned to any Role (none)" do
1189 @query = IssueQuery.new(:name => '_', :project => @project)
1194 @query = IssueQuery.new(:name => '_', :project => @project)
1190 @query.add_filter('assigned_to_role', '!*', [''])
1195 @query.add_filter('assigned_to_role', '!*', [''])
1191
1196
1192 assert_query_result [@issue4, @issue5], @query
1197 assert_query_result [@issue4, @issue5], @query
1193 end
1198 end
1194
1199
1195 should "search assigned to for users assigned to any Role (all)" do
1200 should "search assigned to for users assigned to any Role (all)" do
1196 @query = IssueQuery.new(:name => '_', :project => @project)
1201 @query = IssueQuery.new(:name => '_', :project => @project)
1197 @query.add_filter('assigned_to_role', '*', [''])
1202 @query.add_filter('assigned_to_role', '*', [''])
1198
1203
1199 assert_query_result [@issue1, @issue2, @issue3], @query
1204 assert_query_result [@issue1, @issue2, @issue3], @query
1200 end
1205 end
1201
1206
1202 should "return issues with ! empty role" do
1207 should "return issues with ! empty role" do
1203 @empty_role = Role.generate!
1208 @empty_role = Role.generate!
1204 @query = IssueQuery.new(:name => '_', :project => @project)
1209 @query = IssueQuery.new(:name => '_', :project => @project)
1205 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
1210 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
1206
1211
1207 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
1212 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
1208 end
1213 end
1209 end
1214 end
1210 end
1215 end
1211 end
1216 end
General Comments 0
You need to be logged in to leave comments. Login now