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