##// END OF EJS Templates
Fixed: SQL error when filtering issues with an empty group or role (#7656)....
Jean-Philippe Lang -
r4768:f357912d211c
parent child
Show More
@@ -1,647 +1,657
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 @caption_key = options[:caption] || "field_#{name}"
30 @caption_key = options[:caption] || "field_#{name}"
31 end
31 end
32
32
33 def caption
33 def caption
34 l(@caption_key)
34 l(@caption_key)
35 end
35 end
36
36
37 # Returns true if the column is sortable, otherwise false
37 # Returns true if the column is sortable, otherwise false
38 def sortable?
38 def sortable?
39 !sortable.nil?
39 !sortable.nil?
40 end
40 end
41
41
42 def value(issue)
42 def value(issue)
43 issue.send name
43 issue.send name
44 end
44 end
45 end
45 end
46
46
47 class QueryCustomFieldColumn < QueryColumn
47 class QueryCustomFieldColumn < QueryColumn
48
48
49 def initialize(custom_field)
49 def initialize(custom_field)
50 self.name = "cf_#{custom_field.id}".to_sym
50 self.name = "cf_#{custom_field.id}".to_sym
51 self.sortable = custom_field.order_statement || false
51 self.sortable = custom_field.order_statement || false
52 if %w(list date bool int).include?(custom_field.field_format)
52 if %w(list date bool int).include?(custom_field.field_format)
53 self.groupable = custom_field.order_statement
53 self.groupable = custom_field.order_statement
54 end
54 end
55 self.groupable ||= false
55 self.groupable ||= false
56 @cf = custom_field
56 @cf = custom_field
57 end
57 end
58
58
59 def caption
59 def caption
60 @cf.name
60 @cf.name
61 end
61 end
62
62
63 def custom_field
63 def custom_field
64 @cf
64 @cf
65 end
65 end
66
66
67 def value(issue)
67 def value(issue)
68 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
68 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
69 cv && @cf.cast_value(cv.value)
69 cv && @cf.cast_value(cv.value)
70 end
70 end
71 end
71 end
72
72
73 class Query < ActiveRecord::Base
73 class Query < ActiveRecord::Base
74 class StatementInvalid < ::ActiveRecord::StatementInvalid
74 class StatementInvalid < ::ActiveRecord::StatementInvalid
75 end
75 end
76
76
77 belongs_to :project
77 belongs_to :project
78 belongs_to :user
78 belongs_to :user
79 serialize :filters
79 serialize :filters
80 serialize :column_names
80 serialize :column_names
81 serialize :sort_criteria, Array
81 serialize :sort_criteria, Array
82
82
83 attr_protected :project_id, :user_id
83 attr_protected :project_id, :user_id
84
84
85 validates_presence_of :name, :on => :save
85 validates_presence_of :name, :on => :save
86 validates_length_of :name, :maximum => 255
86 validates_length_of :name, :maximum => 255
87
87
88 @@operators = { "=" => :label_equals,
88 @@operators = { "=" => :label_equals,
89 "!" => :label_not_equals,
89 "!" => :label_not_equals,
90 "o" => :label_open_issues,
90 "o" => :label_open_issues,
91 "c" => :label_closed_issues,
91 "c" => :label_closed_issues,
92 "!*" => :label_none,
92 "!*" => :label_none,
93 "*" => :label_all,
93 "*" => :label_all,
94 ">=" => :label_greater_or_equal,
94 ">=" => :label_greater_or_equal,
95 "<=" => :label_less_or_equal,
95 "<=" => :label_less_or_equal,
96 "<t+" => :label_in_less_than,
96 "<t+" => :label_in_less_than,
97 ">t+" => :label_in_more_than,
97 ">t+" => :label_in_more_than,
98 "t+" => :label_in,
98 "t+" => :label_in,
99 "t" => :label_today,
99 "t" => :label_today,
100 "w" => :label_this_week,
100 "w" => :label_this_week,
101 ">t-" => :label_less_than_ago,
101 ">t-" => :label_less_than_ago,
102 "<t-" => :label_more_than_ago,
102 "<t-" => :label_more_than_ago,
103 "t-" => :label_ago,
103 "t-" => :label_ago,
104 "~" => :label_contains,
104 "~" => :label_contains,
105 "!~" => :label_not_contains }
105 "!~" => :label_not_contains }
106
106
107 cattr_reader :operators
107 cattr_reader :operators
108
108
109 @@operators_by_filter_type = { :list => [ "=", "!" ],
109 @@operators_by_filter_type = { :list => [ "=", "!" ],
110 :list_status => [ "o", "=", "!", "c", "*" ],
110 :list_status => [ "o", "=", "!", "c", "*" ],
111 :list_optional => [ "=", "!", "!*", "*" ],
111 :list_optional => [ "=", "!", "!*", "*" ],
112 :list_subprojects => [ "*", "!*", "=" ],
112 :list_subprojects => [ "*", "!*", "=" ],
113 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
113 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
114 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
114 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
115 :string => [ "=", "~", "!", "!~" ],
115 :string => [ "=", "~", "!", "!~" ],
116 :text => [ "~", "!~" ],
116 :text => [ "~", "!~" ],
117 :integer => [ "=", ">=", "<=", "!*", "*" ] }
117 :integer => [ "=", ">=", "<=", "!*", "*" ] }
118
118
119 cattr_reader :operators_by_filter_type
119 cattr_reader :operators_by_filter_type
120
120
121 @@available_columns = [
121 @@available_columns = [
122 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
122 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
123 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
123 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
124 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
124 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
125 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
125 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
126 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
126 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
127 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
127 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
128 QueryColumn.new(:author),
128 QueryColumn.new(:author),
129 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
129 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
130 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
130 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
131 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
131 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
132 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
132 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
133 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
133 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
134 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
134 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
135 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
135 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
136 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
136 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
137 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
137 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
138 ]
138 ]
139 cattr_reader :available_columns
139 cattr_reader :available_columns
140
140
141 def initialize(attributes = nil)
141 def initialize(attributes = nil)
142 super attributes
142 super attributes
143 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
143 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
144 end
144 end
145
145
146 def after_initialize
146 def after_initialize
147 # Store the fact that project is nil (used in #editable_by?)
147 # Store the fact that project is nil (used in #editable_by?)
148 @is_for_all = project.nil?
148 @is_for_all = project.nil?
149 end
149 end
150
150
151 def validate
151 def validate
152 filters.each_key do |field|
152 filters.each_key do |field|
153 errors.add label_for(field), :blank unless
153 errors.add label_for(field), :blank unless
154 # filter requires one or more values
154 # filter requires one or more values
155 (values_for(field) and !values_for(field).first.blank?) or
155 (values_for(field) and !values_for(field).first.blank?) or
156 # filter doesn't require any value
156 # filter doesn't require any value
157 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
157 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
158 end if filters
158 end if filters
159 end
159 end
160
160
161 def editable_by?(user)
161 def editable_by?(user)
162 return false unless user
162 return false unless user
163 # Admin can edit them all and regular users can edit their private queries
163 # Admin can edit them all and regular users can edit their private queries
164 return true if user.admin? || (!is_public && self.user_id == user.id)
164 return true if user.admin? || (!is_public && self.user_id == user.id)
165 # Members can not edit public queries that are for all project (only admin is allowed to)
165 # Members can not edit public queries that are for all project (only admin is allowed to)
166 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
166 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
167 end
167 end
168
168
169 def available_filters
169 def available_filters
170 return @available_filters if @available_filters
170 return @available_filters if @available_filters
171
171
172 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
172 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
173
173
174 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
174 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
175 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
175 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
176 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
176 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
177 "subject" => { :type => :text, :order => 8 },
177 "subject" => { :type => :text, :order => 8 },
178 "created_on" => { :type => :date_past, :order => 9 },
178 "created_on" => { :type => :date_past, :order => 9 },
179 "updated_on" => { :type => :date_past, :order => 10 },
179 "updated_on" => { :type => :date_past, :order => 10 },
180 "start_date" => { :type => :date, :order => 11 },
180 "start_date" => { :type => :date, :order => 11 },
181 "due_date" => { :type => :date, :order => 12 },
181 "due_date" => { :type => :date, :order => 12 },
182 "estimated_hours" => { :type => :integer, :order => 13 },
182 "estimated_hours" => { :type => :integer, :order => 13 },
183 "done_ratio" => { :type => :integer, :order => 14 }}
183 "done_ratio" => { :type => :integer, :order => 14 }}
184
184
185 user_values = []
185 user_values = []
186 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
186 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
187 if project
187 if project
188 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
188 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
189 else
189 else
190 all_projects = Project.visible.all
190 all_projects = Project.visible.all
191 if all_projects.any?
191 if all_projects.any?
192 # members of visible projects
192 # members of visible projects
193 user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort.collect{|s| [s.name, s.id.to_s] }
193 user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort.collect{|s| [s.name, s.id.to_s] }
194
194
195 # project filter
195 # project filter
196 project_values = []
196 project_values = []
197 Project.project_tree(all_projects) do |p, level|
197 Project.project_tree(all_projects) do |p, level|
198 prefix = (level > 0 ? ('--' * level + ' ') : '')
198 prefix = (level > 0 ? ('--' * level + ' ') : '')
199 project_values << ["#{prefix}#{p.name}", p.id.to_s]
199 project_values << ["#{prefix}#{p.name}", p.id.to_s]
200 end
200 end
201 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
201 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
202 end
202 end
203 end
203 end
204 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
204 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
205 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
205 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
206
206
207 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
207 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
208 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
208 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
209
209
210 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
210 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
211 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
211 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
212
212
213 if User.current.logged?
213 if User.current.logged?
214 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
214 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
215 end
215 end
216
216
217 if project
217 if project
218 # project specific filters
218 # project specific filters
219 unless @project.issue_categories.empty?
219 unless @project.issue_categories.empty?
220 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
220 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
221 end
221 end
222 unless @project.shared_versions.empty?
222 unless @project.shared_versions.empty?
223 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
223 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
224 end
224 end
225 unless @project.descendants.active.empty?
225 unless @project.descendants.active.empty?
226 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
226 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
227 end
227 end
228 add_custom_fields_filters(@project.all_issue_custom_fields)
228 add_custom_fields_filters(@project.all_issue_custom_fields)
229 else
229 else
230 # global filters for cross project issue list
230 # global filters for cross project issue list
231 system_shared_versions = Version.visible.find_all_by_sharing('system')
231 system_shared_versions = Version.visible.find_all_by_sharing('system')
232 unless system_shared_versions.empty?
232 unless system_shared_versions.empty?
233 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
233 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
234 end
234 end
235 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
235 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
236 end
236 end
237 @available_filters
237 @available_filters
238 end
238 end
239
239
240 def add_filter(field, operator, values)
240 def add_filter(field, operator, values)
241 # values must be an array
241 # values must be an array
242 return unless values and values.is_a? Array # and !values.first.empty?
242 return unless values and values.is_a? Array # and !values.first.empty?
243 # check if field is defined as an available filter
243 # check if field is defined as an available filter
244 if available_filters.has_key? field
244 if available_filters.has_key? field
245 filter_options = available_filters[field]
245 filter_options = available_filters[field]
246 # check if operator is allowed for that filter
246 # check if operator is allowed for that filter
247 #if @@operators_by_filter_type[filter_options[:type]].include? operator
247 #if @@operators_by_filter_type[filter_options[:type]].include? operator
248 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
248 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
249 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
249 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
250 #end
250 #end
251 filters[field] = {:operator => operator, :values => values }
251 filters[field] = {:operator => operator, :values => values }
252 end
252 end
253 end
253 end
254
254
255 def add_short_filter(field, expression)
255 def add_short_filter(field, expression)
256 return unless expression
256 return unless expression
257 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
257 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
258 add_filter field, (parms[0] || "="), [parms[1] || ""]
258 add_filter field, (parms[0] || "="), [parms[1] || ""]
259 end
259 end
260
260
261 # Add multiple filters using +add_filter+
261 # Add multiple filters using +add_filter+
262 def add_filters(fields, operators, values)
262 def add_filters(fields, operators, values)
263 if fields.is_a?(Array) && operators.is_a?(Hash) && values.is_a?(Hash)
263 if fields.is_a?(Array) && operators.is_a?(Hash) && values.is_a?(Hash)
264 fields.each do |field|
264 fields.each do |field|
265 add_filter(field, operators[field], values[field])
265 add_filter(field, operators[field], values[field])
266 end
266 end
267 end
267 end
268 end
268 end
269
269
270 def has_filter?(field)
270 def has_filter?(field)
271 filters and filters[field]
271 filters and filters[field]
272 end
272 end
273
273
274 def operator_for(field)
274 def operator_for(field)
275 has_filter?(field) ? filters[field][:operator] : nil
275 has_filter?(field) ? filters[field][:operator] : nil
276 end
276 end
277
277
278 def values_for(field)
278 def values_for(field)
279 has_filter?(field) ? filters[field][:values] : nil
279 has_filter?(field) ? filters[field][:values] : nil
280 end
280 end
281
281
282 def label_for(field)
282 def label_for(field)
283 label = available_filters[field][:name] if available_filters.has_key?(field)
283 label = available_filters[field][:name] if available_filters.has_key?(field)
284 label ||= field.gsub(/\_id$/, "")
284 label ||= field.gsub(/\_id$/, "")
285 end
285 end
286
286
287 def available_columns
287 def available_columns
288 return @available_columns if @available_columns
288 return @available_columns if @available_columns
289 @available_columns = Query.available_columns
289 @available_columns = Query.available_columns
290 @available_columns += (project ?
290 @available_columns += (project ?
291 project.all_issue_custom_fields :
291 project.all_issue_custom_fields :
292 IssueCustomField.find(:all)
292 IssueCustomField.find(:all)
293 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
293 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
294 end
294 end
295
295
296 def self.available_columns=(v)
296 def self.available_columns=(v)
297 self.available_columns = (v)
297 self.available_columns = (v)
298 end
298 end
299
299
300 def self.add_available_column(column)
300 def self.add_available_column(column)
301 self.available_columns << (column) if column.is_a?(QueryColumn)
301 self.available_columns << (column) if column.is_a?(QueryColumn)
302 end
302 end
303
303
304 # Returns an array of columns that can be used to group the results
304 # Returns an array of columns that can be used to group the results
305 def groupable_columns
305 def groupable_columns
306 available_columns.select {|c| c.groupable}
306 available_columns.select {|c| c.groupable}
307 end
307 end
308
308
309 # Returns a Hash of columns and the key for sorting
309 # Returns a Hash of columns and the key for sorting
310 def sortable_columns
310 def sortable_columns
311 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
311 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
312 h[column.name.to_s] = column.sortable
312 h[column.name.to_s] = column.sortable
313 h
313 h
314 })
314 })
315 end
315 end
316
316
317 def columns
317 def columns
318 if has_default_columns?
318 if has_default_columns?
319 available_columns.select do |c|
319 available_columns.select do |c|
320 # Adds the project column by default for cross-project lists
320 # Adds the project column by default for cross-project lists
321 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
321 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
322 end
322 end
323 else
323 else
324 # preserve the column_names order
324 # preserve the column_names order
325 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
325 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
326 end
326 end
327 end
327 end
328
328
329 def column_names=(names)
329 def column_names=(names)
330 if names
330 if names
331 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
331 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
332 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
332 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
333 # Set column_names to nil if default columns
333 # Set column_names to nil if default columns
334 if names.map(&:to_s) == Setting.issue_list_default_columns
334 if names.map(&:to_s) == Setting.issue_list_default_columns
335 names = nil
335 names = nil
336 end
336 end
337 end
337 end
338 write_attribute(:column_names, names)
338 write_attribute(:column_names, names)
339 end
339 end
340
340
341 def has_column?(column)
341 def has_column?(column)
342 column_names && column_names.include?(column.name)
342 column_names && column_names.include?(column.name)
343 end
343 end
344
344
345 def has_default_columns?
345 def has_default_columns?
346 column_names.nil? || column_names.empty?
346 column_names.nil? || column_names.empty?
347 end
347 end
348
348
349 def sort_criteria=(arg)
349 def sort_criteria=(arg)
350 c = []
350 c = []
351 if arg.is_a?(Hash)
351 if arg.is_a?(Hash)
352 arg = arg.keys.sort.collect {|k| arg[k]}
352 arg = arg.keys.sort.collect {|k| arg[k]}
353 end
353 end
354 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
354 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
355 write_attribute(:sort_criteria, c)
355 write_attribute(:sort_criteria, c)
356 end
356 end
357
357
358 def sort_criteria
358 def sort_criteria
359 read_attribute(:sort_criteria) || []
359 read_attribute(:sort_criteria) || []
360 end
360 end
361
361
362 def sort_criteria_key(arg)
362 def sort_criteria_key(arg)
363 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
363 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
364 end
364 end
365
365
366 def sort_criteria_order(arg)
366 def sort_criteria_order(arg)
367 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
367 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
368 end
368 end
369
369
370 # Returns the SQL sort order that should be prepended for grouping
370 # Returns the SQL sort order that should be prepended for grouping
371 def group_by_sort_order
371 def group_by_sort_order
372 if grouped? && (column = group_by_column)
372 if grouped? && (column = group_by_column)
373 column.sortable.is_a?(Array) ?
373 column.sortable.is_a?(Array) ?
374 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
374 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
375 "#{column.sortable} #{column.default_order}"
375 "#{column.sortable} #{column.default_order}"
376 end
376 end
377 end
377 end
378
378
379 # Returns true if the query is a grouped query
379 # Returns true if the query is a grouped query
380 def grouped?
380 def grouped?
381 !group_by_column.nil?
381 !group_by_column.nil?
382 end
382 end
383
383
384 def group_by_column
384 def group_by_column
385 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
385 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
386 end
386 end
387
387
388 def group_by_statement
388 def group_by_statement
389 group_by_column.try(:groupable)
389 group_by_column.try(:groupable)
390 end
390 end
391
391
392 def project_statement
392 def project_statement
393 project_clauses = []
393 project_clauses = []
394 if project && !@project.descendants.active.empty?
394 if project && !@project.descendants.active.empty?
395 ids = [project.id]
395 ids = [project.id]
396 if has_filter?("subproject_id")
396 if has_filter?("subproject_id")
397 case operator_for("subproject_id")
397 case operator_for("subproject_id")
398 when '='
398 when '='
399 # include the selected subprojects
399 # include the selected subprojects
400 ids += values_for("subproject_id").each(&:to_i)
400 ids += values_for("subproject_id").each(&:to_i)
401 when '!*'
401 when '!*'
402 # main project only
402 # main project only
403 else
403 else
404 # all subprojects
404 # all subprojects
405 ids += project.descendants.collect(&:id)
405 ids += project.descendants.collect(&:id)
406 end
406 end
407 elsif Setting.display_subprojects_issues?
407 elsif Setting.display_subprojects_issues?
408 ids += project.descendants.collect(&:id)
408 ids += project.descendants.collect(&:id)
409 end
409 end
410 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
410 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
411 elsif project
411 elsif project
412 project_clauses << "#{Project.table_name}.id = %d" % project.id
412 project_clauses << "#{Project.table_name}.id = %d" % project.id
413 end
413 end
414 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
414 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
415 project_clauses.join(' AND ')
415 project_clauses.join(' AND ')
416 end
416 end
417
417
418 def statement
418 def statement
419 # filters clauses
419 # filters clauses
420 filters_clauses = []
420 filters_clauses = []
421 filters.each_key do |field|
421 filters.each_key do |field|
422 next if field == "subproject_id"
422 next if field == "subproject_id"
423 v = values_for(field).clone
423 v = values_for(field).clone
424 next unless v and !v.empty?
424 next unless v and !v.empty?
425 operator = operator_for(field)
425 operator = operator_for(field)
426
426
427 # "me" value subsitution
427 # "me" value subsitution
428 if %w(assigned_to_id author_id watcher_id).include?(field)
428 if %w(assigned_to_id author_id watcher_id).include?(field)
429 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
429 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
430 end
430 end
431
431
432 sql = ''
432 sql = ''
433 if field =~ /^cf_(\d+)$/
433 if field =~ /^cf_(\d+)$/
434 # custom field
434 # custom field
435 db_table = CustomValue.table_name
435 db_table = CustomValue.table_name
436 db_field = 'value'
436 db_field = 'value'
437 is_custom_filter = true
437 is_custom_filter = true
438 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
438 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
439 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
439 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
440 elsif field == 'watcher_id'
440 elsif field == 'watcher_id'
441 db_table = Watcher.table_name
441 db_table = Watcher.table_name
442 db_field = 'user_id'
442 db_field = 'user_id'
443 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
443 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
444 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
444 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
445 elsif field == "member_of_group" # named field
445 elsif field == "member_of_group" # named field
446 if operator == '*' # Any group
446 if operator == '*' # Any group
447 groups = Group.all
447 groups = Group.all
448 operator = '=' # Override the operator since we want to find by assigned_to
448 operator = '=' # Override the operator since we want to find by assigned_to
449 elsif operator == "!*"
449 elsif operator == "!*"
450 groups = Group.all
450 groups = Group.all
451 operator = '!' # Override the operator since we want to find by assigned_to
451 operator = '!' # Override the operator since we want to find by assigned_to
452 else
452 else
453 groups = Group.find_all_by_id(v)
453 groups = Group.find_all_by_id(v)
454 end
454 end
455 groups ||= []
455 groups ||= []
456
456
457 members_of_groups = groups.inject([]) {|user_ids, group|
457 members_of_groups = groups.inject([]) {|user_ids, group|
458 if group && group.user_ids.present?
458 if group && group.user_ids.present?
459 user_ids << group.user_ids
459 user_ids << group.user_ids
460 end
460 end
461 user_ids.flatten.uniq.compact
461 user_ids.flatten.uniq.compact
462 }.sort.collect(&:to_s)
462 }.sort.collect(&:to_s)
463
463
464 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
464 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
465
465
466 elsif field == "assigned_to_role" # named field
466 elsif field == "assigned_to_role" # named field
467 if operator == "*" # Any Role
467 if operator == "*" # Any Role
468 roles = Role.givable
468 roles = Role.givable
469 operator = '=' # Override the operator since we want to find by assigned_to
469 operator = '=' # Override the operator since we want to find by assigned_to
470 elsif operator == "!*" # No role
470 elsif operator == "!*" # No role
471 roles = Role.givable
471 roles = Role.givable
472 operator = '!' # Override the operator since we want to find by assigned_to
472 operator = '!' # Override the operator since we want to find by assigned_to
473 else
473 else
474 roles = Role.givable.find_all_by_id(v)
474 roles = Role.givable.find_all_by_id(v)
475 end
475 end
476 roles ||= []
476 roles ||= []
477
477
478 members_of_roles = roles.inject([]) {|user_ids, role|
478 members_of_roles = roles.inject([]) {|user_ids, role|
479 if role && role.members
479 if role && role.members
480 user_ids << role.members.collect(&:user_id)
480 user_ids << role.members.collect(&:user_id)
481 end
481 end
482 user_ids.flatten.uniq.compact
482 user_ids.flatten.uniq.compact
483 }.sort.collect(&:to_s)
483 }.sort.collect(&:to_s)
484
484
485 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
485 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
486 else
486 else
487 # regular field
487 # regular field
488 db_table = Issue.table_name
488 db_table = Issue.table_name
489 db_field = field
489 db_field = field
490 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
490 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
491 end
491 end
492 filters_clauses << sql
492 filters_clauses << sql
493
493
494 end if filters and valid?
494 end if filters and valid?
495
495
496 (filters_clauses << project_statement).join(' AND ')
496 (filters_clauses << project_statement).join(' AND ')
497 end
497 end
498
498
499 # Returns the issue count
499 # Returns the issue count
500 def issue_count
500 def issue_count
501 Issue.count(:include => [:status, :project], :conditions => statement)
501 Issue.count(:include => [:status, :project], :conditions => statement)
502 rescue ::ActiveRecord::StatementInvalid => e
502 rescue ::ActiveRecord::StatementInvalid => e
503 raise StatementInvalid.new(e.message)
503 raise StatementInvalid.new(e.message)
504 end
504 end
505
505
506 # Returns the issue count by group or nil if query is not grouped
506 # Returns the issue count by group or nil if query is not grouped
507 def issue_count_by_group
507 def issue_count_by_group
508 r = nil
508 r = nil
509 if grouped?
509 if grouped?
510 begin
510 begin
511 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
511 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
512 r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
512 r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
513 rescue ActiveRecord::RecordNotFound
513 rescue ActiveRecord::RecordNotFound
514 r = {nil => issue_count}
514 r = {nil => issue_count}
515 end
515 end
516 c = group_by_column
516 c = group_by_column
517 if c.is_a?(QueryCustomFieldColumn)
517 if c.is_a?(QueryCustomFieldColumn)
518 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
518 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
519 end
519 end
520 end
520 end
521 r
521 r
522 rescue ::ActiveRecord::StatementInvalid => e
522 rescue ::ActiveRecord::StatementInvalid => e
523 raise StatementInvalid.new(e.message)
523 raise StatementInvalid.new(e.message)
524 end
524 end
525
525
526 # Returns the issues
526 # Returns the issues
527 # Valid options are :order, :offset, :limit, :include, :conditions
527 # Valid options are :order, :offset, :limit, :include, :conditions
528 def issues(options={})
528 def issues(options={})
529 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
529 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
530 order_option = nil if order_option.blank?
530 order_option = nil if order_option.blank?
531
531
532 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
532 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
533 :conditions => Query.merge_conditions(statement, options[:conditions]),
533 :conditions => Query.merge_conditions(statement, options[:conditions]),
534 :order => order_option,
534 :order => order_option,
535 :limit => options[:limit],
535 :limit => options[:limit],
536 :offset => options[:offset]
536 :offset => options[:offset]
537 rescue ::ActiveRecord::StatementInvalid => e
537 rescue ::ActiveRecord::StatementInvalid => e
538 raise StatementInvalid.new(e.message)
538 raise StatementInvalid.new(e.message)
539 end
539 end
540
540
541 # Returns the journals
541 # Returns the journals
542 # Valid options are :order, :offset, :limit
542 # Valid options are :order, :offset, :limit
543 def journals(options={})
543 def journals(options={})
544 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
544 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
545 :conditions => statement,
545 :conditions => statement,
546 :order => options[:order],
546 :order => options[:order],
547 :limit => options[:limit],
547 :limit => options[:limit],
548 :offset => options[:offset]
548 :offset => options[:offset]
549 rescue ::ActiveRecord::StatementInvalid => e
549 rescue ::ActiveRecord::StatementInvalid => e
550 raise StatementInvalid.new(e.message)
550 raise StatementInvalid.new(e.message)
551 end
551 end
552
552
553 # Returns the versions
553 # Returns the versions
554 # Valid options are :conditions
554 # Valid options are :conditions
555 def versions(options={})
555 def versions(options={})
556 Version.find :all, :include => :project,
556 Version.find :all, :include => :project,
557 :conditions => Query.merge_conditions(project_statement, options[:conditions])
557 :conditions => Query.merge_conditions(project_statement, options[:conditions])
558 rescue ::ActiveRecord::StatementInvalid => e
558 rescue ::ActiveRecord::StatementInvalid => e
559 raise StatementInvalid.new(e.message)
559 raise StatementInvalid.new(e.message)
560 end
560 end
561
561
562 private
562 private
563
563
564 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
564 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
565 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
565 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
566 sql = ''
566 sql = ''
567 case operator
567 case operator
568 when "="
568 when "="
569 if value.any?
569 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
570 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
571 else
572 # IN an empty set
573 sql = "1=0"
574 end
570 when "!"
575 when "!"
576 if value.any?
571 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
577 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
578 else
579 # NOT IN an empty set
580 sql = "1=1"
581 end
572 when "!*"
582 when "!*"
573 sql = "#{db_table}.#{db_field} IS NULL"
583 sql = "#{db_table}.#{db_field} IS NULL"
574 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
584 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
575 when "*"
585 when "*"
576 sql = "#{db_table}.#{db_field} IS NOT NULL"
586 sql = "#{db_table}.#{db_field} IS NOT NULL"
577 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
587 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
578 when ">="
588 when ">="
579 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
589 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
580 when "<="
590 when "<="
581 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
591 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
582 when "o"
592 when "o"
583 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
593 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
584 when "c"
594 when "c"
585 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
595 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
586 when ">t-"
596 when ">t-"
587 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
597 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
588 when "<t-"
598 when "<t-"
589 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
599 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
590 when "t-"
600 when "t-"
591 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
601 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
592 when ">t+"
602 when ">t+"
593 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
603 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
594 when "<t+"
604 when "<t+"
595 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
605 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
596 when "t+"
606 when "t+"
597 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
607 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
598 when "t"
608 when "t"
599 sql = date_range_clause(db_table, db_field, 0, 0)
609 sql = date_range_clause(db_table, db_field, 0, 0)
600 when "w"
610 when "w"
601 from = l(:general_first_day_of_week) == '7' ?
611 from = l(:general_first_day_of_week) == '7' ?
602 # week starts on sunday
612 # week starts on sunday
603 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
613 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
604 # week starts on monday (Rails default)
614 # week starts on monday (Rails default)
605 Time.now.at_beginning_of_week
615 Time.now.at_beginning_of_week
606 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
616 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
607 when "~"
617 when "~"
608 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
618 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
609 when "!~"
619 when "!~"
610 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
620 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
611 end
621 end
612
622
613 return sql
623 return sql
614 end
624 end
615
625
616 def add_custom_fields_filters(custom_fields)
626 def add_custom_fields_filters(custom_fields)
617 @available_filters ||= {}
627 @available_filters ||= {}
618
628
619 custom_fields.select(&:is_filter?).each do |field|
629 custom_fields.select(&:is_filter?).each do |field|
620 case field.field_format
630 case field.field_format
621 when "text"
631 when "text"
622 options = { :type => :text, :order => 20 }
632 options = { :type => :text, :order => 20 }
623 when "list"
633 when "list"
624 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
634 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
625 when "date"
635 when "date"
626 options = { :type => :date, :order => 20 }
636 options = { :type => :date, :order => 20 }
627 when "bool"
637 when "bool"
628 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
638 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
629 else
639 else
630 options = { :type => :string, :order => 20 }
640 options = { :type => :string, :order => 20 }
631 end
641 end
632 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
642 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
633 end
643 end
634 end
644 end
635
645
636 # Returns a SQL clause for a date or datetime field.
646 # Returns a SQL clause for a date or datetime field.
637 def date_range_clause(table, field, from, to)
647 def date_range_clause(table, field, from, to)
638 s = []
648 s = []
639 if from
649 if from
640 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
650 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
641 end
651 end
642 if to
652 if to
643 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
653 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
644 end
654 end
645 s.join(' AND ')
655 s.join(' AND ')
646 end
656 end
647 end
657 end
@@ -1,546 +1,576
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 fixtures :projects, :enabled_modules, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :watchers, :custom_fields, :custom_values, :versions, :queries
21 fixtures :projects, :enabled_modules, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :watchers, :custom_fields, :custom_values, :versions, :queries
22
22
23 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
23 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
24 query = Query.new(:project => nil, :name => '_')
24 query = Query.new(:project => nil, :name => '_')
25 assert query.available_filters.has_key?('cf_1')
25 assert query.available_filters.has_key?('cf_1')
26 assert !query.available_filters.has_key?('cf_3')
26 assert !query.available_filters.has_key?('cf_3')
27 end
27 end
28
28
29 def test_system_shared_versions_should_be_available_in_global_queries
29 def test_system_shared_versions_should_be_available_in_global_queries
30 Version.find(2).update_attribute :sharing, 'system'
30 Version.find(2).update_attribute :sharing, 'system'
31 query = Query.new(:project => nil, :name => '_')
31 query = Query.new(:project => nil, :name => '_')
32 assert query.available_filters.has_key?('fixed_version_id')
32 assert query.available_filters.has_key?('fixed_version_id')
33 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
33 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
34 end
34 end
35
35
36 def test_project_filter_in_global_queries
36 def test_project_filter_in_global_queries
37 query = Query.new(:project => nil, :name => '_')
37 query = Query.new(:project => nil, :name => '_')
38 project_filter = query.available_filters["project_id"]
38 project_filter = query.available_filters["project_id"]
39 assert_not_nil project_filter
39 assert_not_nil project_filter
40 project_ids = project_filter[:values].map{|p| p[1]}
40 project_ids = project_filter[:values].map{|p| p[1]}
41 assert project_ids.include?("1") #public project
41 assert project_ids.include?("1") #public project
42 assert !project_ids.include?("2") #private project user cannot see
42 assert !project_ids.include?("2") #private project user cannot see
43 end
43 end
44
44
45 def find_issues_with_query(query)
45 def find_issues_with_query(query)
46 Issue.find :all,
46 Issue.find :all,
47 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
47 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
48 :conditions => query.statement
48 :conditions => query.statement
49 end
49 end
50
50
51 def assert_find_issues_with_query_is_successful(query)
51 def assert_find_issues_with_query_is_successful(query)
52 assert_nothing_raised do
52 assert_nothing_raised do
53 find_issues_with_query(query)
53 find_issues_with_query(query)
54 end
54 end
55 end
55 end
56
56
57 def assert_query_statement_includes(query, condition)
57 def assert_query_statement_includes(query, condition)
58 assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
58 assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
59 end
59 end
60
60
61 def test_query_should_allow_shared_versions_for_a_project_query
61 def test_query_should_allow_shared_versions_for_a_project_query
62 subproject_version = Version.find(4)
62 subproject_version = Version.find(4)
63 query = Query.new(:project => Project.find(1), :name => '_')
63 query = Query.new(:project => Project.find(1), :name => '_')
64 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
64 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
65
65
66 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
66 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
67 end
67 end
68
68
69 def test_query_with_multiple_custom_fields
69 def test_query_with_multiple_custom_fields
70 query = Query.find(1)
70 query = Query.find(1)
71 assert query.valid?
71 assert query.valid?
72 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
72 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
73 issues = find_issues_with_query(query)
73 issues = find_issues_with_query(query)
74 assert_equal 1, issues.length
74 assert_equal 1, issues.length
75 assert_equal Issue.find(3), issues.first
75 assert_equal Issue.find(3), issues.first
76 end
76 end
77
77
78 def test_operator_none
78 def test_operator_none
79 query = Query.new(:project => Project.find(1), :name => '_')
79 query = Query.new(:project => Project.find(1), :name => '_')
80 query.add_filter('fixed_version_id', '!*', [''])
80 query.add_filter('fixed_version_id', '!*', [''])
81 query.add_filter('cf_1', '!*', [''])
81 query.add_filter('cf_1', '!*', [''])
82 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
82 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
83 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
83 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
84 find_issues_with_query(query)
84 find_issues_with_query(query)
85 end
85 end
86
86
87 def test_operator_none_for_integer
87 def test_operator_none_for_integer
88 query = Query.new(:project => Project.find(1), :name => '_')
88 query = Query.new(:project => Project.find(1), :name => '_')
89 query.add_filter('estimated_hours', '!*', [''])
89 query.add_filter('estimated_hours', '!*', [''])
90 issues = find_issues_with_query(query)
90 issues = find_issues_with_query(query)
91 assert !issues.empty?
91 assert !issues.empty?
92 assert issues.all? {|i| !i.estimated_hours}
92 assert issues.all? {|i| !i.estimated_hours}
93 end
93 end
94
94
95 def test_operator_all
95 def test_operator_all
96 query = Query.new(:project => Project.find(1), :name => '_')
96 query = Query.new(:project => Project.find(1), :name => '_')
97 query.add_filter('fixed_version_id', '*', [''])
97 query.add_filter('fixed_version_id', '*', [''])
98 query.add_filter('cf_1', '*', [''])
98 query.add_filter('cf_1', '*', [''])
99 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
99 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
100 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
100 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
101 find_issues_with_query(query)
101 find_issues_with_query(query)
102 end
102 end
103
103
104 def test_operator_greater_than
104 def test_operator_greater_than
105 query = Query.new(:project => Project.find(1), :name => '_')
105 query = Query.new(:project => Project.find(1), :name => '_')
106 query.add_filter('done_ratio', '>=', ['40'])
106 query.add_filter('done_ratio', '>=', ['40'])
107 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
107 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
108 find_issues_with_query(query)
108 find_issues_with_query(query)
109 end
109 end
110
110
111 def test_operator_in_more_than
111 def test_operator_in_more_than
112 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
112 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
113 query = Query.new(:project => Project.find(1), :name => '_')
113 query = Query.new(:project => Project.find(1), :name => '_')
114 query.add_filter('due_date', '>t+', ['15'])
114 query.add_filter('due_date', '>t+', ['15'])
115 issues = find_issues_with_query(query)
115 issues = find_issues_with_query(query)
116 assert !issues.empty?
116 assert !issues.empty?
117 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
117 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
118 end
118 end
119
119
120 def test_operator_in_less_than
120 def test_operator_in_less_than
121 query = Query.new(:project => Project.find(1), :name => '_')
121 query = Query.new(:project => Project.find(1), :name => '_')
122 query.add_filter('due_date', '<t+', ['15'])
122 query.add_filter('due_date', '<t+', ['15'])
123 issues = find_issues_with_query(query)
123 issues = find_issues_with_query(query)
124 assert !issues.empty?
124 assert !issues.empty?
125 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
125 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
126 end
126 end
127
127
128 def test_operator_less_than_ago
128 def test_operator_less_than_ago
129 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
129 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
130 query = Query.new(:project => Project.find(1), :name => '_')
130 query = Query.new(:project => Project.find(1), :name => '_')
131 query.add_filter('due_date', '>t-', ['3'])
131 query.add_filter('due_date', '>t-', ['3'])
132 issues = find_issues_with_query(query)
132 issues = find_issues_with_query(query)
133 assert !issues.empty?
133 assert !issues.empty?
134 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
134 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
135 end
135 end
136
136
137 def test_operator_more_than_ago
137 def test_operator_more_than_ago
138 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
138 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
139 query = Query.new(:project => Project.find(1), :name => '_')
139 query = Query.new(:project => Project.find(1), :name => '_')
140 query.add_filter('due_date', '<t-', ['10'])
140 query.add_filter('due_date', '<t-', ['10'])
141 assert query.statement.include?("#{Issue.table_name}.due_date <=")
141 assert query.statement.include?("#{Issue.table_name}.due_date <=")
142 issues = find_issues_with_query(query)
142 issues = find_issues_with_query(query)
143 assert !issues.empty?
143 assert !issues.empty?
144 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
144 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
145 end
145 end
146
146
147 def test_operator_in
147 def test_operator_in
148 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
148 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
149 query = Query.new(:project => Project.find(1), :name => '_')
149 query = Query.new(:project => Project.find(1), :name => '_')
150 query.add_filter('due_date', 't+', ['2'])
150 query.add_filter('due_date', 't+', ['2'])
151 issues = find_issues_with_query(query)
151 issues = find_issues_with_query(query)
152 assert !issues.empty?
152 assert !issues.empty?
153 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
153 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
154 end
154 end
155
155
156 def test_operator_ago
156 def test_operator_ago
157 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
157 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
158 query = Query.new(:project => Project.find(1), :name => '_')
158 query = Query.new(:project => Project.find(1), :name => '_')
159 query.add_filter('due_date', 't-', ['3'])
159 query.add_filter('due_date', 't-', ['3'])
160 issues = find_issues_with_query(query)
160 issues = find_issues_with_query(query)
161 assert !issues.empty?
161 assert !issues.empty?
162 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
162 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
163 end
163 end
164
164
165 def test_operator_today
165 def test_operator_today
166 query = Query.new(:project => Project.find(1), :name => '_')
166 query = Query.new(:project => Project.find(1), :name => '_')
167 query.add_filter('due_date', 't', [''])
167 query.add_filter('due_date', 't', [''])
168 issues = find_issues_with_query(query)
168 issues = find_issues_with_query(query)
169 assert !issues.empty?
169 assert !issues.empty?
170 issues.each {|issue| assert_equal Date.today, issue.due_date}
170 issues.each {|issue| assert_equal Date.today, issue.due_date}
171 end
171 end
172
172
173 def test_operator_this_week_on_date
173 def test_operator_this_week_on_date
174 query = Query.new(:project => Project.find(1), :name => '_')
174 query = Query.new(:project => Project.find(1), :name => '_')
175 query.add_filter('due_date', 'w', [''])
175 query.add_filter('due_date', 'w', [''])
176 find_issues_with_query(query)
176 find_issues_with_query(query)
177 end
177 end
178
178
179 def test_operator_this_week_on_datetime
179 def test_operator_this_week_on_datetime
180 query = Query.new(:project => Project.find(1), :name => '_')
180 query = Query.new(:project => Project.find(1), :name => '_')
181 query.add_filter('created_on', 'w', [''])
181 query.add_filter('created_on', 'w', [''])
182 find_issues_with_query(query)
182 find_issues_with_query(query)
183 end
183 end
184
184
185 def test_operator_contains
185 def test_operator_contains
186 query = Query.new(:project => Project.find(1), :name => '_')
186 query = Query.new(:project => Project.find(1), :name => '_')
187 query.add_filter('subject', '~', ['uNable'])
187 query.add_filter('subject', '~', ['uNable'])
188 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
188 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
189 result = find_issues_with_query(query)
189 result = find_issues_with_query(query)
190 assert result.empty?
190 assert result.empty?
191 result.each {|issue| assert issue.subject.downcase.include?('unable') }
191 result.each {|issue| assert issue.subject.downcase.include?('unable') }
192 end
192 end
193
193
194 def test_operator_does_not_contains
194 def test_operator_does_not_contains
195 query = Query.new(:project => Project.find(1), :name => '_')
195 query = Query.new(:project => Project.find(1), :name => '_')
196 query.add_filter('subject', '!~', ['uNable'])
196 query.add_filter('subject', '!~', ['uNable'])
197 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
197 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
198 find_issues_with_query(query)
198 find_issues_with_query(query)
199 end
199 end
200
200
201 def test_filter_watched_issues
201 def test_filter_watched_issues
202 User.current = User.find(1)
202 User.current = User.find(1)
203 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
203 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
204 result = find_issues_with_query(query)
204 result = find_issues_with_query(query)
205 assert_not_nil result
205 assert_not_nil result
206 assert !result.empty?
206 assert !result.empty?
207 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
207 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
208 User.current = nil
208 User.current = nil
209 end
209 end
210
210
211 def test_filter_unwatched_issues
211 def test_filter_unwatched_issues
212 User.current = User.find(1)
212 User.current = User.find(1)
213 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
213 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
214 result = find_issues_with_query(query)
214 result = find_issues_with_query(query)
215 assert_not_nil result
215 assert_not_nil result
216 assert !result.empty?
216 assert !result.empty?
217 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
217 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
218 User.current = nil
218 User.current = nil
219 end
219 end
220
220
221 def test_default_columns
221 def test_default_columns
222 q = Query.new
222 q = Query.new
223 assert !q.columns.empty?
223 assert !q.columns.empty?
224 end
224 end
225
225
226 def test_set_column_names
226 def test_set_column_names
227 q = Query.new
227 q = Query.new
228 q.column_names = ['tracker', :subject, '', 'unknonw_column']
228 q.column_names = ['tracker', :subject, '', 'unknonw_column']
229 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
229 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
230 c = q.columns.first
230 c = q.columns.first
231 assert q.has_column?(c)
231 assert q.has_column?(c)
232 end
232 end
233
233
234 def test_groupable_columns_should_include_custom_fields
234 def test_groupable_columns_should_include_custom_fields
235 q = Query.new
235 q = Query.new
236 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
236 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
237 end
237 end
238
238
239 def test_grouped_with_valid_column
239 def test_grouped_with_valid_column
240 q = Query.new(:group_by => 'status')
240 q = Query.new(:group_by => 'status')
241 assert q.grouped?
241 assert q.grouped?
242 assert_not_nil q.group_by_column
242 assert_not_nil q.group_by_column
243 assert_equal :status, q.group_by_column.name
243 assert_equal :status, q.group_by_column.name
244 assert_not_nil q.group_by_statement
244 assert_not_nil q.group_by_statement
245 assert_equal 'status', q.group_by_statement
245 assert_equal 'status', q.group_by_statement
246 end
246 end
247
247
248 def test_grouped_with_invalid_column
248 def test_grouped_with_invalid_column
249 q = Query.new(:group_by => 'foo')
249 q = Query.new(:group_by => 'foo')
250 assert !q.grouped?
250 assert !q.grouped?
251 assert_nil q.group_by_column
251 assert_nil q.group_by_column
252 assert_nil q.group_by_statement
252 assert_nil q.group_by_statement
253 end
253 end
254
254
255 def test_default_sort
255 def test_default_sort
256 q = Query.new
256 q = Query.new
257 assert_equal [], q.sort_criteria
257 assert_equal [], q.sort_criteria
258 end
258 end
259
259
260 def test_set_sort_criteria_with_hash
260 def test_set_sort_criteria_with_hash
261 q = Query.new
261 q = Query.new
262 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
262 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
263 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
263 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
264 end
264 end
265
265
266 def test_set_sort_criteria_with_array
266 def test_set_sort_criteria_with_array
267 q = Query.new
267 q = Query.new
268 q.sort_criteria = [['priority', 'desc'], 'tracker']
268 q.sort_criteria = [['priority', 'desc'], 'tracker']
269 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
269 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
270 end
270 end
271
271
272 def test_create_query_with_sort
272 def test_create_query_with_sort
273 q = Query.new(:name => 'Sorted')
273 q = Query.new(:name => 'Sorted')
274 q.sort_criteria = [['priority', 'desc'], 'tracker']
274 q.sort_criteria = [['priority', 'desc'], 'tracker']
275 assert q.save
275 assert q.save
276 q.reload
276 q.reload
277 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
277 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
278 end
278 end
279
279
280 def test_sort_by_string_custom_field_asc
280 def test_sort_by_string_custom_field_asc
281 q = Query.new
281 q = Query.new
282 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
282 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
283 assert c
283 assert c
284 assert c.sortable
284 assert c.sortable
285 issues = Issue.find :all,
285 issues = Issue.find :all,
286 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
286 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
287 :conditions => q.statement,
287 :conditions => q.statement,
288 :order => "#{c.sortable} ASC"
288 :order => "#{c.sortable} ASC"
289 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
289 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
290 assert !values.empty?
290 assert !values.empty?
291 assert_equal values.sort, values
291 assert_equal values.sort, values
292 end
292 end
293
293
294 def test_sort_by_string_custom_field_desc
294 def test_sort_by_string_custom_field_desc
295 q = Query.new
295 q = Query.new
296 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
296 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
297 assert c
297 assert c
298 assert c.sortable
298 assert c.sortable
299 issues = Issue.find :all,
299 issues = Issue.find :all,
300 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
300 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
301 :conditions => q.statement,
301 :conditions => q.statement,
302 :order => "#{c.sortable} DESC"
302 :order => "#{c.sortable} DESC"
303 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
303 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
304 assert !values.empty?
304 assert !values.empty?
305 assert_equal values.sort.reverse, values
305 assert_equal values.sort.reverse, values
306 end
306 end
307
307
308 def test_sort_by_float_custom_field_asc
308 def test_sort_by_float_custom_field_asc
309 q = Query.new
309 q = Query.new
310 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
310 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
311 assert c
311 assert c
312 assert c.sortable
312 assert c.sortable
313 issues = Issue.find :all,
313 issues = Issue.find :all,
314 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
314 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
315 :conditions => q.statement,
315 :conditions => q.statement,
316 :order => "#{c.sortable} ASC"
316 :order => "#{c.sortable} ASC"
317 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
317 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
318 assert !values.empty?
318 assert !values.empty?
319 assert_equal values.sort, values
319 assert_equal values.sort, values
320 end
320 end
321
321
322 def test_invalid_query_should_raise_query_statement_invalid_error
322 def test_invalid_query_should_raise_query_statement_invalid_error
323 q = Query.new
323 q = Query.new
324 assert_raise Query::StatementInvalid do
324 assert_raise Query::StatementInvalid do
325 q.issues(:conditions => "foo = 1")
325 q.issues(:conditions => "foo = 1")
326 end
326 end
327 end
327 end
328
328
329 def test_issue_count_by_association_group
329 def test_issue_count_by_association_group
330 q = Query.new(:name => '_', :group_by => 'assigned_to')
330 q = Query.new(:name => '_', :group_by => 'assigned_to')
331 count_by_group = q.issue_count_by_group
331 count_by_group = q.issue_count_by_group
332 assert_kind_of Hash, count_by_group
332 assert_kind_of Hash, count_by_group
333 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
333 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
334 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
334 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
335 assert count_by_group.has_key?(User.find(3))
335 assert count_by_group.has_key?(User.find(3))
336 end
336 end
337
337
338 def test_issue_count_by_list_custom_field_group
338 def test_issue_count_by_list_custom_field_group
339 q = Query.new(:name => '_', :group_by => 'cf_1')
339 q = Query.new(:name => '_', :group_by => 'cf_1')
340 count_by_group = q.issue_count_by_group
340 count_by_group = q.issue_count_by_group
341 assert_kind_of Hash, count_by_group
341 assert_kind_of Hash, count_by_group
342 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
342 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
343 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
343 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
344 assert count_by_group.has_key?('MySQL')
344 assert count_by_group.has_key?('MySQL')
345 end
345 end
346
346
347 def test_issue_count_by_date_custom_field_group
347 def test_issue_count_by_date_custom_field_group
348 q = Query.new(:name => '_', :group_by => 'cf_8')
348 q = Query.new(:name => '_', :group_by => 'cf_8')
349 count_by_group = q.issue_count_by_group
349 count_by_group = q.issue_count_by_group
350 assert_kind_of Hash, count_by_group
350 assert_kind_of Hash, count_by_group
351 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
351 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
352 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
352 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
353 end
353 end
354
354
355 def test_label_for
355 def test_label_for
356 q = Query.new
356 q = Query.new
357 assert_equal 'assigned_to', q.label_for('assigned_to_id')
357 assert_equal 'assigned_to', q.label_for('assigned_to_id')
358 end
358 end
359
359
360 def test_editable_by
360 def test_editable_by
361 admin = User.find(1)
361 admin = User.find(1)
362 manager = User.find(2)
362 manager = User.find(2)
363 developer = User.find(3)
363 developer = User.find(3)
364
364
365 # Public query on project 1
365 # Public query on project 1
366 q = Query.find(1)
366 q = Query.find(1)
367 assert q.editable_by?(admin)
367 assert q.editable_by?(admin)
368 assert q.editable_by?(manager)
368 assert q.editable_by?(manager)
369 assert !q.editable_by?(developer)
369 assert !q.editable_by?(developer)
370
370
371 # Private query on project 1
371 # Private query on project 1
372 q = Query.find(2)
372 q = Query.find(2)
373 assert q.editable_by?(admin)
373 assert q.editable_by?(admin)
374 assert !q.editable_by?(manager)
374 assert !q.editable_by?(manager)
375 assert q.editable_by?(developer)
375 assert q.editable_by?(developer)
376
376
377 # Private query for all projects
377 # Private query for all projects
378 q = Query.find(3)
378 q = Query.find(3)
379 assert q.editable_by?(admin)
379 assert q.editable_by?(admin)
380 assert !q.editable_by?(manager)
380 assert !q.editable_by?(manager)
381 assert q.editable_by?(developer)
381 assert q.editable_by?(developer)
382
382
383 # Public query for all projects
383 # Public query for all projects
384 q = Query.find(4)
384 q = Query.find(4)
385 assert q.editable_by?(admin)
385 assert q.editable_by?(admin)
386 assert !q.editable_by?(manager)
386 assert !q.editable_by?(manager)
387 assert !q.editable_by?(developer)
387 assert !q.editable_by?(developer)
388 end
388 end
389
389
390 context "#available_filters" do
390 context "#available_filters" do
391 setup do
391 setup do
392 @query = Query.new(:name => "_")
392 @query = Query.new(:name => "_")
393 end
393 end
394
394
395 should "include users of visible projects in cross-project view" do
395 should "include users of visible projects in cross-project view" do
396 users = @query.available_filters["assigned_to_id"]
396 users = @query.available_filters["assigned_to_id"]
397 assert_not_nil users
397 assert_not_nil users
398 assert users[:values].map{|u|u[1]}.include?("3")
398 assert users[:values].map{|u|u[1]}.include?("3")
399 end
399 end
400
400
401 should "include visible projects in cross-project view" do
401 should "include visible projects in cross-project view" do
402 projects = @query.available_filters["project_id"]
402 projects = @query.available_filters["project_id"]
403 assert_not_nil projects
403 assert_not_nil projects
404 assert projects[:values].map{|u|u[1]}.include?("1")
404 assert projects[:values].map{|u|u[1]}.include?("1")
405 end
405 end
406
406
407 context "'member_of_group' filter" do
407 context "'member_of_group' filter" do
408 should "be present" do
408 should "be present" do
409 assert @query.available_filters.keys.include?("member_of_group")
409 assert @query.available_filters.keys.include?("member_of_group")
410 end
410 end
411
411
412 should "be an optional list" do
412 should "be an optional list" do
413 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
413 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
414 end
414 end
415
415
416 should "have a list of the groups as values" do
416 should "have a list of the groups as values" do
417 Group.destroy_all # No fixtures
417 Group.destroy_all # No fixtures
418 group1 = Group.generate!.reload
418 group1 = Group.generate!.reload
419 group2 = Group.generate!.reload
419 group2 = Group.generate!.reload
420
420
421 expected_group_list = [
421 expected_group_list = [
422 [group1.name, group1.id.to_s],
422 [group1.name, group1.id.to_s],
423 [group2.name, group2.id.to_s]
423 [group2.name, group2.id.to_s]
424 ]
424 ]
425 assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort
425 assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort
426 end
426 end
427
427
428 end
428 end
429
429
430 context "'assigned_to_role' filter" do
430 context "'assigned_to_role' filter" do
431 should "be present" do
431 should "be present" do
432 assert @query.available_filters.keys.include?("assigned_to_role")
432 assert @query.available_filters.keys.include?("assigned_to_role")
433 end
433 end
434
434
435 should "be an optional list" do
435 should "be an optional list" do
436 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
436 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
437 end
437 end
438
438
439 should "have a list of the Roles as values" do
439 should "have a list of the Roles as values" do
440 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
440 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
441 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
441 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
442 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
442 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
443 end
443 end
444
444
445 should "not include the built in Roles as values" do
445 should "not include the built in Roles as values" do
446 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
446 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
447 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
447 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
448 end
448 end
449
449
450 end
450 end
451
451
452 end
452 end
453
453
454 context "#statement" do
454 context "#statement" do
455 context "with 'member_of_group' filter" do
455 context "with 'member_of_group' filter" do
456 setup do
456 setup do
457 Group.destroy_all # No fixtures
457 Group.destroy_all # No fixtures
458 @user_in_group = User.generate!
458 @user_in_group = User.generate!
459 @second_user_in_group = User.generate!
459 @second_user_in_group = User.generate!
460 @user_in_group2 = User.generate!
460 @user_in_group2 = User.generate!
461 @user_not_in_group = User.generate!
461 @user_not_in_group = User.generate!
462
462
463 @group = Group.generate!.reload
463 @group = Group.generate!.reload
464 @group.users << @user_in_group
464 @group.users << @user_in_group
465 @group.users << @second_user_in_group
465 @group.users << @second_user_in_group
466
466
467 @group2 = Group.generate!.reload
467 @group2 = Group.generate!.reload
468 @group2.users << @user_in_group2
468 @group2.users << @user_in_group2
469
469
470 end
470 end
471
471
472 should "search assigned to for users in the group" do
472 should "search assigned to for users in the group" do
473 @query = Query.new(:name => '_')
473 @query = Query.new(:name => '_')
474 @query.add_filter('member_of_group', '=', [@group.id.to_s])
474 @query.add_filter('member_of_group', '=', [@group.id.to_s])
475
475
476 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
476 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
477 assert_find_issues_with_query_is_successful @query
477 assert_find_issues_with_query_is_successful @query
478 end
478 end
479
479
480 should "search not assigned to any group member (none)" do
480 should "search not assigned to any group member (none)" do
481 @query = Query.new(:name => '_')
481 @query = Query.new(:name => '_')
482 @query.add_filter('member_of_group', '!*', [''])
482 @query.add_filter('member_of_group', '!*', [''])
483
483
484 # Users not in a group
484 # Users not in a group
485 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}')"
485 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}')"
486 assert_find_issues_with_query_is_successful @query
486 assert_find_issues_with_query_is_successful @query
487
488 end
487 end
489
488
490 should "search assigned to any group member (all)" do
489 should "search assigned to any group member (all)" do
491 @query = Query.new(:name => '_')
490 @query = Query.new(:name => '_')
492 @query.add_filter('member_of_group', '*', [''])
491 @query.add_filter('member_of_group', '*', [''])
493
492
494 # Only users in a group
493 # Only users in a group
495 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}')"
494 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}')"
496 assert_find_issues_with_query_is_successful @query
495 assert_find_issues_with_query_is_successful @query
496 end
497
497
498 should "return an empty set with = empty group" do
499 @empty_group = Group.generate!
500 @query = Query.new(:name => '_')
501 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
502
503 assert_equal [], find_issues_with_query(@query)
504 end
505
506 should "return issues with ! empty group" do
507 @empty_group = Group.generate!
508 @query = Query.new(:name => '_')
509 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
510
511 assert_find_issues_with_query_is_successful @query
498 end
512 end
499 end
513 end
500
514
501 context "with 'assigned_to_role' filter" do
515 context "with 'assigned_to_role' filter" do
502 setup do
516 setup do
503 # No fixtures
517 # No fixtures
504 MemberRole.delete_all
518 MemberRole.delete_all
505 Member.delete_all
519 Member.delete_all
506 Role.delete_all
520 Role.delete_all
507
521
508 @manager_role = Role.generate!(:name => 'Manager')
522 @manager_role = Role.generate!(:name => 'Manager')
509 @developer_role = Role.generate!(:name => 'Developer')
523 @developer_role = Role.generate!(:name => 'Developer')
510
524
511 @project = Project.generate!
525 @project = Project.generate!
512 @manager = User.generate!
526 @manager = User.generate!
513 @developer = User.generate!
527 @developer = User.generate!
514 @boss = User.generate!
528 @boss = User.generate!
515 User.add_to_project(@manager, @project, @manager_role)
529 User.add_to_project(@manager, @project, @manager_role)
516 User.add_to_project(@developer, @project, @developer_role)
530 User.add_to_project(@developer, @project, @developer_role)
517 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
531 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
518 end
532 end
519
533
520 should "search assigned to for users with the Role" do
534 should "search assigned to for users with the Role" do
521 @query = Query.new(:name => '_')
535 @query = Query.new(:name => '_')
522 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
536 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
523
537
524 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@manager.id}','#{@boss.id}')"
538 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@manager.id}','#{@boss.id}')"
525 assert_find_issues_with_query_is_successful @query
539 assert_find_issues_with_query_is_successful @query
526 end
540 end
527
541
528 should "search assigned to for users not assigned to any Role (none)" do
542 should "search assigned to for users not assigned to any Role (none)" do
529 @query = Query.new(:name => '_')
543 @query = Query.new(:name => '_')
530 @query.add_filter('assigned_to_role', '!*', [''])
544 @query.add_filter('assigned_to_role', '!*', [''])
531
545
532 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@manager.id}','#{@developer.id}','#{@boss.id}')"
546 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@manager.id}','#{@developer.id}','#{@boss.id}')"
533 assert_find_issues_with_query_is_successful @query
547 assert_find_issues_with_query_is_successful @query
534 end
548 end
535
549
536 should "search assigned to for users assigned to any Role (all)" do
550 should "search assigned to for users assigned to any Role (all)" do
537 @query = Query.new(:name => '_')
551 @query = Query.new(:name => '_')
538 @query.add_filter('assigned_to_role', '*', [''])
552 @query.add_filter('assigned_to_role', '*', [''])
539
553
540 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@manager.id}','#{@developer.id}','#{@boss.id}')"
554 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@manager.id}','#{@developer.id}','#{@boss.id}')"
541 assert_find_issues_with_query_is_successful @query
555 assert_find_issues_with_query_is_successful @query
542 end
556 end
557
558 should "return an empty set with empty role" do
559 @empty_role = Role.generate!
560 @query = Query.new(:name => '_')
561 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
562
563 assert_equal [], find_issues_with_query(@query)
564 end
565
566 should "return issues with ! empty role" do
567 @empty_role = Role.generate!
568 @query = Query.new(:name => '_')
569 @query.add_filter('member_of_group', '!', [@empty_role.id.to_s])
570
571 assert_find_issues_with_query_is_successful @query
572 end
543 end
573 end
544 end
574 end
545
575
546 end
576 end
General Comments 0
You need to be logged in to leave comments. Login now