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