##// END OF EJS Templates
Fixed that Query#has_column? returns false with default columns....
Jean-Philippe Lang -
r15835:4a5ebfb77845
parent child
Show More
@@ -1,1311 +1,1312
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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, :totalable, :default_order
19 attr_accessor :name, :sortable, :groupable, :totalable, :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.totalable = options[:totalable] || false
29 self.totalable = options[:totalable] || false
30 self.default_order = options[:default_order]
30 self.default_order = options[:default_order]
31 @inline = options.key?(:inline) ? options[:inline] : true
31 @inline = options.key?(:inline) ? options[:inline] : true
32 @caption_key = options[:caption] || "field_#{name}".to_sym
32 @caption_key = options[:caption] || "field_#{name}".to_sym
33 @frozen = options[:frozen]
33 @frozen = options[:frozen]
34 end
34 end
35
35
36 def caption
36 def caption
37 case @caption_key
37 case @caption_key
38 when Symbol
38 when Symbol
39 l(@caption_key)
39 l(@caption_key)
40 when Proc
40 when Proc
41 @caption_key.call
41 @caption_key.call
42 else
42 else
43 @caption_key
43 @caption_key
44 end
44 end
45 end
45 end
46
46
47 # Returns true if the column is sortable, otherwise false
47 # Returns true if the column is sortable, otherwise false
48 def sortable?
48 def sortable?
49 !@sortable.nil?
49 !@sortable.nil?
50 end
50 end
51
51
52 def sortable
52 def sortable
53 @sortable.is_a?(Proc) ? @sortable.call : @sortable
53 @sortable.is_a?(Proc) ? @sortable.call : @sortable
54 end
54 end
55
55
56 def inline?
56 def inline?
57 @inline
57 @inline
58 end
58 end
59
59
60 def frozen?
60 def frozen?
61 @frozen
61 @frozen
62 end
62 end
63
63
64 def value(object)
64 def value(object)
65 object.send name
65 object.send name
66 end
66 end
67
67
68 def value_object(object)
68 def value_object(object)
69 object.send name
69 object.send name
70 end
70 end
71
71
72 def css_classes
72 def css_classes
73 name
73 name
74 end
74 end
75 end
75 end
76
76
77 class QueryAssociationColumn < QueryColumn
77 class QueryAssociationColumn < QueryColumn
78
78
79 def initialize(association, attribute, options={})
79 def initialize(association, attribute, options={})
80 @association = association
80 @association = association
81 @attribute = attribute
81 @attribute = attribute
82 name_with_assoc = "#{association}.#{attribute}".to_sym
82 name_with_assoc = "#{association}.#{attribute}".to_sym
83 super(name_with_assoc, options)
83 super(name_with_assoc, options)
84 end
84 end
85
85
86 def value_object(object)
86 def value_object(object)
87 if assoc = object.send(@association)
87 if assoc = object.send(@association)
88 assoc.send @attribute
88 assoc.send @attribute
89 end
89 end
90 end
90 end
91
91
92 def css_classes
92 def css_classes
93 @css_classes ||= "#{@association}-#{@attribute}"
93 @css_classes ||= "#{@association}-#{@attribute}"
94 end
94 end
95 end
95 end
96
96
97 class QueryCustomFieldColumn < QueryColumn
97 class QueryCustomFieldColumn < QueryColumn
98
98
99 def initialize(custom_field, options={})
99 def initialize(custom_field, options={})
100 self.name = "cf_#{custom_field.id}".to_sym
100 self.name = "cf_#{custom_field.id}".to_sym
101 self.sortable = custom_field.order_statement || false
101 self.sortable = custom_field.order_statement || false
102 self.groupable = custom_field.group_statement || false
102 self.groupable = custom_field.group_statement || false
103 self.totalable = options.key?(:totalable) ? !!options[:totalable] : custom_field.totalable?
103 self.totalable = options.key?(:totalable) ? !!options[:totalable] : custom_field.totalable?
104 @inline = true
104 @inline = true
105 @cf = custom_field
105 @cf = custom_field
106 end
106 end
107
107
108 def caption
108 def caption
109 @cf.name
109 @cf.name
110 end
110 end
111
111
112 def custom_field
112 def custom_field
113 @cf
113 @cf
114 end
114 end
115
115
116 def value_object(object)
116 def value_object(object)
117 if custom_field.visible_by?(object.project, User.current)
117 if custom_field.visible_by?(object.project, User.current)
118 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
118 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
119 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
119 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
120 else
120 else
121 nil
121 nil
122 end
122 end
123 end
123 end
124
124
125 def value(object)
125 def value(object)
126 raw = value_object(object)
126 raw = value_object(object)
127 if raw.is_a?(Array)
127 if raw.is_a?(Array)
128 raw.map {|r| @cf.cast_value(r.value)}
128 raw.map {|r| @cf.cast_value(r.value)}
129 elsif raw
129 elsif raw
130 @cf.cast_value(raw.value)
130 @cf.cast_value(raw.value)
131 else
131 else
132 nil
132 nil
133 end
133 end
134 end
134 end
135
135
136 def css_classes
136 def css_classes
137 @css_classes ||= "#{name} #{@cf.field_format}"
137 @css_classes ||= "#{name} #{@cf.field_format}"
138 end
138 end
139 end
139 end
140
140
141 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
141 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
142
142
143 def initialize(association, custom_field, options={})
143 def initialize(association, custom_field, options={})
144 super(custom_field, options)
144 super(custom_field, options)
145 self.name = "#{association}.cf_#{custom_field.id}".to_sym
145 self.name = "#{association}.cf_#{custom_field.id}".to_sym
146 # TODO: support sorting/grouping by association custom field
146 # TODO: support sorting/grouping by association custom field
147 self.sortable = false
147 self.sortable = false
148 self.groupable = false
148 self.groupable = false
149 @association = association
149 @association = association
150 end
150 end
151
151
152 def value_object(object)
152 def value_object(object)
153 if assoc = object.send(@association)
153 if assoc = object.send(@association)
154 super(assoc)
154 super(assoc)
155 end
155 end
156 end
156 end
157
157
158 def css_classes
158 def css_classes
159 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
159 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
160 end
160 end
161 end
161 end
162
162
163 class QueryFilter
163 class QueryFilter
164 include Redmine::I18n
164 include Redmine::I18n
165
165
166 def initialize(field, options)
166 def initialize(field, options)
167 @field = field.to_s
167 @field = field.to_s
168 @options = options
168 @options = options
169 @options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
169 @options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
170 # Consider filters with a Proc for values as remote by default
170 # Consider filters with a Proc for values as remote by default
171 @remote = options.key?(:remote) ? options[:remote] : options[:values].is_a?(Proc)
171 @remote = options.key?(:remote) ? options[:remote] : options[:values].is_a?(Proc)
172 end
172 end
173
173
174 def [](arg)
174 def [](arg)
175 if arg == :values
175 if arg == :values
176 values
176 values
177 else
177 else
178 @options[arg]
178 @options[arg]
179 end
179 end
180 end
180 end
181
181
182 def values
182 def values
183 @values ||= begin
183 @values ||= begin
184 values = @options[:values]
184 values = @options[:values]
185 if values.is_a?(Proc)
185 if values.is_a?(Proc)
186 values = values.call
186 values = values.call
187 end
187 end
188 values
188 values
189 end
189 end
190 end
190 end
191
191
192 def remote
192 def remote
193 @remote
193 @remote
194 end
194 end
195 end
195 end
196
196
197 class Query < ActiveRecord::Base
197 class Query < ActiveRecord::Base
198 class StatementInvalid < ::ActiveRecord::StatementInvalid
198 class StatementInvalid < ::ActiveRecord::StatementInvalid
199 end
199 end
200
200
201 include Redmine::SubclassFactory
201 include Redmine::SubclassFactory
202
202
203 VISIBILITY_PRIVATE = 0
203 VISIBILITY_PRIVATE = 0
204 VISIBILITY_ROLES = 1
204 VISIBILITY_ROLES = 1
205 VISIBILITY_PUBLIC = 2
205 VISIBILITY_PUBLIC = 2
206
206
207 belongs_to :project
207 belongs_to :project
208 belongs_to :user
208 belongs_to :user
209 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
209 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
210 serialize :filters
210 serialize :filters
211 serialize :column_names
211 serialize :column_names
212 serialize :sort_criteria, Array
212 serialize :sort_criteria, Array
213 serialize :options, Hash
213 serialize :options, Hash
214
214
215 attr_protected :project_id, :user_id
215 attr_protected :project_id, :user_id
216
216
217 validates_presence_of :name
217 validates_presence_of :name
218 validates_length_of :name, :maximum => 255
218 validates_length_of :name, :maximum => 255
219 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
219 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
220 validate :validate_query_filters
220 validate :validate_query_filters
221 validate do |query|
221 validate do |query|
222 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
222 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
223 end
223 end
224
224
225 after_save do |query|
225 after_save do |query|
226 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
226 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
227 query.roles.clear
227 query.roles.clear
228 end
228 end
229 end
229 end
230
230
231 class_attribute :operators
231 class_attribute :operators
232 self.operators = {
232 self.operators = {
233 "=" => :label_equals,
233 "=" => :label_equals,
234 "!" => :label_not_equals,
234 "!" => :label_not_equals,
235 "o" => :label_open_issues,
235 "o" => :label_open_issues,
236 "c" => :label_closed_issues,
236 "c" => :label_closed_issues,
237 "!*" => :label_none,
237 "!*" => :label_none,
238 "*" => :label_any,
238 "*" => :label_any,
239 ">=" => :label_greater_or_equal,
239 ">=" => :label_greater_or_equal,
240 "<=" => :label_less_or_equal,
240 "<=" => :label_less_or_equal,
241 "><" => :label_between,
241 "><" => :label_between,
242 "<t+" => :label_in_less_than,
242 "<t+" => :label_in_less_than,
243 ">t+" => :label_in_more_than,
243 ">t+" => :label_in_more_than,
244 "><t+"=> :label_in_the_next_days,
244 "><t+"=> :label_in_the_next_days,
245 "t+" => :label_in,
245 "t+" => :label_in,
246 "t" => :label_today,
246 "t" => :label_today,
247 "ld" => :label_yesterday,
247 "ld" => :label_yesterday,
248 "w" => :label_this_week,
248 "w" => :label_this_week,
249 "lw" => :label_last_week,
249 "lw" => :label_last_week,
250 "l2w" => [:label_last_n_weeks, {:count => 2}],
250 "l2w" => [:label_last_n_weeks, {:count => 2}],
251 "m" => :label_this_month,
251 "m" => :label_this_month,
252 "lm" => :label_last_month,
252 "lm" => :label_last_month,
253 "y" => :label_this_year,
253 "y" => :label_this_year,
254 ">t-" => :label_less_than_ago,
254 ">t-" => :label_less_than_ago,
255 "<t-" => :label_more_than_ago,
255 "<t-" => :label_more_than_ago,
256 "><t-"=> :label_in_the_past_days,
256 "><t-"=> :label_in_the_past_days,
257 "t-" => :label_ago,
257 "t-" => :label_ago,
258 "~" => :label_contains,
258 "~" => :label_contains,
259 "!~" => :label_not_contains,
259 "!~" => :label_not_contains,
260 "=p" => :label_any_issues_in_project,
260 "=p" => :label_any_issues_in_project,
261 "=!p" => :label_any_issues_not_in_project,
261 "=!p" => :label_any_issues_not_in_project,
262 "!p" => :label_no_issues_in_project,
262 "!p" => :label_no_issues_in_project,
263 "*o" => :label_any_open_issues,
263 "*o" => :label_any_open_issues,
264 "!o" => :label_no_open_issues
264 "!o" => :label_no_open_issues
265 }
265 }
266
266
267 class_attribute :operators_by_filter_type
267 class_attribute :operators_by_filter_type
268 self.operators_by_filter_type = {
268 self.operators_by_filter_type = {
269 :list => [ "=", "!" ],
269 :list => [ "=", "!" ],
270 :list_status => [ "o", "=", "!", "c", "*" ],
270 :list_status => [ "o", "=", "!", "c", "*" ],
271 :list_optional => [ "=", "!", "!*", "*" ],
271 :list_optional => [ "=", "!", "!*", "*" ],
272 :list_subprojects => [ "*", "!*", "=", "!" ],
272 :list_subprojects => [ "*", "!*", "=", "!" ],
273 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
273 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
274 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
274 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
275 :string => [ "=", "~", "!", "!~", "!*", "*" ],
275 :string => [ "=", "~", "!", "!~", "!*", "*" ],
276 :text => [ "~", "!~", "!*", "*" ],
276 :text => [ "~", "!~", "!*", "*" ],
277 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
277 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
278 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
278 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
279 :relation => ["=", "=p", "=!p", "!p", "*o", "!o", "!*", "*"],
279 :relation => ["=", "=p", "=!p", "!p", "*o", "!o", "!*", "*"],
280 :tree => ["=", "~", "!*", "*"]
280 :tree => ["=", "~", "!*", "*"]
281 }
281 }
282
282
283 class_attribute :available_columns
283 class_attribute :available_columns
284 self.available_columns = []
284 self.available_columns = []
285
285
286 class_attribute :queried_class
286 class_attribute :queried_class
287
287
288 # Permission required to view the queries, set on subclasses.
288 # Permission required to view the queries, set on subclasses.
289 class_attribute :view_permission
289 class_attribute :view_permission
290
290
291 # Scope of queries that are global or on the given project
291 # Scope of queries that are global or on the given project
292 scope :global_or_on_project, lambda {|project|
292 scope :global_or_on_project, lambda {|project|
293 where(:project_id => (project.nil? ? nil : [nil, project.id]))
293 where(:project_id => (project.nil? ? nil : [nil, project.id]))
294 }
294 }
295
295
296 scope :sorted, lambda {order(:name, :id)}
296 scope :sorted, lambda {order(:name, :id)}
297
297
298 # Scope of visible queries, can be used from subclasses only.
298 # Scope of visible queries, can be used from subclasses only.
299 # Unlike other visible scopes, a class methods is used as it
299 # Unlike other visible scopes, a class methods is used as it
300 # let handle inheritance more nicely than scope DSL.
300 # let handle inheritance more nicely than scope DSL.
301 def self.visible(*args)
301 def self.visible(*args)
302 if self == ::Query
302 if self == ::Query
303 # Visibility depends on permissions for each subclass,
303 # Visibility depends on permissions for each subclass,
304 # raise an error if the scope is called from Query (eg. Query.visible)
304 # raise an error if the scope is called from Query (eg. Query.visible)
305 raise Exception.new("Cannot call .visible scope from the base Query class, but from subclasses only.")
305 raise Exception.new("Cannot call .visible scope from the base Query class, but from subclasses only.")
306 end
306 end
307
307
308 user = args.shift || User.current
308 user = args.shift || User.current
309 base = Project.allowed_to_condition(user, view_permission, *args)
309 base = Project.allowed_to_condition(user, view_permission, *args)
310 scope = joins("LEFT OUTER JOIN #{Project.table_name} ON #{table_name}.project_id = #{Project.table_name}.id").
310 scope = joins("LEFT OUTER JOIN #{Project.table_name} ON #{table_name}.project_id = #{Project.table_name}.id").
311 where("#{table_name}.project_id IS NULL OR (#{base})")
311 where("#{table_name}.project_id IS NULL OR (#{base})")
312
312
313 if user.admin?
313 if user.admin?
314 scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", VISIBILITY_PRIVATE, user.id)
314 scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", VISIBILITY_PRIVATE, user.id)
315 elsif user.memberships.any?
315 elsif user.memberships.any?
316 scope.where("#{table_name}.visibility = ?" +
316 scope.where("#{table_name}.visibility = ?" +
317 " OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" +
317 " OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" +
318 "SELECT DISTINCT q.id FROM #{table_name} q" +
318 "SELECT DISTINCT q.id FROM #{table_name} q" +
319 " INNER JOIN #{table_name_prefix}queries_roles#{table_name_suffix} qr on qr.query_id = q.id" +
319 " INNER JOIN #{table_name_prefix}queries_roles#{table_name_suffix} qr on qr.query_id = q.id" +
320 " INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = qr.role_id" +
320 " INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = qr.role_id" +
321 " INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" +
321 " INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" +
322 " WHERE q.project_id IS NULL OR q.project_id = m.project_id))" +
322 " WHERE q.project_id IS NULL OR q.project_id = m.project_id))" +
323 " OR #{table_name}.user_id = ?",
323 " OR #{table_name}.user_id = ?",
324 VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, user.id)
324 VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, user.id)
325 elsif user.logged?
325 elsif user.logged?
326 scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", VISIBILITY_PUBLIC, user.id)
326 scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", VISIBILITY_PUBLIC, user.id)
327 else
327 else
328 scope.where("#{table_name}.visibility = ?", VISIBILITY_PUBLIC)
328 scope.where("#{table_name}.visibility = ?", VISIBILITY_PUBLIC)
329 end
329 end
330 end
330 end
331
331
332 # Returns true if the query is visible to +user+ or the current user.
332 # Returns true if the query is visible to +user+ or the current user.
333 def visible?(user=User.current)
333 def visible?(user=User.current)
334 return true if user.admin?
334 return true if user.admin?
335 return false unless project.nil? || user.allowed_to?(self.class.view_permission, project)
335 return false unless project.nil? || user.allowed_to?(self.class.view_permission, project)
336 case visibility
336 case visibility
337 when VISIBILITY_PUBLIC
337 when VISIBILITY_PUBLIC
338 true
338 true
339 when VISIBILITY_ROLES
339 when VISIBILITY_ROLES
340 if project
340 if project
341 (user.roles_for_project(project) & roles).any?
341 (user.roles_for_project(project) & roles).any?
342 else
342 else
343 Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any?
343 Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any?
344 end
344 end
345 else
345 else
346 user == self.user
346 user == self.user
347 end
347 end
348 end
348 end
349
349
350 def is_private?
350 def is_private?
351 visibility == VISIBILITY_PRIVATE
351 visibility == VISIBILITY_PRIVATE
352 end
352 end
353
353
354 def is_public?
354 def is_public?
355 !is_private?
355 !is_private?
356 end
356 end
357
357
358 def queried_table_name
358 def queried_table_name
359 @queried_table_name ||= self.class.queried_class.table_name
359 @queried_table_name ||= self.class.queried_class.table_name
360 end
360 end
361
361
362 def initialize(attributes=nil, *args)
362 def initialize(attributes=nil, *args)
363 super attributes
363 super attributes
364 @is_for_all = project.nil?
364 @is_for_all = project.nil?
365 end
365 end
366
366
367 # Builds the query from the given params
367 # Builds the query from the given params
368 def build_from_params(params)
368 def build_from_params(params)
369 if params[:fields] || params[:f]
369 if params[:fields] || params[:f]
370 self.filters = {}
370 self.filters = {}
371 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
371 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
372 else
372 else
373 available_filters.keys.each do |field|
373 available_filters.keys.each do |field|
374 add_short_filter(field, params[field]) if params[field]
374 add_short_filter(field, params[field]) if params[field]
375 end
375 end
376 end
376 end
377 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
377 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
378 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
378 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
379 self.totalable_names = params[:t] || (params[:query] && params[:query][:totalable_names])
379 self.totalable_names = params[:t] || (params[:query] && params[:query][:totalable_names])
380 self
380 self
381 end
381 end
382
382
383 # Builds a new query from the given params and attributes
383 # Builds a new query from the given params and attributes
384 def self.build_from_params(params, attributes={})
384 def self.build_from_params(params, attributes={})
385 new(attributes).build_from_params(params)
385 new(attributes).build_from_params(params)
386 end
386 end
387
387
388 def validate_query_filters
388 def validate_query_filters
389 filters.each_key do |field|
389 filters.each_key do |field|
390 if values_for(field)
390 if values_for(field)
391 case type_for(field)
391 case type_for(field)
392 when :integer
392 when :integer
393 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(,[+-]?\d+)*\z/) }
393 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(,[+-]?\d+)*\z/) }
394 when :float
394 when :float
395 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(\.\d*)?\z/) }
395 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(\.\d*)?\z/) }
396 when :date, :date_past
396 when :date, :date_past
397 case operator_for(field)
397 case operator_for(field)
398 when "=", ">=", "<=", "><"
398 when "=", ">=", "<=", "><"
399 add_filter_error(field, :invalid) if values_for(field).detect {|v|
399 add_filter_error(field, :invalid) if values_for(field).detect {|v|
400 v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?)
400 v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?)
401 }
401 }
402 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
402 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
403 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
403 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
404 end
404 end
405 end
405 end
406 end
406 end
407
407
408 add_filter_error(field, :blank) unless
408 add_filter_error(field, :blank) unless
409 # filter requires one or more values
409 # filter requires one or more values
410 (values_for(field) and !values_for(field).first.blank?) or
410 (values_for(field) and !values_for(field).first.blank?) or
411 # filter doesn't require any value
411 # filter doesn't require any value
412 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "*o", "!o"].include? operator_for(field)
412 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "*o", "!o"].include? operator_for(field)
413 end if filters
413 end if filters
414 end
414 end
415
415
416 def add_filter_error(field, message)
416 def add_filter_error(field, message)
417 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
417 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
418 errors.add(:base, m)
418 errors.add(:base, m)
419 end
419 end
420
420
421 def editable_by?(user)
421 def editable_by?(user)
422 return false unless user
422 return false unless user
423 # Admin can edit them all and regular users can edit their private queries
423 # Admin can edit them all and regular users can edit their private queries
424 return true if user.admin? || (is_private? && self.user_id == user.id)
424 return true if user.admin? || (is_private? && self.user_id == user.id)
425 # Members can not edit public queries that are for all project (only admin is allowed to)
425 # Members can not edit public queries that are for all project (only admin is allowed to)
426 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
426 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
427 end
427 end
428
428
429 def trackers
429 def trackers
430 @trackers ||= (project.nil? ? Tracker.all : project.rolled_up_trackers).visible.sorted
430 @trackers ||= (project.nil? ? Tracker.all : project.rolled_up_trackers).visible.sorted
431 end
431 end
432
432
433 # Returns a hash of localized labels for all filter operators
433 # Returns a hash of localized labels for all filter operators
434 def self.operators_labels
434 def self.operators_labels
435 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
435 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
436 end
436 end
437
437
438 # Returns a representation of the available filters for JSON serialization
438 # Returns a representation of the available filters for JSON serialization
439 def available_filters_as_json
439 def available_filters_as_json
440 json = {}
440 json = {}
441 available_filters.each do |field, filter|
441 available_filters.each do |field, filter|
442 options = {:type => filter[:type], :name => filter[:name]}
442 options = {:type => filter[:type], :name => filter[:name]}
443 options[:remote] = true if filter.remote
443 options[:remote] = true if filter.remote
444
444
445 if has_filter?(field) || !filter.remote
445 if has_filter?(field) || !filter.remote
446 options[:values] = filter.values
446 options[:values] = filter.values
447 if options[:values] && values_for(field)
447 if options[:values] && values_for(field)
448 missing = Array(values_for(field)).select(&:present?) - options[:values].map(&:last)
448 missing = Array(values_for(field)).select(&:present?) - options[:values].map(&:last)
449 if missing.any? && respond_to?(method = "find_#{field}_filter_values")
449 if missing.any? && respond_to?(method = "find_#{field}_filter_values")
450 options[:values] += send(method, missing)
450 options[:values] += send(method, missing)
451 end
451 end
452 end
452 end
453 end
453 end
454 json[field] = options.stringify_keys
454 json[field] = options.stringify_keys
455 end
455 end
456 json
456 json
457 end
457 end
458
458
459 def all_projects
459 def all_projects
460 @all_projects ||= Project.visible.to_a
460 @all_projects ||= Project.visible.to_a
461 end
461 end
462
462
463 def all_projects_values
463 def all_projects_values
464 return @all_projects_values if @all_projects_values
464 return @all_projects_values if @all_projects_values
465
465
466 values = []
466 values = []
467 Project.project_tree(all_projects) do |p, level|
467 Project.project_tree(all_projects) do |p, level|
468 prefix = (level > 0 ? ('--' * level + ' ') : '')
468 prefix = (level > 0 ? ('--' * level + ' ') : '')
469 values << ["#{prefix}#{p.name}", p.id.to_s]
469 values << ["#{prefix}#{p.name}", p.id.to_s]
470 end
470 end
471 @all_projects_values = values
471 @all_projects_values = values
472 end
472 end
473
473
474 def project_values
474 def project_values
475 project_values = []
475 project_values = []
476 if User.current.logged? && User.current.memberships.any?
476 if User.current.logged? && User.current.memberships.any?
477 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
477 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
478 end
478 end
479 project_values += all_projects_values
479 project_values += all_projects_values
480 project_values
480 project_values
481 end
481 end
482
482
483 def subproject_values
483 def subproject_values
484 project.descendants.visible.collect{|s| [s.name, s.id.to_s] }
484 project.descendants.visible.collect{|s| [s.name, s.id.to_s] }
485 end
485 end
486
486
487 def principals
487 def principals
488 @principal ||= begin
488 @principal ||= begin
489 principals = []
489 principals = []
490 if project
490 if project
491 principals += project.principals.visible
491 principals += project.principals.visible
492 unless project.leaf?
492 unless project.leaf?
493 principals += Principal.member_of(project.descendants.visible).visible
493 principals += Principal.member_of(project.descendants.visible).visible
494 end
494 end
495 else
495 else
496 principals += Principal.member_of(all_projects).visible
496 principals += Principal.member_of(all_projects).visible
497 end
497 end
498 principals.uniq!
498 principals.uniq!
499 principals.sort!
499 principals.sort!
500 principals.reject! {|p| p.is_a?(GroupBuiltin)}
500 principals.reject! {|p| p.is_a?(GroupBuiltin)}
501 principals
501 principals
502 end
502 end
503 end
503 end
504
504
505 def users
505 def users
506 principals.select {|p| p.is_a?(User)}
506 principals.select {|p| p.is_a?(User)}
507 end
507 end
508
508
509 def author_values
509 def author_values
510 author_values = []
510 author_values = []
511 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
511 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
512 author_values += users.collect{|s| [s.name, s.id.to_s] }
512 author_values += users.collect{|s| [s.name, s.id.to_s] }
513 author_values
513 author_values
514 end
514 end
515
515
516 def assigned_to_values
516 def assigned_to_values
517 assigned_to_values = []
517 assigned_to_values = []
518 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
518 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
519 assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
519 assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
520 assigned_to_values
520 assigned_to_values
521 end
521 end
522
522
523 def fixed_version_values
523 def fixed_version_values
524 versions = []
524 versions = []
525 if project
525 if project
526 versions = project.shared_versions.to_a
526 versions = project.shared_versions.to_a
527 else
527 else
528 versions = Version.visible.where(:sharing => 'system').to_a
528 versions = Version.visible.where(:sharing => 'system').to_a
529 end
529 end
530 Version.sort_by_status(versions).collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s, l("version_status_#{s.status}")] }
530 Version.sort_by_status(versions).collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s, l("version_status_#{s.status}")] }
531 end
531 end
532
532
533 # Adds available filters
533 # Adds available filters
534 def initialize_available_filters
534 def initialize_available_filters
535 # implemented by sub-classes
535 # implemented by sub-classes
536 end
536 end
537 protected :initialize_available_filters
537 protected :initialize_available_filters
538
538
539 # Adds an available filter
539 # Adds an available filter
540 def add_available_filter(field, options)
540 def add_available_filter(field, options)
541 @available_filters ||= ActiveSupport::OrderedHash.new
541 @available_filters ||= ActiveSupport::OrderedHash.new
542 @available_filters[field] = QueryFilter.new(field, options)
542 @available_filters[field] = QueryFilter.new(field, options)
543 @available_filters
543 @available_filters
544 end
544 end
545
545
546 # Removes an available filter
546 # Removes an available filter
547 def delete_available_filter(field)
547 def delete_available_filter(field)
548 if @available_filters
548 if @available_filters
549 @available_filters.delete(field)
549 @available_filters.delete(field)
550 end
550 end
551 end
551 end
552
552
553 # Return a hash of available filters
553 # Return a hash of available filters
554 def available_filters
554 def available_filters
555 unless @available_filters
555 unless @available_filters
556 initialize_available_filters
556 initialize_available_filters
557 @available_filters ||= {}
557 @available_filters ||= {}
558 end
558 end
559 @available_filters
559 @available_filters
560 end
560 end
561
561
562 def add_filter(field, operator, values=nil)
562 def add_filter(field, operator, values=nil)
563 # values must be an array
563 # values must be an array
564 return unless values.nil? || values.is_a?(Array)
564 return unless values.nil? || values.is_a?(Array)
565 # check if field is defined as an available filter
565 # check if field is defined as an available filter
566 if available_filters.has_key? field
566 if available_filters.has_key? field
567 filter_options = available_filters[field]
567 filter_options = available_filters[field]
568 filters[field] = {:operator => operator, :values => (values || [''])}
568 filters[field] = {:operator => operator, :values => (values || [''])}
569 end
569 end
570 end
570 end
571
571
572 def add_short_filter(field, expression)
572 def add_short_filter(field, expression)
573 return unless expression && available_filters.has_key?(field)
573 return unless expression && available_filters.has_key?(field)
574 field_type = available_filters[field][:type]
574 field_type = available_filters[field][:type]
575 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
575 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
576 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
576 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
577 values = $1
577 values = $1
578 add_filter field, operator, values.present? ? values.split('|') : ['']
578 add_filter field, operator, values.present? ? values.split('|') : ['']
579 end || add_filter(field, '=', expression.to_s.split('|'))
579 end || add_filter(field, '=', expression.to_s.split('|'))
580 end
580 end
581
581
582 # Add multiple filters using +add_filter+
582 # Add multiple filters using +add_filter+
583 def add_filters(fields, operators, values)
583 def add_filters(fields, operators, values)
584 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
584 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
585 fields.each do |field|
585 fields.each do |field|
586 add_filter(field, operators[field], values && values[field])
586 add_filter(field, operators[field], values && values[field])
587 end
587 end
588 end
588 end
589 end
589 end
590
590
591 def has_filter?(field)
591 def has_filter?(field)
592 filters and filters[field]
592 filters and filters[field]
593 end
593 end
594
594
595 def type_for(field)
595 def type_for(field)
596 available_filters[field][:type] if available_filters.has_key?(field)
596 available_filters[field][:type] if available_filters.has_key?(field)
597 end
597 end
598
598
599 def operator_for(field)
599 def operator_for(field)
600 has_filter?(field) ? filters[field][:operator] : nil
600 has_filter?(field) ? filters[field][:operator] : nil
601 end
601 end
602
602
603 def values_for(field)
603 def values_for(field)
604 has_filter?(field) ? filters[field][:values] : nil
604 has_filter?(field) ? filters[field][:values] : nil
605 end
605 end
606
606
607 def value_for(field, index=0)
607 def value_for(field, index=0)
608 (values_for(field) || [])[index]
608 (values_for(field) || [])[index]
609 end
609 end
610
610
611 def label_for(field)
611 def label_for(field)
612 label = available_filters[field][:name] if available_filters.has_key?(field)
612 label = available_filters[field][:name] if available_filters.has_key?(field)
613 label ||= queried_class.human_attribute_name(field, :default => field)
613 label ||= queried_class.human_attribute_name(field, :default => field)
614 end
614 end
615
615
616 def self.add_available_column(column)
616 def self.add_available_column(column)
617 self.available_columns << (column) if column.is_a?(QueryColumn)
617 self.available_columns << (column) if column.is_a?(QueryColumn)
618 end
618 end
619
619
620 # Returns an array of columns that can be used to group the results
620 # Returns an array of columns that can be used to group the results
621 def groupable_columns
621 def groupable_columns
622 available_columns.select {|c| c.groupable}
622 available_columns.select {|c| c.groupable}
623 end
623 end
624
624
625 # Returns a Hash of columns and the key for sorting
625 # Returns a Hash of columns and the key for sorting
626 def sortable_columns
626 def sortable_columns
627 available_columns.inject({}) {|h, column|
627 available_columns.inject({}) {|h, column|
628 h[column.name.to_s] = column.sortable
628 h[column.name.to_s] = column.sortable
629 h
629 h
630 }
630 }
631 end
631 end
632
632
633 def columns
633 def columns
634 # preserve the column_names order
634 # preserve the column_names order
635 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
635 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
636 available_columns.find { |col| col.name == name }
636 available_columns.find { |col| col.name == name }
637 end.compact
637 end.compact
638 available_columns.select(&:frozen?) | cols
638 available_columns.select(&:frozen?) | cols
639 end
639 end
640
640
641 def inline_columns
641 def inline_columns
642 columns.select(&:inline?)
642 columns.select(&:inline?)
643 end
643 end
644
644
645 def block_columns
645 def block_columns
646 columns.reject(&:inline?)
646 columns.reject(&:inline?)
647 end
647 end
648
648
649 def available_inline_columns
649 def available_inline_columns
650 available_columns.select(&:inline?)
650 available_columns.select(&:inline?)
651 end
651 end
652
652
653 def available_block_columns
653 def available_block_columns
654 available_columns.reject(&:inline?)
654 available_columns.reject(&:inline?)
655 end
655 end
656
656
657 def available_totalable_columns
657 def available_totalable_columns
658 available_columns.select(&:totalable)
658 available_columns.select(&:totalable)
659 end
659 end
660
660
661 def default_columns_names
661 def default_columns_names
662 []
662 []
663 end
663 end
664
664
665 def default_totalable_names
665 def default_totalable_names
666 []
666 []
667 end
667 end
668
668
669 def column_names=(names)
669 def column_names=(names)
670 if names
670 if names
671 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
671 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
672 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
672 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
673 # Set column_names to nil if default columns
673 # Set column_names to nil if default columns
674 if names == default_columns_names
674 if names == default_columns_names
675 names = nil
675 names = nil
676 end
676 end
677 end
677 end
678 write_attribute(:column_names, names)
678 write_attribute(:column_names, names)
679 end
679 end
680
680
681 def has_column?(column)
681 def has_column?(column)
682 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
682 name = column.is_a?(QueryColumn) ? column.name : column
683 columns.detect {|c| c.name == name}
683 end
684 end
684
685
685 def has_custom_field_column?
686 def has_custom_field_column?
686 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
687 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
687 end
688 end
688
689
689 def has_default_columns?
690 def has_default_columns?
690 column_names.nil? || column_names.empty?
691 column_names.nil? || column_names.empty?
691 end
692 end
692
693
693 def totalable_columns
694 def totalable_columns
694 names = totalable_names
695 names = totalable_names
695 available_totalable_columns.select {|column| names.include?(column.name)}
696 available_totalable_columns.select {|column| names.include?(column.name)}
696 end
697 end
697
698
698 def totalable_names=(names)
699 def totalable_names=(names)
699 if names
700 if names
700 names = names.select(&:present?).map {|n| n.is_a?(Symbol) ? n : n.to_sym}
701 names = names.select(&:present?).map {|n| n.is_a?(Symbol) ? n : n.to_sym}
701 end
702 end
702 options[:totalable_names] = names
703 options[:totalable_names] = names
703 end
704 end
704
705
705 def totalable_names
706 def totalable_names
706 options[:totalable_names] || default_totalable_names || []
707 options[:totalable_names] || default_totalable_names || []
707 end
708 end
708
709
709 def sort_criteria=(arg)
710 def sort_criteria=(arg)
710 c = []
711 c = []
711 if arg.is_a?(Hash)
712 if arg.is_a?(Hash)
712 arg = arg.keys.sort.collect {|k| arg[k]}
713 arg = arg.keys.sort.collect {|k| arg[k]}
713 end
714 end
714 if arg
715 if arg
715 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
716 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
716 end
717 end
717 write_attribute(:sort_criteria, c)
718 write_attribute(:sort_criteria, c)
718 end
719 end
719
720
720 def sort_criteria
721 def sort_criteria
721 read_attribute(:sort_criteria) || []
722 read_attribute(:sort_criteria) || []
722 end
723 end
723
724
724 def sort_criteria_key(arg)
725 def sort_criteria_key(arg)
725 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
726 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
726 end
727 end
727
728
728 def sort_criteria_order(arg)
729 def sort_criteria_order(arg)
729 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
730 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
730 end
731 end
731
732
732 def sort_criteria_order_for(key)
733 def sort_criteria_order_for(key)
733 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
734 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
734 end
735 end
735
736
736 # Returns the SQL sort order that should be prepended for grouping
737 # Returns the SQL sort order that should be prepended for grouping
737 def group_by_sort_order
738 def group_by_sort_order
738 if column = group_by_column
739 if column = group_by_column
739 order = (sort_criteria_order_for(column.name) || column.default_order || 'asc').try(:upcase)
740 order = (sort_criteria_order_for(column.name) || column.default_order || 'asc').try(:upcase)
740 Array(column.sortable).map {|s| "#{s} #{order}"}
741 Array(column.sortable).map {|s| "#{s} #{order}"}
741 end
742 end
742 end
743 end
743
744
744 # Returns true if the query is a grouped query
745 # Returns true if the query is a grouped query
745 def grouped?
746 def grouped?
746 !group_by_column.nil?
747 !group_by_column.nil?
747 end
748 end
748
749
749 def group_by_column
750 def group_by_column
750 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
751 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
751 end
752 end
752
753
753 def group_by_statement
754 def group_by_statement
754 group_by_column.try(:groupable)
755 group_by_column.try(:groupable)
755 end
756 end
756
757
757 def project_statement
758 def project_statement
758 project_clauses = []
759 project_clauses = []
759 active_subprojects_ids = []
760 active_subprojects_ids = []
760
761
761 active_subprojects_ids = project.descendants.active.map(&:id) if project
762 active_subprojects_ids = project.descendants.active.map(&:id) if project
762 if active_subprojects_ids.any?
763 if active_subprojects_ids.any?
763 if has_filter?("subproject_id")
764 if has_filter?("subproject_id")
764 case operator_for("subproject_id")
765 case operator_for("subproject_id")
765 when '='
766 when '='
766 # include the selected subprojects
767 # include the selected subprojects
767 ids = [project.id] + values_for("subproject_id").map(&:to_i)
768 ids = [project.id] + values_for("subproject_id").map(&:to_i)
768 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
769 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
769 when '!'
770 when '!'
770 # exclude the selected subprojects
771 # exclude the selected subprojects
771 ids = [project.id] + active_subprojects_ids - values_for("subproject_id").map(&:to_i)
772 ids = [project.id] + active_subprojects_ids - values_for("subproject_id").map(&:to_i)
772 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
773 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
773 when '!*'
774 when '!*'
774 # main project only
775 # main project only
775 project_clauses << "#{Project.table_name}.id = %d" % project.id
776 project_clauses << "#{Project.table_name}.id = %d" % project.id
776 else
777 else
777 # all subprojects
778 # all subprojects
778 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
779 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
779 end
780 end
780 elsif Setting.display_subprojects_issues?
781 elsif Setting.display_subprojects_issues?
781 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
782 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
782 else
783 else
783 project_clauses << "#{Project.table_name}.id = %d" % project.id
784 project_clauses << "#{Project.table_name}.id = %d" % project.id
784 end
785 end
785 elsif project
786 elsif project
786 project_clauses << "#{Project.table_name}.id = %d" % project.id
787 project_clauses << "#{Project.table_name}.id = %d" % project.id
787 end
788 end
788 project_clauses.any? ? project_clauses.join(' AND ') : nil
789 project_clauses.any? ? project_clauses.join(' AND ') : nil
789 end
790 end
790
791
791 def statement
792 def statement
792 # filters clauses
793 # filters clauses
793 filters_clauses = []
794 filters_clauses = []
794 filters.each_key do |field|
795 filters.each_key do |field|
795 next if field == "subproject_id"
796 next if field == "subproject_id"
796 v = values_for(field).clone
797 v = values_for(field).clone
797 next unless v and !v.empty?
798 next unless v and !v.empty?
798 operator = operator_for(field)
799 operator = operator_for(field)
799
800
800 # "me" value substitution
801 # "me" value substitution
801 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
802 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
802 if v.delete("me")
803 if v.delete("me")
803 if User.current.logged?
804 if User.current.logged?
804 v.push(User.current.id.to_s)
805 v.push(User.current.id.to_s)
805 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
806 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
806 else
807 else
807 v.push("0")
808 v.push("0")
808 end
809 end
809 end
810 end
810 end
811 end
811
812
812 if field == 'project_id'
813 if field == 'project_id'
813 if v.delete('mine')
814 if v.delete('mine')
814 v += User.current.memberships.map(&:project_id).map(&:to_s)
815 v += User.current.memberships.map(&:project_id).map(&:to_s)
815 end
816 end
816 end
817 end
817
818
818 if field =~ /^cf_(\d+)\.cf_(\d+)$/
819 if field =~ /^cf_(\d+)\.cf_(\d+)$/
819 filters_clauses << sql_for_chained_custom_field(field, operator, v, $1, $2)
820 filters_clauses << sql_for_chained_custom_field(field, operator, v, $1, $2)
820 elsif field =~ /cf_(\d+)$/
821 elsif field =~ /cf_(\d+)$/
821 # custom field
822 # custom field
822 filters_clauses << sql_for_custom_field(field, operator, v, $1)
823 filters_clauses << sql_for_custom_field(field, operator, v, $1)
823 elsif field =~ /^cf_(\d+)\.(.+)$/
824 elsif field =~ /^cf_(\d+)\.(.+)$/
824 filters_clauses << sql_for_custom_field_attribute(field, operator, v, $1, $2)
825 filters_clauses << sql_for_custom_field_attribute(field, operator, v, $1, $2)
825 elsif respond_to?(method = "sql_for_#{field.gsub('.','_')}_field")
826 elsif respond_to?(method = "sql_for_#{field.gsub('.','_')}_field")
826 # specific statement
827 # specific statement
827 filters_clauses << send(method, field, operator, v)
828 filters_clauses << send(method, field, operator, v)
828 else
829 else
829 # regular field
830 # regular field
830 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
831 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
831 end
832 end
832 end if filters and valid?
833 end if filters and valid?
833
834
834 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
835 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
835 # Excludes results for which the grouped custom field is not visible
836 # Excludes results for which the grouped custom field is not visible
836 filters_clauses << c.custom_field.visibility_by_project_condition
837 filters_clauses << c.custom_field.visibility_by_project_condition
837 end
838 end
838
839
839 filters_clauses << project_statement
840 filters_clauses << project_statement
840 filters_clauses.reject!(&:blank?)
841 filters_clauses.reject!(&:blank?)
841
842
842 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
843 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
843 end
844 end
844
845
845 # Returns the sum of values for the given column
846 # Returns the sum of values for the given column
846 def total_for(column)
847 def total_for(column)
847 total_with_scope(column, base_scope)
848 total_with_scope(column, base_scope)
848 end
849 end
849
850
850 # Returns a hash of the sum of the given column for each group,
851 # Returns a hash of the sum of the given column for each group,
851 # or nil if the query is not grouped
852 # or nil if the query is not grouped
852 def total_by_group_for(column)
853 def total_by_group_for(column)
853 grouped_query do |scope|
854 grouped_query do |scope|
854 total_with_scope(column, scope)
855 total_with_scope(column, scope)
855 end
856 end
856 end
857 end
857
858
858 def totals
859 def totals
859 totals = totalable_columns.map {|column| [column, total_for(column)]}
860 totals = totalable_columns.map {|column| [column, total_for(column)]}
860 yield totals if block_given?
861 yield totals if block_given?
861 totals
862 totals
862 end
863 end
863
864
864 def totals_by_group
865 def totals_by_group
865 totals = totalable_columns.map {|column| [column, total_by_group_for(column)]}
866 totals = totalable_columns.map {|column| [column, total_by_group_for(column)]}
866 yield totals if block_given?
867 yield totals if block_given?
867 totals
868 totals
868 end
869 end
869
870
870 private
871 private
871
872
872 def grouped_query(&block)
873 def grouped_query(&block)
873 r = nil
874 r = nil
874 if grouped?
875 if grouped?
875 begin
876 begin
876 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
877 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
877 r = yield base_group_scope
878 r = yield base_group_scope
878 rescue ActiveRecord::RecordNotFound
879 rescue ActiveRecord::RecordNotFound
879 r = {nil => yield(base_scope)}
880 r = {nil => yield(base_scope)}
880 end
881 end
881 c = group_by_column
882 c = group_by_column
882 if c.is_a?(QueryCustomFieldColumn)
883 if c.is_a?(QueryCustomFieldColumn)
883 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
884 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
884 end
885 end
885 end
886 end
886 r
887 r
887 rescue ::ActiveRecord::StatementInvalid => e
888 rescue ::ActiveRecord::StatementInvalid => e
888 raise StatementInvalid.new(e.message)
889 raise StatementInvalid.new(e.message)
889 end
890 end
890
891
891 def total_with_scope(column, scope)
892 def total_with_scope(column, scope)
892 unless column.is_a?(QueryColumn)
893 unless column.is_a?(QueryColumn)
893 column = column.to_sym
894 column = column.to_sym
894 column = available_totalable_columns.detect {|c| c.name == column}
895 column = available_totalable_columns.detect {|c| c.name == column}
895 end
896 end
896 if column.is_a?(QueryCustomFieldColumn)
897 if column.is_a?(QueryCustomFieldColumn)
897 custom_field = column.custom_field
898 custom_field = column.custom_field
898 send "total_for_custom_field", custom_field, scope
899 send "total_for_custom_field", custom_field, scope
899 else
900 else
900 send "total_for_#{column.name}", scope
901 send "total_for_#{column.name}", scope
901 end
902 end
902 rescue ::ActiveRecord::StatementInvalid => e
903 rescue ::ActiveRecord::StatementInvalid => e
903 raise StatementInvalid.new(e.message)
904 raise StatementInvalid.new(e.message)
904 end
905 end
905
906
906 def base_scope
907 def base_scope
907 raise "unimplemented"
908 raise "unimplemented"
908 end
909 end
909
910
910 def base_group_scope
911 def base_group_scope
911 base_scope.
912 base_scope.
912 joins(joins_for_order_statement(group_by_statement)).
913 joins(joins_for_order_statement(group_by_statement)).
913 group(group_by_statement)
914 group(group_by_statement)
914 end
915 end
915
916
916 def total_for_custom_field(custom_field, scope, &block)
917 def total_for_custom_field(custom_field, scope, &block)
917 total = custom_field.format.total_for_scope(custom_field, scope)
918 total = custom_field.format.total_for_scope(custom_field, scope)
918 total = map_total(total) {|t| custom_field.format.cast_total_value(custom_field, t)}
919 total = map_total(total) {|t| custom_field.format.cast_total_value(custom_field, t)}
919 total
920 total
920 end
921 end
921
922
922 def map_total(total, &block)
923 def map_total(total, &block)
923 if total.is_a?(Hash)
924 if total.is_a?(Hash)
924 total.keys.each {|k| total[k] = yield total[k]}
925 total.keys.each {|k| total[k] = yield total[k]}
925 else
926 else
926 total = yield total
927 total = yield total
927 end
928 end
928 total
929 total
929 end
930 end
930
931
931 def sql_for_custom_field(field, operator, value, custom_field_id)
932 def sql_for_custom_field(field, operator, value, custom_field_id)
932 db_table = CustomValue.table_name
933 db_table = CustomValue.table_name
933 db_field = 'value'
934 db_field = 'value'
934 filter = @available_filters[field]
935 filter = @available_filters[field]
935 return nil unless filter
936 return nil unless filter
936 if filter[:field].format.target_class && filter[:field].format.target_class <= User
937 if filter[:field].format.target_class && filter[:field].format.target_class <= User
937 if value.delete('me')
938 if value.delete('me')
938 value.push User.current.id.to_s
939 value.push User.current.id.to_s
939 end
940 end
940 end
941 end
941 not_in = nil
942 not_in = nil
942 if operator == '!'
943 if operator == '!'
943 # Makes ! operator work for custom fields with multiple values
944 # Makes ! operator work for custom fields with multiple values
944 operator = '='
945 operator = '='
945 not_in = 'NOT'
946 not_in = 'NOT'
946 end
947 end
947 customized_key = "id"
948 customized_key = "id"
948 customized_class = queried_class
949 customized_class = queried_class
949 if field =~ /^(.+)\.cf_/
950 if field =~ /^(.+)\.cf_/
950 assoc = $1
951 assoc = $1
951 customized_key = "#{assoc}_id"
952 customized_key = "#{assoc}_id"
952 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
953 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
953 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
954 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
954 end
955 end
955 where = sql_for_field(field, operator, value, db_table, db_field, true)
956 where = sql_for_field(field, operator, value, db_table, db_field, true)
956 if operator =~ /[<>]/
957 if operator =~ /[<>]/
957 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
958 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
958 end
959 end
959 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
960 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
960 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
961 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
961 " 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}" +
962 " 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}" +
962 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
963 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
963 end
964 end
964
965
965 def sql_for_chained_custom_field(field, operator, value, custom_field_id, chained_custom_field_id)
966 def sql_for_chained_custom_field(field, operator, value, custom_field_id, chained_custom_field_id)
966 not_in = nil
967 not_in = nil
967 if operator == '!'
968 if operator == '!'
968 # Makes ! operator work for custom fields with multiple values
969 # Makes ! operator work for custom fields with multiple values
969 operator = '='
970 operator = '='
970 not_in = 'NOT'
971 not_in = 'NOT'
971 end
972 end
972
973
973 filter = available_filters[field]
974 filter = available_filters[field]
974 target_class = filter[:through].format.target_class
975 target_class = filter[:through].format.target_class
975
976
976 "#{queried_table_name}.id #{not_in} IN (" +
977 "#{queried_table_name}.id #{not_in} IN (" +
977 "SELECT customized_id FROM #{CustomValue.table_name}" +
978 "SELECT customized_id FROM #{CustomValue.table_name}" +
978 " WHERE customized_type='#{queried_class}' AND custom_field_id=#{custom_field_id}" +
979 " WHERE customized_type='#{queried_class}' AND custom_field_id=#{custom_field_id}" +
979 " AND CAST(CASE value WHEN '' THEN '0' ELSE value END AS decimal(30,0)) IN (" +
980 " AND CAST(CASE value WHEN '' THEN '0' ELSE value END AS decimal(30,0)) IN (" +
980 " SELECT customized_id FROM #{CustomValue.table_name}" +
981 " SELECT customized_id FROM #{CustomValue.table_name}" +
981 " WHERE customized_type='#{target_class}' AND custom_field_id=#{chained_custom_field_id}" +
982 " WHERE customized_type='#{target_class}' AND custom_field_id=#{chained_custom_field_id}" +
982 " AND #{sql_for_field(field, operator, value, CustomValue.table_name, 'value')}))"
983 " AND #{sql_for_field(field, operator, value, CustomValue.table_name, 'value')}))"
983
984
984 end
985 end
985
986
986 def sql_for_custom_field_attribute(field, operator, value, custom_field_id, attribute)
987 def sql_for_custom_field_attribute(field, operator, value, custom_field_id, attribute)
987 attribute = 'effective_date' if attribute == 'due_date'
988 attribute = 'effective_date' if attribute == 'due_date'
988 not_in = nil
989 not_in = nil
989 if operator == '!'
990 if operator == '!'
990 # Makes ! operator work for custom fields with multiple values
991 # Makes ! operator work for custom fields with multiple values
991 operator = '='
992 operator = '='
992 not_in = 'NOT'
993 not_in = 'NOT'
993 end
994 end
994
995
995 filter = available_filters[field]
996 filter = available_filters[field]
996 target_table_name = filter[:field].format.target_class.table_name
997 target_table_name = filter[:field].format.target_class.table_name
997
998
998 "#{queried_table_name}.id #{not_in} IN (" +
999 "#{queried_table_name}.id #{not_in} IN (" +
999 "SELECT customized_id FROM #{CustomValue.table_name}" +
1000 "SELECT customized_id FROM #{CustomValue.table_name}" +
1000 " WHERE customized_type='#{queried_class}' AND custom_field_id=#{custom_field_id}" +
1001 " WHERE customized_type='#{queried_class}' AND custom_field_id=#{custom_field_id}" +
1001 " AND CAST(CASE value WHEN '' THEN '0' ELSE value END AS decimal(30,0)) IN (" +
1002 " AND CAST(CASE value WHEN '' THEN '0' ELSE value END AS decimal(30,0)) IN (" +
1002 " SELECT id FROM #{target_table_name} WHERE #{sql_for_field(field, operator, value, filter[:field].format.target_class.table_name, attribute)}))"
1003 " SELECT id FROM #{target_table_name} WHERE #{sql_for_field(field, operator, value, filter[:field].format.target_class.table_name, attribute)}))"
1003 end
1004 end
1004
1005
1005 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
1006 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
1006 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
1007 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
1007 sql = ''
1008 sql = ''
1008 case operator
1009 case operator
1009 when "="
1010 when "="
1010 if value.any?
1011 if value.any?
1011 case type_for(field)
1012 case type_for(field)
1012 when :date, :date_past
1013 when :date, :date_past
1013 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
1014 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
1014 when :integer
1015 when :integer
1015 int_values = value.first.to_s.scan(/[+-]?\d+/).map(&:to_i).join(",")
1016 int_values = value.first.to_s.scan(/[+-]?\d+/).map(&:to_i).join(",")
1016 if int_values.present?
1017 if int_values.present?
1017 if is_custom_filter
1018 if is_custom_filter
1018 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)) IN (#{int_values}))"
1019 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)) IN (#{int_values}))"
1019 else
1020 else
1020 sql = "#{db_table}.#{db_field} IN (#{int_values})"
1021 sql = "#{db_table}.#{db_field} IN (#{int_values})"
1021 end
1022 end
1022 else
1023 else
1023 sql = "1=0"
1024 sql = "1=0"
1024 end
1025 end
1025 when :float
1026 when :float
1026 if is_custom_filter
1027 if is_custom_filter
1027 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})"
1028 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})"
1028 else
1029 else
1029 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
1030 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
1030 end
1031 end
1031 else
1032 else
1032 sql = queried_class.send(:sanitize_sql_for_conditions, ["#{db_table}.#{db_field} IN (?)", value])
1033 sql = queried_class.send(:sanitize_sql_for_conditions, ["#{db_table}.#{db_field} IN (?)", value])
1033 end
1034 end
1034 else
1035 else
1035 # IN an empty set
1036 # IN an empty set
1036 sql = "1=0"
1037 sql = "1=0"
1037 end
1038 end
1038 when "!"
1039 when "!"
1039 if value.any?
1040 if value.any?
1040 sql = queried_class.send(:sanitize_sql_for_conditions, ["(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (?))", value])
1041 sql = queried_class.send(:sanitize_sql_for_conditions, ["(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (?))", value])
1041 else
1042 else
1042 # NOT IN an empty set
1043 # NOT IN an empty set
1043 sql = "1=1"
1044 sql = "1=1"
1044 end
1045 end
1045 when "!*"
1046 when "!*"
1046 sql = "#{db_table}.#{db_field} IS NULL"
1047 sql = "#{db_table}.#{db_field} IS NULL"
1047 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
1048 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
1048 when "*"
1049 when "*"
1049 sql = "#{db_table}.#{db_field} IS NOT NULL"
1050 sql = "#{db_table}.#{db_field} IS NOT NULL"
1050 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
1051 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
1051 when ">="
1052 when ">="
1052 if [:date, :date_past].include?(type_for(field))
1053 if [:date, :date_past].include?(type_for(field))
1053 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
1054 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
1054 else
1055 else
1055 if is_custom_filter
1056 if is_custom_filter
1056 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})"
1057 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})"
1057 else
1058 else
1058 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
1059 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
1059 end
1060 end
1060 end
1061 end
1061 when "<="
1062 when "<="
1062 if [:date, :date_past].include?(type_for(field))
1063 if [:date, :date_past].include?(type_for(field))
1063 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
1064 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
1064 else
1065 else
1065 if is_custom_filter
1066 if is_custom_filter
1066 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})"
1067 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})"
1067 else
1068 else
1068 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
1069 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
1069 end
1070 end
1070 end
1071 end
1071 when "><"
1072 when "><"
1072 if [:date, :date_past].include?(type_for(field))
1073 if [:date, :date_past].include?(type_for(field))
1073 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
1074 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
1074 else
1075 else
1075 if is_custom_filter
1076 if is_custom_filter
1076 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})"
1077 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})"
1077 else
1078 else
1078 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
1079 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
1079 end
1080 end
1080 end
1081 end
1081 when "o"
1082 when "o"
1082 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false})" if field == "status_id"
1083 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false})" if field == "status_id"
1083 when "c"
1084 when "c"
1084 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_true})" if field == "status_id"
1085 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_true})" if field == "status_id"
1085 when "><t-"
1086 when "><t-"
1086 # between today - n days and today
1087 # between today - n days and today
1087 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
1088 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
1088 when ">t-"
1089 when ">t-"
1089 # >= today - n days
1090 # >= today - n days
1090 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
1091 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
1091 when "<t-"
1092 when "<t-"
1092 # <= today - n days
1093 # <= today - n days
1093 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
1094 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
1094 when "t-"
1095 when "t-"
1095 # = n days in past
1096 # = n days in past
1096 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
1097 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
1097 when "><t+"
1098 when "><t+"
1098 # between today and today + n days
1099 # between today and today + n days
1099 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
1100 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
1100 when ">t+"
1101 when ">t+"
1101 # >= today + n days
1102 # >= today + n days
1102 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
1103 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
1103 when "<t+"
1104 when "<t+"
1104 # <= today + n days
1105 # <= today + n days
1105 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
1106 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
1106 when "t+"
1107 when "t+"
1107 # = today + n days
1108 # = today + n days
1108 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
1109 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
1109 when "t"
1110 when "t"
1110 # = today
1111 # = today
1111 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
1112 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
1112 when "ld"
1113 when "ld"
1113 # = yesterday
1114 # = yesterday
1114 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
1115 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
1115 when "w"
1116 when "w"
1116 # = this week
1117 # = this week
1117 first_day_of_week = l(:general_first_day_of_week).to_i
1118 first_day_of_week = l(:general_first_day_of_week).to_i
1118 day_of_week = User.current.today.cwday
1119 day_of_week = User.current.today.cwday
1119 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
1120 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
1120 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
1121 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
1121 when "lw"
1122 when "lw"
1122 # = last week
1123 # = last week
1123 first_day_of_week = l(:general_first_day_of_week).to_i
1124 first_day_of_week = l(:general_first_day_of_week).to_i
1124 day_of_week = User.current.today.cwday
1125 day_of_week = User.current.today.cwday
1125 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
1126 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
1126 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
1127 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
1127 when "l2w"
1128 when "l2w"
1128 # = last 2 weeks
1129 # = last 2 weeks
1129 first_day_of_week = l(:general_first_day_of_week).to_i
1130 first_day_of_week = l(:general_first_day_of_week).to_i
1130 day_of_week = User.current.today.cwday
1131 day_of_week = User.current.today.cwday
1131 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
1132 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
1132 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
1133 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
1133 when "m"
1134 when "m"
1134 # = this month
1135 # = this month
1135 date = User.current.today
1136 date = User.current.today
1136 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
1137 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
1137 when "lm"
1138 when "lm"
1138 # = last month
1139 # = last month
1139 date = User.current.today.prev_month
1140 date = User.current.today.prev_month
1140 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
1141 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
1141 when "y"
1142 when "y"
1142 # = this year
1143 # = this year
1143 date = User.current.today
1144 date = User.current.today
1144 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
1145 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
1145 when "~"
1146 when "~"
1146 sql = sql_contains("#{db_table}.#{db_field}", value.first)
1147 sql = sql_contains("#{db_table}.#{db_field}", value.first)
1147 when "!~"
1148 when "!~"
1148 sql = sql_contains("#{db_table}.#{db_field}", value.first, false)
1149 sql = sql_contains("#{db_table}.#{db_field}", value.first, false)
1149 else
1150 else
1150 raise "Unknown query operator #{operator}"
1151 raise "Unknown query operator #{operator}"
1151 end
1152 end
1152
1153
1153 return sql
1154 return sql
1154 end
1155 end
1155
1156
1156 # Returns a SQL LIKE statement with wildcards
1157 # Returns a SQL LIKE statement with wildcards
1157 def sql_contains(db_field, value, match=true)
1158 def sql_contains(db_field, value, match=true)
1158 queried_class.send :sanitize_sql_for_conditions,
1159 queried_class.send :sanitize_sql_for_conditions,
1159 [Redmine::Database.like(db_field, '?', :match => match), "%#{value}%"]
1160 [Redmine::Database.like(db_field, '?', :match => match), "%#{value}%"]
1160 end
1161 end
1161
1162
1162 # Adds a filter for the given custom field
1163 # Adds a filter for the given custom field
1163 def add_custom_field_filter(field, assoc=nil)
1164 def add_custom_field_filter(field, assoc=nil)
1164 options = field.query_filter_options(self)
1165 options = field.query_filter_options(self)
1165
1166
1166 filter_id = "cf_#{field.id}"
1167 filter_id = "cf_#{field.id}"
1167 filter_name = field.name
1168 filter_name = field.name
1168 if assoc.present?
1169 if assoc.present?
1169 filter_id = "#{assoc}.#{filter_id}"
1170 filter_id = "#{assoc}.#{filter_id}"
1170 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
1171 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
1171 end
1172 end
1172 add_available_filter filter_id, options.merge({
1173 add_available_filter filter_id, options.merge({
1173 :name => filter_name,
1174 :name => filter_name,
1174 :field => field
1175 :field => field
1175 })
1176 })
1176 end
1177 end
1177
1178
1178 # Adds filters for custom fields associated to the custom field target class
1179 # Adds filters for custom fields associated to the custom field target class
1179 # Eg. having a version custom field "Milestone" for issues and a date custom field "Release date"
1180 # Eg. having a version custom field "Milestone" for issues and a date custom field "Release date"
1180 # for versions, it will add an issue filter on Milestone'e Release date.
1181 # for versions, it will add an issue filter on Milestone'e Release date.
1181 def add_chained_custom_field_filters(field)
1182 def add_chained_custom_field_filters(field)
1182 klass = field.format.target_class
1183 klass = field.format.target_class
1183 if klass
1184 if klass
1184 CustomField.where(:is_filter => true, :type => "#{klass.name}CustomField").each do |chained|
1185 CustomField.where(:is_filter => true, :type => "#{klass.name}CustomField").each do |chained|
1185 options = chained.query_filter_options(self)
1186 options = chained.query_filter_options(self)
1186
1187
1187 filter_id = "cf_#{field.id}.cf_#{chained.id}"
1188 filter_id = "cf_#{field.id}.cf_#{chained.id}"
1188 filter_name = chained.name
1189 filter_name = chained.name
1189
1190
1190 add_available_filter filter_id, options.merge({
1191 add_available_filter filter_id, options.merge({
1191 :name => l(:label_attribute_of_object, :name => chained.name, :object_name => field.name),
1192 :name => l(:label_attribute_of_object, :name => chained.name, :object_name => field.name),
1192 :field => chained,
1193 :field => chained,
1193 :through => field
1194 :through => field
1194 })
1195 })
1195 end
1196 end
1196 end
1197 end
1197 end
1198 end
1198
1199
1199 # Adds filters for the given custom fields scope
1200 # Adds filters for the given custom fields scope
1200 def add_custom_fields_filters(scope, assoc=nil)
1201 def add_custom_fields_filters(scope, assoc=nil)
1201 scope.visible.where(:is_filter => true).sorted.each do |field|
1202 scope.visible.where(:is_filter => true).sorted.each do |field|
1202 add_custom_field_filter(field, assoc)
1203 add_custom_field_filter(field, assoc)
1203 if assoc.nil?
1204 if assoc.nil?
1204 add_chained_custom_field_filters(field)
1205 add_chained_custom_field_filters(field)
1205
1206
1206 if field.format.target_class && field.format.target_class == Version
1207 if field.format.target_class && field.format.target_class == Version
1207 add_available_filter "cf_#{field.id}.due_date",
1208 add_available_filter "cf_#{field.id}.due_date",
1208 :type => :date,
1209 :type => :date,
1209 :field => field,
1210 :field => field,
1210 :name => l(:label_attribute_of_object, :name => l(:field_effective_date), :object_name => field.name)
1211 :name => l(:label_attribute_of_object, :name => l(:field_effective_date), :object_name => field.name)
1211
1212
1212 add_available_filter "cf_#{field.id}.status",
1213 add_available_filter "cf_#{field.id}.status",
1213 :type => :list,
1214 :type => :list,
1214 :field => field,
1215 :field => field,
1215 :name => l(:label_attribute_of_object, :name => l(:field_status), :object_name => field.name),
1216 :name => l(:label_attribute_of_object, :name => l(:field_status), :object_name => field.name),
1216 :values => Version::VERSION_STATUSES.map{|s| [l("version_status_#{s}"), s] }
1217 :values => Version::VERSION_STATUSES.map{|s| [l("version_status_#{s}"), s] }
1217 end
1218 end
1218 end
1219 end
1219 end
1220 end
1220 end
1221 end
1221
1222
1222 # Adds filters for the given associations custom fields
1223 # Adds filters for the given associations custom fields
1223 def add_associations_custom_fields_filters(*associations)
1224 def add_associations_custom_fields_filters(*associations)
1224 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
1225 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
1225 associations.each do |assoc|
1226 associations.each do |assoc|
1226 association_klass = queried_class.reflect_on_association(assoc).klass
1227 association_klass = queried_class.reflect_on_association(assoc).klass
1227 fields_by_class.each do |field_class, fields|
1228 fields_by_class.each do |field_class, fields|
1228 if field_class.customized_class <= association_klass
1229 if field_class.customized_class <= association_klass
1229 fields.sort.each do |field|
1230 fields.sort.each do |field|
1230 add_custom_field_filter(field, assoc)
1231 add_custom_field_filter(field, assoc)
1231 end
1232 end
1232 end
1233 end
1233 end
1234 end
1234 end
1235 end
1235 end
1236 end
1236
1237
1237 def quoted_time(time, is_custom_filter)
1238 def quoted_time(time, is_custom_filter)
1238 if is_custom_filter
1239 if is_custom_filter
1239 # Custom field values are stored as strings in the DB
1240 # Custom field values are stored as strings in the DB
1240 # using this format that does not depend on DB date representation
1241 # using this format that does not depend on DB date representation
1241 time.strftime("%Y-%m-%d %H:%M:%S")
1242 time.strftime("%Y-%m-%d %H:%M:%S")
1242 else
1243 else
1243 self.class.connection.quoted_date(time)
1244 self.class.connection.quoted_date(time)
1244 end
1245 end
1245 end
1246 end
1246
1247
1247 def date_for_user_time_zone(y, m, d)
1248 def date_for_user_time_zone(y, m, d)
1248 if tz = User.current.time_zone
1249 if tz = User.current.time_zone
1249 tz.local y, m, d
1250 tz.local y, m, d
1250 else
1251 else
1251 Time.local y, m, d
1252 Time.local y, m, d
1252 end
1253 end
1253 end
1254 end
1254
1255
1255 # Returns a SQL clause for a date or datetime field.
1256 # Returns a SQL clause for a date or datetime field.
1256 def date_clause(table, field, from, to, is_custom_filter)
1257 def date_clause(table, field, from, to, is_custom_filter)
1257 s = []
1258 s = []
1258 if from
1259 if from
1259 if from.is_a?(Date)
1260 if from.is_a?(Date)
1260 from = date_for_user_time_zone(from.year, from.month, from.day).yesterday.end_of_day
1261 from = date_for_user_time_zone(from.year, from.month, from.day).yesterday.end_of_day
1261 else
1262 else
1262 from = from - 1 # second
1263 from = from - 1 # second
1263 end
1264 end
1264 if self.class.default_timezone == :utc
1265 if self.class.default_timezone == :utc
1265 from = from.utc
1266 from = from.utc
1266 end
1267 end
1267 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
1268 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
1268 end
1269 end
1269 if to
1270 if to
1270 if to.is_a?(Date)
1271 if to.is_a?(Date)
1271 to = date_for_user_time_zone(to.year, to.month, to.day).end_of_day
1272 to = date_for_user_time_zone(to.year, to.month, to.day).end_of_day
1272 end
1273 end
1273 if self.class.default_timezone == :utc
1274 if self.class.default_timezone == :utc
1274 to = to.utc
1275 to = to.utc
1275 end
1276 end
1276 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
1277 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
1277 end
1278 end
1278 s.join(' AND ')
1279 s.join(' AND ')
1279 end
1280 end
1280
1281
1281 # Returns a SQL clause for a date or datetime field using relative dates.
1282 # Returns a SQL clause for a date or datetime field using relative dates.
1282 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
1283 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
1283 date_clause(table, field, (days_from ? User.current.today + days_from : nil), (days_to ? User.current.today + days_to : nil), is_custom_filter)
1284 date_clause(table, field, (days_from ? User.current.today + days_from : nil), (days_to ? User.current.today + days_to : nil), is_custom_filter)
1284 end
1285 end
1285
1286
1286 # Returns a Date or Time from the given filter value
1287 # Returns a Date or Time from the given filter value
1287 def parse_date(arg)
1288 def parse_date(arg)
1288 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
1289 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
1289 Time.parse(arg) rescue nil
1290 Time.parse(arg) rescue nil
1290 else
1291 else
1291 Date.parse(arg) rescue nil
1292 Date.parse(arg) rescue nil
1292 end
1293 end
1293 end
1294 end
1294
1295
1295 # Additional joins required for the given sort options
1296 # Additional joins required for the given sort options
1296 def joins_for_order_statement(order_options)
1297 def joins_for_order_statement(order_options)
1297 joins = []
1298 joins = []
1298
1299
1299 if order_options
1300 if order_options
1300 order_options.scan(/cf_\d+/).uniq.each do |name|
1301 order_options.scan(/cf_\d+/).uniq.each do |name|
1301 column = available_columns.detect {|c| c.name.to_s == name}
1302 column = available_columns.detect {|c| c.name.to_s == name}
1302 join = column && column.custom_field.join_for_order_statement
1303 join = column && column.custom_field.join_for_order_statement
1303 if join
1304 if join
1304 joins << join
1305 joins << join
1305 end
1306 end
1306 end
1307 end
1307 end
1308 end
1308
1309
1309 joins.any? ? joins.join(' ') : nil
1310 joins.any? ? joins.join(' ') : nil
1310 end
1311 end
1311 end
1312 end
@@ -1,1895 +1,1903
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require File.expand_path('../../test_helper', __FILE__)
20 require File.expand_path('../../test_helper', __FILE__)
21
21
22 class QueryTest < ActiveSupport::TestCase
22 class QueryTest < ActiveSupport::TestCase
23 include Redmine::I18n
23 include Redmine::I18n
24
24
25 fixtures :projects, :enabled_modules, :users, :members,
25 fixtures :projects, :enabled_modules, :users, :members,
26 :member_roles, :roles, :trackers, :issue_statuses,
26 :member_roles, :roles, :trackers, :issue_statuses,
27 :issue_categories, :enumerations, :issues,
27 :issue_categories, :enumerations, :issues,
28 :watchers, :custom_fields, :custom_values, :versions,
28 :watchers, :custom_fields, :custom_values, :versions,
29 :queries,
29 :queries,
30 :projects_trackers,
30 :projects_trackers,
31 :custom_fields_trackers,
31 :custom_fields_trackers,
32 :workflows
32 :workflows
33
33
34 def setup
34 def setup
35 User.current = nil
35 User.current = nil
36 end
36 end
37
37
38 def test_query_with_roles_visibility_should_validate_roles
38 def test_query_with_roles_visibility_should_validate_roles
39 set_language_if_valid 'en'
39 set_language_if_valid 'en'
40 query = IssueQuery.new(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES)
40 query = IssueQuery.new(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES)
41 assert !query.save
41 assert !query.save
42 assert_include "Roles cannot be blank", query.errors.full_messages
42 assert_include "Roles cannot be blank", query.errors.full_messages
43 query.role_ids = [1, 2]
43 query.role_ids = [1, 2]
44 assert query.save
44 assert query.save
45 end
45 end
46
46
47 def test_changing_roles_visibility_should_clear_roles
47 def test_changing_roles_visibility_should_clear_roles
48 query = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES, :role_ids => [1, 2])
48 query = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES, :role_ids => [1, 2])
49 assert_equal 2, query.roles.count
49 assert_equal 2, query.roles.count
50
50
51 query.visibility = IssueQuery::VISIBILITY_PUBLIC
51 query.visibility = IssueQuery::VISIBILITY_PUBLIC
52 query.save!
52 query.save!
53 assert_equal 0, query.roles.count
53 assert_equal 0, query.roles.count
54 end
54 end
55
55
56 def test_available_filters_should_be_ordered
56 def test_available_filters_should_be_ordered
57 set_language_if_valid 'en'
57 set_language_if_valid 'en'
58 query = IssueQuery.new
58 query = IssueQuery.new
59 assert_equal 0, query.available_filters.keys.index('status_id')
59 assert_equal 0, query.available_filters.keys.index('status_id')
60 expected_order = [
60 expected_order = [
61 "Status",
61 "Status",
62 "Project",
62 "Project",
63 "Tracker",
63 "Tracker",
64 "Priority"
64 "Priority"
65 ]
65 ]
66 assert_equal expected_order,
66 assert_equal expected_order,
67 (query.available_filters.values.map{|v| v[:name]} & expected_order)
67 (query.available_filters.values.map{|v| v[:name]} & expected_order)
68 end
68 end
69
69
70 def test_available_filters_with_custom_fields_should_be_ordered
70 def test_available_filters_with_custom_fields_should_be_ordered
71 set_language_if_valid 'en'
71 set_language_if_valid 'en'
72 UserCustomField.create!(
72 UserCustomField.create!(
73 :name => 'order test', :field_format => 'string',
73 :name => 'order test', :field_format => 'string',
74 :is_for_all => true, :is_filter => true
74 :is_for_all => true, :is_filter => true
75 )
75 )
76 query = IssueQuery.new
76 query = IssueQuery.new
77 expected_order = [
77 expected_order = [
78 "Searchable field",
78 "Searchable field",
79 "Database",
79 "Database",
80 "Project's Development status",
80 "Project's Development status",
81 "Author's order test",
81 "Author's order test",
82 "Assignee's order test"
82 "Assignee's order test"
83 ]
83 ]
84 assert_equal expected_order,
84 assert_equal expected_order,
85 (query.available_filters.values.map{|v| v[:name]} & expected_order)
85 (query.available_filters.values.map{|v| v[:name]} & expected_order)
86 end
86 end
87
87
88 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
88 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
89 query = IssueQuery.new(:project => nil, :name => '_')
89 query = IssueQuery.new(:project => nil, :name => '_')
90 assert query.available_filters.has_key?('cf_1')
90 assert query.available_filters.has_key?('cf_1')
91 assert !query.available_filters.has_key?('cf_3')
91 assert !query.available_filters.has_key?('cf_3')
92 end
92 end
93
93
94 def test_system_shared_versions_should_be_available_in_global_queries
94 def test_system_shared_versions_should_be_available_in_global_queries
95 Version.find(2).update_attribute :sharing, 'system'
95 Version.find(2).update_attribute :sharing, 'system'
96 query = IssueQuery.new(:project => nil, :name => '_')
96 query = IssueQuery.new(:project => nil, :name => '_')
97 assert query.available_filters.has_key?('fixed_version_id')
97 assert query.available_filters.has_key?('fixed_version_id')
98 assert query.available_filters['fixed_version_id'][:values].detect {|v| v[1] == '2'}
98 assert query.available_filters['fixed_version_id'][:values].detect {|v| v[1] == '2'}
99 end
99 end
100
100
101 def test_project_filter_in_global_queries
101 def test_project_filter_in_global_queries
102 query = IssueQuery.new(:project => nil, :name => '_')
102 query = IssueQuery.new(:project => nil, :name => '_')
103 project_filter = query.available_filters["project_id"]
103 project_filter = query.available_filters["project_id"]
104 assert_not_nil project_filter
104 assert_not_nil project_filter
105 project_ids = project_filter[:values].map{|p| p[1]}
105 project_ids = project_filter[:values].map{|p| p[1]}
106 assert project_ids.include?("1") #public project
106 assert project_ids.include?("1") #public project
107 assert !project_ids.include?("2") #private project user cannot see
107 assert !project_ids.include?("2") #private project user cannot see
108 end
108 end
109
109
110 def test_available_filters_should_not_include_fields_disabled_on_all_trackers
110 def test_available_filters_should_not_include_fields_disabled_on_all_trackers
111 Tracker.all.each do |tracker|
111 Tracker.all.each do |tracker|
112 tracker.core_fields = Tracker::CORE_FIELDS - ['start_date']
112 tracker.core_fields = Tracker::CORE_FIELDS - ['start_date']
113 tracker.save!
113 tracker.save!
114 end
114 end
115
115
116 query = IssueQuery.new(:name => '_')
116 query = IssueQuery.new(:name => '_')
117 assert_include 'due_date', query.available_filters
117 assert_include 'due_date', query.available_filters
118 assert_not_include 'start_date', query.available_filters
118 assert_not_include 'start_date', query.available_filters
119 end
119 end
120
120
121 def test_filter_values_without_project_should_be_arrays
121 def test_filter_values_without_project_should_be_arrays
122 q = IssueQuery.new
122 q = IssueQuery.new
123 assert_nil q.project
123 assert_nil q.project
124
124
125 q.available_filters.each do |name, filter|
125 q.available_filters.each do |name, filter|
126 values = filter.values
126 values = filter.values
127 assert (values.nil? || values.is_a?(Array)),
127 assert (values.nil? || values.is_a?(Array)),
128 "#values for #{name} filter returned a #{values.class.name}"
128 "#values for #{name} filter returned a #{values.class.name}"
129 end
129 end
130 end
130 end
131
131
132 def test_filter_values_with_project_should_be_arrays
132 def test_filter_values_with_project_should_be_arrays
133 q = IssueQuery.new(:project => Project.find(1))
133 q = IssueQuery.new(:project => Project.find(1))
134 assert_not_nil q.project
134 assert_not_nil q.project
135
135
136 q.available_filters.each do |name, filter|
136 q.available_filters.each do |name, filter|
137 values = filter.values
137 values = filter.values
138 assert (values.nil? || values.is_a?(Array)),
138 assert (values.nil? || values.is_a?(Array)),
139 "#values for #{name} filter returned a #{values.class.name}"
139 "#values for #{name} filter returned a #{values.class.name}"
140 end
140 end
141 end
141 end
142
142
143 def find_issues_with_query(query)
143 def find_issues_with_query(query)
144 Issue.joins(:status, :tracker, :project, :priority).where(
144 Issue.joins(:status, :tracker, :project, :priority).where(
145 query.statement
145 query.statement
146 ).to_a
146 ).to_a
147 end
147 end
148
148
149 def assert_find_issues_with_query_is_successful(query)
149 def assert_find_issues_with_query_is_successful(query)
150 assert_nothing_raised do
150 assert_nothing_raised do
151 find_issues_with_query(query)
151 find_issues_with_query(query)
152 end
152 end
153 end
153 end
154
154
155 def assert_query_statement_includes(query, condition)
155 def assert_query_statement_includes(query, condition)
156 assert_include condition, query.statement
156 assert_include condition, query.statement
157 end
157 end
158
158
159 def assert_query_result(expected, query)
159 def assert_query_result(expected, query)
160 assert_nothing_raised do
160 assert_nothing_raised do
161 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
161 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
162 assert_equal expected.size, query.issue_count
162 assert_equal expected.size, query.issue_count
163 end
163 end
164 end
164 end
165
165
166 def test_query_should_allow_shared_versions_for_a_project_query
166 def test_query_should_allow_shared_versions_for_a_project_query
167 subproject_version = Version.find(4)
167 subproject_version = Version.find(4)
168 query = IssueQuery.new(:project => Project.find(1), :name => '_')
168 query = IssueQuery.new(:project => Project.find(1), :name => '_')
169 filter = query.available_filters["fixed_version_id"]
169 filter = query.available_filters["fixed_version_id"]
170 assert_not_nil filter
170 assert_not_nil filter
171 assert_include subproject_version.id.to_s, filter[:values].map(&:second)
171 assert_include subproject_version.id.to_s, filter[:values].map(&:second)
172 end
172 end
173
173
174 def test_query_with_multiple_custom_fields
174 def test_query_with_multiple_custom_fields
175 query = IssueQuery.find(1)
175 query = IssueQuery.find(1)
176 assert query.valid?
176 assert query.valid?
177 issues = find_issues_with_query(query)
177 issues = find_issues_with_query(query)
178 assert_equal 1, issues.length
178 assert_equal 1, issues.length
179 assert_equal Issue.find(3), issues.first
179 assert_equal Issue.find(3), issues.first
180 end
180 end
181
181
182 def test_operator_none
182 def test_operator_none
183 query = IssueQuery.new(:project => Project.find(1), :name => '_')
183 query = IssueQuery.new(:project => Project.find(1), :name => '_')
184 query.add_filter('fixed_version_id', '!*', [''])
184 query.add_filter('fixed_version_id', '!*', [''])
185 query.add_filter('cf_1', '!*', [''])
185 query.add_filter('cf_1', '!*', [''])
186 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
186 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
187 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
187 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
188 find_issues_with_query(query)
188 find_issues_with_query(query)
189 end
189 end
190
190
191 def test_operator_none_for_integer
191 def test_operator_none_for_integer
192 query = IssueQuery.new(:project => Project.find(1), :name => '_')
192 query = IssueQuery.new(:project => Project.find(1), :name => '_')
193 query.add_filter('estimated_hours', '!*', [''])
193 query.add_filter('estimated_hours', '!*', [''])
194 issues = find_issues_with_query(query)
194 issues = find_issues_with_query(query)
195 assert !issues.empty?
195 assert !issues.empty?
196 assert issues.all? {|i| !i.estimated_hours}
196 assert issues.all? {|i| !i.estimated_hours}
197 end
197 end
198
198
199 def test_operator_none_for_date
199 def test_operator_none_for_date
200 query = IssueQuery.new(:project => Project.find(1), :name => '_')
200 query = IssueQuery.new(:project => Project.find(1), :name => '_')
201 query.add_filter('start_date', '!*', [''])
201 query.add_filter('start_date', '!*', [''])
202 issues = find_issues_with_query(query)
202 issues = find_issues_with_query(query)
203 assert !issues.empty?
203 assert !issues.empty?
204 assert issues.all? {|i| i.start_date.nil?}
204 assert issues.all? {|i| i.start_date.nil?}
205 end
205 end
206
206
207 def test_operator_none_for_string_custom_field
207 def test_operator_none_for_string_custom_field
208 CustomField.find(2).update_attribute :default_value, ""
208 CustomField.find(2).update_attribute :default_value, ""
209 query = IssueQuery.new(:project => Project.find(1), :name => '_')
209 query = IssueQuery.new(:project => Project.find(1), :name => '_')
210 query.add_filter('cf_2', '!*', [''])
210 query.add_filter('cf_2', '!*', [''])
211 assert query.has_filter?('cf_2')
211 assert query.has_filter?('cf_2')
212 issues = find_issues_with_query(query)
212 issues = find_issues_with_query(query)
213 assert !issues.empty?
213 assert !issues.empty?
214 assert issues.all? {|i| i.custom_field_value(2).blank?}
214 assert issues.all? {|i| i.custom_field_value(2).blank?}
215 end
215 end
216
216
217 def test_operator_all
217 def test_operator_all
218 query = IssueQuery.new(:project => Project.find(1), :name => '_')
218 query = IssueQuery.new(:project => Project.find(1), :name => '_')
219 query.add_filter('fixed_version_id', '*', [''])
219 query.add_filter('fixed_version_id', '*', [''])
220 query.add_filter('cf_1', '*', [''])
220 query.add_filter('cf_1', '*', [''])
221 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
221 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
222 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
222 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
223 find_issues_with_query(query)
223 find_issues_with_query(query)
224 end
224 end
225
225
226 def test_operator_all_for_date
226 def test_operator_all_for_date
227 query = IssueQuery.new(:project => Project.find(1), :name => '_')
227 query = IssueQuery.new(:project => Project.find(1), :name => '_')
228 query.add_filter('start_date', '*', [''])
228 query.add_filter('start_date', '*', [''])
229 issues = find_issues_with_query(query)
229 issues = find_issues_with_query(query)
230 assert !issues.empty?
230 assert !issues.empty?
231 assert issues.all? {|i| i.start_date.present?}
231 assert issues.all? {|i| i.start_date.present?}
232 end
232 end
233
233
234 def test_operator_all_for_string_custom_field
234 def test_operator_all_for_string_custom_field
235 query = IssueQuery.new(:project => Project.find(1), :name => '_')
235 query = IssueQuery.new(:project => Project.find(1), :name => '_')
236 query.add_filter('cf_2', '*', [''])
236 query.add_filter('cf_2', '*', [''])
237 assert query.has_filter?('cf_2')
237 assert query.has_filter?('cf_2')
238 issues = find_issues_with_query(query)
238 issues = find_issues_with_query(query)
239 assert !issues.empty?
239 assert !issues.empty?
240 assert issues.all? {|i| i.custom_field_value(2).present?}
240 assert issues.all? {|i| i.custom_field_value(2).present?}
241 end
241 end
242
242
243 def test_numeric_filter_should_not_accept_non_numeric_values
243 def test_numeric_filter_should_not_accept_non_numeric_values
244 query = IssueQuery.new(:name => '_')
244 query = IssueQuery.new(:name => '_')
245 query.add_filter('estimated_hours', '=', ['a'])
245 query.add_filter('estimated_hours', '=', ['a'])
246
246
247 assert query.has_filter?('estimated_hours')
247 assert query.has_filter?('estimated_hours')
248 assert !query.valid?
248 assert !query.valid?
249 end
249 end
250
250
251 def test_operator_is_on_float
251 def test_operator_is_on_float
252 Issue.where(:id => 2).update_all("estimated_hours = 171.2")
252 Issue.where(:id => 2).update_all("estimated_hours = 171.2")
253 query = IssueQuery.new(:name => '_')
253 query = IssueQuery.new(:name => '_')
254 query.add_filter('estimated_hours', '=', ['171.20'])
254 query.add_filter('estimated_hours', '=', ['171.20'])
255 issues = find_issues_with_query(query)
255 issues = find_issues_with_query(query)
256 assert_equal 1, issues.size
256 assert_equal 1, issues.size
257 assert_equal 2, issues.first.id
257 assert_equal 2, issues.first.id
258 end
258 end
259
259
260 def test_operator_is_on_issue_id_should_accept_comma_separated_values
260 def test_operator_is_on_issue_id_should_accept_comma_separated_values
261 query = IssueQuery.new(:name => '_')
261 query = IssueQuery.new(:name => '_')
262 query.add_filter("issue_id", '=', ['1,3'])
262 query.add_filter("issue_id", '=', ['1,3'])
263 issues = find_issues_with_query(query)
263 issues = find_issues_with_query(query)
264 assert_equal 2, issues.size
264 assert_equal 2, issues.size
265 assert_equal [1,3], issues.map(&:id).sort
265 assert_equal [1,3], issues.map(&:id).sort
266 end
266 end
267
267
268 def test_operator_between_on_issue_id_should_return_range
268 def test_operator_between_on_issue_id_should_return_range
269 query = IssueQuery.new(:name => '_')
269 query = IssueQuery.new(:name => '_')
270 query.add_filter("issue_id", '><', ['2','3'])
270 query.add_filter("issue_id", '><', ['2','3'])
271 issues = find_issues_with_query(query)
271 issues = find_issues_with_query(query)
272 assert_equal 2, issues.size
272 assert_equal 2, issues.size
273 assert_equal [2,3], issues.map(&:id).sort
273 assert_equal [2,3], issues.map(&:id).sort
274 end
274 end
275
275
276 def test_operator_is_on_integer_custom_field
276 def test_operator_is_on_integer_custom_field
277 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true, :trackers => Tracker.all)
277 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true, :trackers => Tracker.all)
278 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
278 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
279 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
279 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
280 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
280 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
281
281
282 query = IssueQuery.new(:name => '_')
282 query = IssueQuery.new(:name => '_')
283 query.add_filter("cf_#{f.id}", '=', ['12'])
283 query.add_filter("cf_#{f.id}", '=', ['12'])
284 issues = find_issues_with_query(query)
284 issues = find_issues_with_query(query)
285 assert_equal 1, issues.size
285 assert_equal 1, issues.size
286 assert_equal 2, issues.first.id
286 assert_equal 2, issues.first.id
287 end
287 end
288
288
289 def test_operator_is_on_integer_custom_field_should_accept_negative_value
289 def test_operator_is_on_integer_custom_field_should_accept_negative_value
290 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true, :trackers => Tracker.all)
290 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true, :trackers => Tracker.all)
291 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
291 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
292 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12')
292 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12')
293 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
293 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
294
294
295 query = IssueQuery.new(:name => '_')
295 query = IssueQuery.new(:name => '_')
296 query.add_filter("cf_#{f.id}", '=', ['-12'])
296 query.add_filter("cf_#{f.id}", '=', ['-12'])
297 assert query.valid?
297 assert query.valid?
298 issues = find_issues_with_query(query)
298 issues = find_issues_with_query(query)
299 assert_equal 1, issues.size
299 assert_equal 1, issues.size
300 assert_equal 2, issues.first.id
300 assert_equal 2, issues.first.id
301 end
301 end
302
302
303 def test_operator_is_on_float_custom_field
303 def test_operator_is_on_float_custom_field
304 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
304 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
305 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
305 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
306 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
306 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
307 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
307 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
308
308
309 query = IssueQuery.new(:name => '_')
309 query = IssueQuery.new(:name => '_')
310 query.add_filter("cf_#{f.id}", '=', ['12.7'])
310 query.add_filter("cf_#{f.id}", '=', ['12.7'])
311 issues = find_issues_with_query(query)
311 issues = find_issues_with_query(query)
312 assert_equal 1, issues.size
312 assert_equal 1, issues.size
313 assert_equal 2, issues.first.id
313 assert_equal 2, issues.first.id
314 end
314 end
315
315
316 def test_operator_is_on_float_custom_field_should_accept_negative_value
316 def test_operator_is_on_float_custom_field_should_accept_negative_value
317 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
317 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
318 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
318 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
319 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7')
319 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7')
320 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
320 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
321
321
322 query = IssueQuery.new(:name => '_')
322 query = IssueQuery.new(:name => '_')
323 query.add_filter("cf_#{f.id}", '=', ['-12.7'])
323 query.add_filter("cf_#{f.id}", '=', ['-12.7'])
324 assert query.valid?
324 assert query.valid?
325 issues = find_issues_with_query(query)
325 issues = find_issues_with_query(query)
326 assert_equal 1, issues.size
326 assert_equal 1, issues.size
327 assert_equal 2, issues.first.id
327 assert_equal 2, issues.first.id
328 end
328 end
329
329
330 def test_operator_is_on_multi_list_custom_field
330 def test_operator_is_on_multi_list_custom_field
331 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
331 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
332 :possible_values => ['value1', 'value2', 'value3'], :multiple => true, :trackers => Tracker.all)
332 :possible_values => ['value1', 'value2', 'value3'], :multiple => true, :trackers => Tracker.all)
333 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
333 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
334 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
334 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
335 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
335 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
336
336
337 query = IssueQuery.new(:name => '_')
337 query = IssueQuery.new(:name => '_')
338 query.add_filter("cf_#{f.id}", '=', ['value1'])
338 query.add_filter("cf_#{f.id}", '=', ['value1'])
339 issues = find_issues_with_query(query)
339 issues = find_issues_with_query(query)
340 assert_equal [1, 3], issues.map(&:id).sort
340 assert_equal [1, 3], issues.map(&:id).sort
341
341
342 query = IssueQuery.new(:name => '_')
342 query = IssueQuery.new(:name => '_')
343 query.add_filter("cf_#{f.id}", '=', ['value2'])
343 query.add_filter("cf_#{f.id}", '=', ['value2'])
344 issues = find_issues_with_query(query)
344 issues = find_issues_with_query(query)
345 assert_equal [1], issues.map(&:id).sort
345 assert_equal [1], issues.map(&:id).sort
346 end
346 end
347
347
348 def test_operator_is_not_on_multi_list_custom_field
348 def test_operator_is_not_on_multi_list_custom_field
349 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
349 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
350 :possible_values => ['value1', 'value2', 'value3'], :multiple => true, :trackers => Tracker.all)
350 :possible_values => ['value1', 'value2', 'value3'], :multiple => true, :trackers => Tracker.all)
351 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
351 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
352 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
352 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
353 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
353 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
354
354
355 query = IssueQuery.new(:name => '_')
355 query = IssueQuery.new(:name => '_')
356 query.add_filter("cf_#{f.id}", '!', ['value1'])
356 query.add_filter("cf_#{f.id}", '!', ['value1'])
357 issues = find_issues_with_query(query)
357 issues = find_issues_with_query(query)
358 assert !issues.map(&:id).include?(1)
358 assert !issues.map(&:id).include?(1)
359 assert !issues.map(&:id).include?(3)
359 assert !issues.map(&:id).include?(3)
360
360
361 query = IssueQuery.new(:name => '_')
361 query = IssueQuery.new(:name => '_')
362 query.add_filter("cf_#{f.id}", '!', ['value2'])
362 query.add_filter("cf_#{f.id}", '!', ['value2'])
363 issues = find_issues_with_query(query)
363 issues = find_issues_with_query(query)
364 assert !issues.map(&:id).include?(1)
364 assert !issues.map(&:id).include?(1)
365 assert issues.map(&:id).include?(3)
365 assert issues.map(&:id).include?(3)
366 end
366 end
367
367
368 def test_operator_is_on_string_custom_field_with_utf8_value
368 def test_operator_is_on_string_custom_field_with_utf8_value
369 f = IssueCustomField.create!(:name => 'filter', :field_format => 'string', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
369 f = IssueCustomField.create!(:name => 'filter', :field_format => 'string', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
370 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'KiÑ»ƒm')
370 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'KiÑ»ƒm')
371
371
372 query = IssueQuery.new(:name => '_')
372 query = IssueQuery.new(:name => '_')
373 query.add_filter("cf_#{f.id}", '=', ['KiÑ»ƒm'])
373 query.add_filter("cf_#{f.id}", '=', ['KiÑ»ƒm'])
374 issues = find_issues_with_query(query)
374 issues = find_issues_with_query(query)
375 assert_equal [1], issues.map(&:id).sort
375 assert_equal [1], issues.map(&:id).sort
376 end
376 end
377
377
378 def test_operator_is_on_is_private_field
378 def test_operator_is_on_is_private_field
379 # is_private filter only available for those who can set issues private
379 # is_private filter only available for those who can set issues private
380 User.current = User.find(2)
380 User.current = User.find(2)
381
381
382 query = IssueQuery.new(:name => '_')
382 query = IssueQuery.new(:name => '_')
383 assert query.available_filters.key?('is_private')
383 assert query.available_filters.key?('is_private')
384
384
385 query.add_filter("is_private", '=', ['1'])
385 query.add_filter("is_private", '=', ['1'])
386 issues = find_issues_with_query(query)
386 issues = find_issues_with_query(query)
387 assert issues.any?
387 assert issues.any?
388 assert_nil issues.detect {|issue| !issue.is_private?}
388 assert_nil issues.detect {|issue| !issue.is_private?}
389 ensure
389 ensure
390 User.current = nil
390 User.current = nil
391 end
391 end
392
392
393 def test_operator_is_not_on_is_private_field
393 def test_operator_is_not_on_is_private_field
394 # is_private filter only available for those who can set issues private
394 # is_private filter only available for those who can set issues private
395 User.current = User.find(2)
395 User.current = User.find(2)
396
396
397 query = IssueQuery.new(:name => '_')
397 query = IssueQuery.new(:name => '_')
398 assert query.available_filters.key?('is_private')
398 assert query.available_filters.key?('is_private')
399
399
400 query.add_filter("is_private", '!', ['1'])
400 query.add_filter("is_private", '!', ['1'])
401 issues = find_issues_with_query(query)
401 issues = find_issues_with_query(query)
402 assert issues.any?
402 assert issues.any?
403 assert_nil issues.detect {|issue| issue.is_private?}
403 assert_nil issues.detect {|issue| issue.is_private?}
404 ensure
404 ensure
405 User.current = nil
405 User.current = nil
406 end
406 end
407
407
408 def test_operator_greater_than
408 def test_operator_greater_than
409 query = IssueQuery.new(:project => Project.find(1), :name => '_')
409 query = IssueQuery.new(:project => Project.find(1), :name => '_')
410 query.add_filter('done_ratio', '>=', ['40'])
410 query.add_filter('done_ratio', '>=', ['40'])
411 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
411 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
412 find_issues_with_query(query)
412 find_issues_with_query(query)
413 end
413 end
414
414
415 def test_operator_greater_than_a_float
415 def test_operator_greater_than_a_float
416 query = IssueQuery.new(:project => Project.find(1), :name => '_')
416 query = IssueQuery.new(:project => Project.find(1), :name => '_')
417 query.add_filter('estimated_hours', '>=', ['40.5'])
417 query.add_filter('estimated_hours', '>=', ['40.5'])
418 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
418 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
419 find_issues_with_query(query)
419 find_issues_with_query(query)
420 end
420 end
421
421
422 def test_operator_greater_than_on_int_custom_field
422 def test_operator_greater_than_on_int_custom_field
423 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
423 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
424 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
424 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
425 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
425 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
426 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
426 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
427
427
428 query = IssueQuery.new(:project => Project.find(1), :name => '_')
428 query = IssueQuery.new(:project => Project.find(1), :name => '_')
429 query.add_filter("cf_#{f.id}", '>=', ['8'])
429 query.add_filter("cf_#{f.id}", '>=', ['8'])
430 issues = find_issues_with_query(query)
430 issues = find_issues_with_query(query)
431 assert_equal 1, issues.size
431 assert_equal 1, issues.size
432 assert_equal 2, issues.first.id
432 assert_equal 2, issues.first.id
433 end
433 end
434
434
435 def test_operator_lesser_than
435 def test_operator_lesser_than
436 query = IssueQuery.new(:project => Project.find(1), :name => '_')
436 query = IssueQuery.new(:project => Project.find(1), :name => '_')
437 query.add_filter('done_ratio', '<=', ['30'])
437 query.add_filter('done_ratio', '<=', ['30'])
438 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
438 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
439 find_issues_with_query(query)
439 find_issues_with_query(query)
440 end
440 end
441
441
442 def test_operator_lesser_than_on_custom_field
442 def test_operator_lesser_than_on_custom_field
443 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
443 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
444 query = IssueQuery.new(:project => Project.find(1), :name => '_')
444 query = IssueQuery.new(:project => Project.find(1), :name => '_')
445 query.add_filter("cf_#{f.id}", '<=', ['30'])
445 query.add_filter("cf_#{f.id}", '<=', ['30'])
446 assert_match /CAST.+ <= 30\.0/, query.statement
446 assert_match /CAST.+ <= 30\.0/, query.statement
447 find_issues_with_query(query)
447 find_issues_with_query(query)
448 end
448 end
449
449
450 def test_operator_lesser_than_on_date_custom_field
450 def test_operator_lesser_than_on_date_custom_field
451 f = IssueCustomField.create!(:name => 'filter', :field_format => 'date', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
451 f = IssueCustomField.create!(:name => 'filter', :field_format => 'date', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
452 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '2013-04-11')
452 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '2013-04-11')
453 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '2013-05-14')
453 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '2013-05-14')
454 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
454 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
455
455
456 query = IssueQuery.new(:project => Project.find(1), :name => '_')
456 query = IssueQuery.new(:project => Project.find(1), :name => '_')
457 query.add_filter("cf_#{f.id}", '<=', ['2013-05-01'])
457 query.add_filter("cf_#{f.id}", '<=', ['2013-05-01'])
458 issue_ids = find_issues_with_query(query).map(&:id)
458 issue_ids = find_issues_with_query(query).map(&:id)
459 assert_include 1, issue_ids
459 assert_include 1, issue_ids
460 assert_not_include 2, issue_ids
460 assert_not_include 2, issue_ids
461 assert_not_include 3, issue_ids
461 assert_not_include 3, issue_ids
462 end
462 end
463
463
464 def test_operator_between
464 def test_operator_between
465 query = IssueQuery.new(:project => Project.find(1), :name => '_')
465 query = IssueQuery.new(:project => Project.find(1), :name => '_')
466 query.add_filter('done_ratio', '><', ['30', '40'])
466 query.add_filter('done_ratio', '><', ['30', '40'])
467 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
467 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
468 find_issues_with_query(query)
468 find_issues_with_query(query)
469 end
469 end
470
470
471 def test_operator_between_on_custom_field
471 def test_operator_between_on_custom_field
472 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
472 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
473 query = IssueQuery.new(:project => Project.find(1), :name => '_')
473 query = IssueQuery.new(:project => Project.find(1), :name => '_')
474 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
474 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
475 assert_match /CAST.+ BETWEEN 30.0 AND 40.0/, query.statement
475 assert_match /CAST.+ BETWEEN 30.0 AND 40.0/, query.statement
476 find_issues_with_query(query)
476 find_issues_with_query(query)
477 end
477 end
478
478
479 def test_date_filter_should_not_accept_non_date_values
479 def test_date_filter_should_not_accept_non_date_values
480 query = IssueQuery.new(:name => '_')
480 query = IssueQuery.new(:name => '_')
481 query.add_filter('created_on', '=', ['a'])
481 query.add_filter('created_on', '=', ['a'])
482
482
483 assert query.has_filter?('created_on')
483 assert query.has_filter?('created_on')
484 assert !query.valid?
484 assert !query.valid?
485 end
485 end
486
486
487 def test_date_filter_should_not_accept_invalid_date_values
487 def test_date_filter_should_not_accept_invalid_date_values
488 query = IssueQuery.new(:name => '_')
488 query = IssueQuery.new(:name => '_')
489 query.add_filter('created_on', '=', ['2011-01-34'])
489 query.add_filter('created_on', '=', ['2011-01-34'])
490
490
491 assert query.has_filter?('created_on')
491 assert query.has_filter?('created_on')
492 assert !query.valid?
492 assert !query.valid?
493 end
493 end
494
494
495 def test_relative_date_filter_should_not_accept_non_integer_values
495 def test_relative_date_filter_should_not_accept_non_integer_values
496 query = IssueQuery.new(:name => '_')
496 query = IssueQuery.new(:name => '_')
497 query.add_filter('created_on', '>t-', ['a'])
497 query.add_filter('created_on', '>t-', ['a'])
498
498
499 assert query.has_filter?('created_on')
499 assert query.has_filter?('created_on')
500 assert !query.valid?
500 assert !query.valid?
501 end
501 end
502
502
503 def test_operator_date_equals
503 def test_operator_date_equals
504 query = IssueQuery.new(:name => '_')
504 query = IssueQuery.new(:name => '_')
505 query.add_filter('due_date', '=', ['2011-07-10'])
505 query.add_filter('due_date', '=', ['2011-07-10'])
506 assert_match /issues\.due_date > '#{quoted_date "2011-07-09"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-07-10"} 23:59:59(\.\d+)?/,
506 assert_match /issues\.due_date > '#{quoted_date "2011-07-09"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-07-10"} 23:59:59(\.\d+)?/,
507 query.statement
507 query.statement
508 find_issues_with_query(query)
508 find_issues_with_query(query)
509 end
509 end
510
510
511 def test_operator_date_lesser_than
511 def test_operator_date_lesser_than
512 query = IssueQuery.new(:name => '_')
512 query = IssueQuery.new(:name => '_')
513 query.add_filter('due_date', '<=', ['2011-07-10'])
513 query.add_filter('due_date', '<=', ['2011-07-10'])
514 assert_match /issues\.due_date <= '#{quoted_date "2011-07-10"} 23:59:59(\.\d+)?/, query.statement
514 assert_match /issues\.due_date <= '#{quoted_date "2011-07-10"} 23:59:59(\.\d+)?/, query.statement
515 find_issues_with_query(query)
515 find_issues_with_query(query)
516 end
516 end
517
517
518 def test_operator_date_lesser_than_with_timestamp
518 def test_operator_date_lesser_than_with_timestamp
519 query = IssueQuery.new(:name => '_')
519 query = IssueQuery.new(:name => '_')
520 query.add_filter('updated_on', '<=', ['2011-07-10T19:13:52'])
520 query.add_filter('updated_on', '<=', ['2011-07-10T19:13:52'])
521 assert_match /issues\.updated_on <= '#{quoted_date "2011-07-10"} 19:13:52/, query.statement
521 assert_match /issues\.updated_on <= '#{quoted_date "2011-07-10"} 19:13:52/, query.statement
522 find_issues_with_query(query)
522 find_issues_with_query(query)
523 end
523 end
524
524
525 def test_operator_date_greater_than
525 def test_operator_date_greater_than
526 query = IssueQuery.new(:name => '_')
526 query = IssueQuery.new(:name => '_')
527 query.add_filter('due_date', '>=', ['2011-07-10'])
527 query.add_filter('due_date', '>=', ['2011-07-10'])
528 assert_match /issues\.due_date > '#{quoted_date "2011-07-09"} 23:59:59(\.\d+)?'/, query.statement
528 assert_match /issues\.due_date > '#{quoted_date "2011-07-09"} 23:59:59(\.\d+)?'/, query.statement
529 find_issues_with_query(query)
529 find_issues_with_query(query)
530 end
530 end
531
531
532 def test_operator_date_greater_than_with_timestamp
532 def test_operator_date_greater_than_with_timestamp
533 query = IssueQuery.new(:name => '_')
533 query = IssueQuery.new(:name => '_')
534 query.add_filter('updated_on', '>=', ['2011-07-10T19:13:52'])
534 query.add_filter('updated_on', '>=', ['2011-07-10T19:13:52'])
535 assert_match /issues\.updated_on > '#{quoted_date "2011-07-10"} 19:13:51(\.0+)?'/, query.statement
535 assert_match /issues\.updated_on > '#{quoted_date "2011-07-10"} 19:13:51(\.0+)?'/, query.statement
536 find_issues_with_query(query)
536 find_issues_with_query(query)
537 end
537 end
538
538
539 def test_operator_date_between
539 def test_operator_date_between
540 query = IssueQuery.new(:name => '_')
540 query = IssueQuery.new(:name => '_')
541 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
541 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
542 assert_match /issues\.due_date > '#{quoted_date "2011-06-22"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-07-10"} 23:59:59(\.\d+)?'/,
542 assert_match /issues\.due_date > '#{quoted_date "2011-06-22"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-07-10"} 23:59:59(\.\d+)?'/,
543 query.statement
543 query.statement
544 find_issues_with_query(query)
544 find_issues_with_query(query)
545 end
545 end
546
546
547 def test_operator_in_more_than
547 def test_operator_in_more_than
548 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
548 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
549 query = IssueQuery.new(:project => Project.find(1), :name => '_')
549 query = IssueQuery.new(:project => Project.find(1), :name => '_')
550 query.add_filter('due_date', '>t+', ['15'])
550 query.add_filter('due_date', '>t+', ['15'])
551 issues = find_issues_with_query(query)
551 issues = find_issues_with_query(query)
552 assert !issues.empty?
552 assert !issues.empty?
553 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
553 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
554 end
554 end
555
555
556 def test_operator_in_less_than
556 def test_operator_in_less_than
557 query = IssueQuery.new(:project => Project.find(1), :name => '_')
557 query = IssueQuery.new(:project => Project.find(1), :name => '_')
558 query.add_filter('due_date', '<t+', ['15'])
558 query.add_filter('due_date', '<t+', ['15'])
559 issues = find_issues_with_query(query)
559 issues = find_issues_with_query(query)
560 assert !issues.empty?
560 assert !issues.empty?
561 issues.each {|issue| assert(issue.due_date <= (Date.today + 15))}
561 issues.each {|issue| assert(issue.due_date <= (Date.today + 15))}
562 end
562 end
563
563
564 def test_operator_in_the_next_days
564 def test_operator_in_the_next_days
565 query = IssueQuery.new(:project => Project.find(1), :name => '_')
565 query = IssueQuery.new(:project => Project.find(1), :name => '_')
566 query.add_filter('due_date', '><t+', ['15'])
566 query.add_filter('due_date', '><t+', ['15'])
567 issues = find_issues_with_query(query)
567 issues = find_issues_with_query(query)
568 assert !issues.empty?
568 assert !issues.empty?
569 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
569 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
570 end
570 end
571
571
572 def test_operator_less_than_ago
572 def test_operator_less_than_ago
573 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
573 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
574 query = IssueQuery.new(:project => Project.find(1), :name => '_')
574 query = IssueQuery.new(:project => Project.find(1), :name => '_')
575 query.add_filter('due_date', '>t-', ['3'])
575 query.add_filter('due_date', '>t-', ['3'])
576 issues = find_issues_with_query(query)
576 issues = find_issues_with_query(query)
577 assert !issues.empty?
577 assert !issues.empty?
578 issues.each {|issue| assert(issue.due_date >= (Date.today - 3))}
578 issues.each {|issue| assert(issue.due_date >= (Date.today - 3))}
579 end
579 end
580
580
581 def test_operator_in_the_past_days
581 def test_operator_in_the_past_days
582 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
582 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
583 query = IssueQuery.new(:project => Project.find(1), :name => '_')
583 query = IssueQuery.new(:project => Project.find(1), :name => '_')
584 query.add_filter('due_date', '><t-', ['3'])
584 query.add_filter('due_date', '><t-', ['3'])
585 issues = find_issues_with_query(query)
585 issues = find_issues_with_query(query)
586 assert !issues.empty?
586 assert !issues.empty?
587 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
587 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
588 end
588 end
589
589
590 def test_operator_more_than_ago
590 def test_operator_more_than_ago
591 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
591 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
592 query = IssueQuery.new(:project => Project.find(1), :name => '_')
592 query = IssueQuery.new(:project => Project.find(1), :name => '_')
593 query.add_filter('due_date', '<t-', ['10'])
593 query.add_filter('due_date', '<t-', ['10'])
594 assert query.statement.include?("#{Issue.table_name}.due_date <=")
594 assert query.statement.include?("#{Issue.table_name}.due_date <=")
595 issues = find_issues_with_query(query)
595 issues = find_issues_with_query(query)
596 assert !issues.empty?
596 assert !issues.empty?
597 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
597 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
598 end
598 end
599
599
600 def test_operator_in
600 def test_operator_in
601 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
601 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
602 query = IssueQuery.new(:project => Project.find(1), :name => '_')
602 query = IssueQuery.new(:project => Project.find(1), :name => '_')
603 query.add_filter('due_date', 't+', ['2'])
603 query.add_filter('due_date', 't+', ['2'])
604 issues = find_issues_with_query(query)
604 issues = find_issues_with_query(query)
605 assert !issues.empty?
605 assert !issues.empty?
606 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
606 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
607 end
607 end
608
608
609 def test_operator_ago
609 def test_operator_ago
610 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
610 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
611 query = IssueQuery.new(:project => Project.find(1), :name => '_')
611 query = IssueQuery.new(:project => Project.find(1), :name => '_')
612 query.add_filter('due_date', 't-', ['3'])
612 query.add_filter('due_date', 't-', ['3'])
613 issues = find_issues_with_query(query)
613 issues = find_issues_with_query(query)
614 assert !issues.empty?
614 assert !issues.empty?
615 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
615 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
616 end
616 end
617
617
618 def test_operator_today
618 def test_operator_today
619 query = IssueQuery.new(:project => Project.find(1), :name => '_')
619 query = IssueQuery.new(:project => Project.find(1), :name => '_')
620 query.add_filter('due_date', 't', [''])
620 query.add_filter('due_date', 't', [''])
621 issues = find_issues_with_query(query)
621 issues = find_issues_with_query(query)
622 assert !issues.empty?
622 assert !issues.empty?
623 issues.each {|issue| assert_equal Date.today, issue.due_date}
623 issues.each {|issue| assert_equal Date.today, issue.due_date}
624 end
624 end
625
625
626 def test_operator_date_periods
626 def test_operator_date_periods
627 %w(t ld w lw l2w m lm y).each do |operator|
627 %w(t ld w lw l2w m lm y).each do |operator|
628 query = IssueQuery.new(:name => '_')
628 query = IssueQuery.new(:name => '_')
629 query.add_filter('due_date', operator, [''])
629 query.add_filter('due_date', operator, [''])
630 assert query.valid?
630 assert query.valid?
631 assert query.issues
631 assert query.issues
632 end
632 end
633 end
633 end
634
634
635 def test_operator_datetime_periods
635 def test_operator_datetime_periods
636 %w(t ld w lw l2w m lm y).each do |operator|
636 %w(t ld w lw l2w m lm y).each do |operator|
637 query = IssueQuery.new(:name => '_')
637 query = IssueQuery.new(:name => '_')
638 query.add_filter('created_on', operator, [''])
638 query.add_filter('created_on', operator, [''])
639 assert query.valid?
639 assert query.valid?
640 assert query.issues
640 assert query.issues
641 end
641 end
642 end
642 end
643
643
644 def test_operator_contains
644 def test_operator_contains
645 issue = Issue.generate!(:subject => 'AbCdEfG')
645 issue = Issue.generate!(:subject => 'AbCdEfG')
646
646
647 query = IssueQuery.new(:name => '_')
647 query = IssueQuery.new(:name => '_')
648 query.add_filter('subject', '~', ['cdeF'])
648 query.add_filter('subject', '~', ['cdeF'])
649 result = find_issues_with_query(query)
649 result = find_issues_with_query(query)
650 assert_include issue, result
650 assert_include issue, result
651 result.each {|issue| assert issue.subject.downcase.include?('cdef') }
651 result.each {|issue| assert issue.subject.downcase.include?('cdef') }
652 end
652 end
653
653
654 def test_operator_contains_with_utf8_string
654 def test_operator_contains_with_utf8_string
655 issue = Issue.generate!(:subject => 'Subject contains Kiểm')
655 issue = Issue.generate!(:subject => 'Subject contains Kiểm')
656
656
657 query = IssueQuery.new(:name => '_')
657 query = IssueQuery.new(:name => '_')
658 query.add_filter('subject', '~', ['Kiểm'])
658 query.add_filter('subject', '~', ['Kiểm'])
659 result = find_issues_with_query(query)
659 result = find_issues_with_query(query)
660 assert_include issue, result
660 assert_include issue, result
661 assert_equal 1, result.size
661 assert_equal 1, result.size
662 end
662 end
663
663
664 def test_operator_does_not_contain
664 def test_operator_does_not_contain
665 issue = Issue.generate!(:subject => 'AbCdEfG')
665 issue = Issue.generate!(:subject => 'AbCdEfG')
666
666
667 query = IssueQuery.new(:name => '_')
667 query = IssueQuery.new(:name => '_')
668 query.add_filter('subject', '!~', ['cdeF'])
668 query.add_filter('subject', '!~', ['cdeF'])
669 result = find_issues_with_query(query)
669 result = find_issues_with_query(query)
670 assert_not_include issue, result
670 assert_not_include issue, result
671 end
671 end
672
672
673 def test_range_for_this_week_with_week_starting_on_monday
673 def test_range_for_this_week_with_week_starting_on_monday
674 I18n.locale = :fr
674 I18n.locale = :fr
675 assert_equal '1', I18n.t(:general_first_day_of_week)
675 assert_equal '1', I18n.t(:general_first_day_of_week)
676
676
677 Date.stubs(:today).returns(Date.parse('2011-04-29'))
677 Date.stubs(:today).returns(Date.parse('2011-04-29'))
678
678
679 query = IssueQuery.new(:project => Project.find(1), :name => '_')
679 query = IssueQuery.new(:project => Project.find(1), :name => '_')
680 query.add_filter('due_date', 'w', [''])
680 query.add_filter('due_date', 'w', [''])
681 assert_match /issues\.due_date > '#{quoted_date "2011-04-24"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-05-01"} 23:59:59(\.\d+)?/,
681 assert_match /issues\.due_date > '#{quoted_date "2011-04-24"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-05-01"} 23:59:59(\.\d+)?/,
682 query.statement
682 query.statement
683 I18n.locale = :en
683 I18n.locale = :en
684 end
684 end
685
685
686 def test_range_for_this_week_with_week_starting_on_sunday
686 def test_range_for_this_week_with_week_starting_on_sunday
687 I18n.locale = :en
687 I18n.locale = :en
688 assert_equal '7', I18n.t(:general_first_day_of_week)
688 assert_equal '7', I18n.t(:general_first_day_of_week)
689
689
690 Date.stubs(:today).returns(Date.parse('2011-04-29'))
690 Date.stubs(:today).returns(Date.parse('2011-04-29'))
691
691
692 query = IssueQuery.new(:project => Project.find(1), :name => '_')
692 query = IssueQuery.new(:project => Project.find(1), :name => '_')
693 query.add_filter('due_date', 'w', [''])
693 query.add_filter('due_date', 'w', [''])
694 assert_match /issues\.due_date > '#{quoted_date "2011-04-23"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-04-30"} 23:59:59(\.\d+)?/,
694 assert_match /issues\.due_date > '#{quoted_date "2011-04-23"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-04-30"} 23:59:59(\.\d+)?/,
695 query.statement
695 query.statement
696 end
696 end
697
697
698 def test_filter_assigned_to_me
698 def test_filter_assigned_to_me
699 user = User.find(2)
699 user = User.find(2)
700 group = Group.find(10)
700 group = Group.find(10)
701 group.users << user
701 group.users << user
702 other_group = Group.find(11)
702 other_group = Group.find(11)
703 Member.create!(:project_id => 1, :principal => group, :role_ids => [1])
703 Member.create!(:project_id => 1, :principal => group, :role_ids => [1])
704 Member.create!(:project_id => 1, :principal => other_group, :role_ids => [1])
704 Member.create!(:project_id => 1, :principal => other_group, :role_ids => [1])
705 User.current = user
705 User.current = user
706
706
707 with_settings :issue_group_assignment => '1' do
707 with_settings :issue_group_assignment => '1' do
708 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
708 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
709 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
709 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
710 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => other_group)
710 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => other_group)
711
711
712 query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
712 query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
713 result = query.issues
713 result = query.issues
714 assert_equal Issue.visible.where(:assigned_to_id => ([2] + user.reload.group_ids)).sort_by(&:id), result.sort_by(&:id)
714 assert_equal Issue.visible.where(:assigned_to_id => ([2] + user.reload.group_ids)).sort_by(&:id), result.sort_by(&:id)
715
715
716 assert result.include?(i1)
716 assert result.include?(i1)
717 assert result.include?(i2)
717 assert result.include?(i2)
718 assert !result.include?(i3)
718 assert !result.include?(i3)
719 end
719 end
720 end
720 end
721
721
722 def test_user_custom_field_filtered_on_me
722 def test_user_custom_field_filtered_on_me
723 User.current = User.find(2)
723 User.current = User.find(2)
724 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
724 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
725 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
725 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
726 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
726 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
727
727
728 query = IssueQuery.new(:name => '_', :project => Project.find(1))
728 query = IssueQuery.new(:name => '_', :project => Project.find(1))
729 filter = query.available_filters["cf_#{cf.id}"]
729 filter = query.available_filters["cf_#{cf.id}"]
730 assert_not_nil filter
730 assert_not_nil filter
731 assert_include 'me', filter[:values].map{|v| v[1]}
731 assert_include 'me', filter[:values].map{|v| v[1]}
732
732
733 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
733 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
734 result = query.issues
734 result = query.issues
735 assert_equal 1, result.size
735 assert_equal 1, result.size
736 assert_equal issue1, result.first
736 assert_equal issue1, result.first
737 end
737 end
738
738
739 def test_filter_on_me_by_anonymous_user
739 def test_filter_on_me_by_anonymous_user
740 User.current = nil
740 User.current = nil
741 query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
741 query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
742 assert_equal [], query.issues
742 assert_equal [], query.issues
743 end
743 end
744
744
745 def test_filter_my_projects
745 def test_filter_my_projects
746 User.current = User.find(2)
746 User.current = User.find(2)
747 query = IssueQuery.new(:name => '_')
747 query = IssueQuery.new(:name => '_')
748 filter = query.available_filters['project_id']
748 filter = query.available_filters['project_id']
749 assert_not_nil filter
749 assert_not_nil filter
750 assert_include 'mine', filter[:values].map{|v| v[1]}
750 assert_include 'mine', filter[:values].map{|v| v[1]}
751
751
752 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
752 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
753 result = query.issues
753 result = query.issues
754 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
754 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
755 end
755 end
756
756
757 def test_filter_watched_issues
757 def test_filter_watched_issues
758 User.current = User.find(1)
758 User.current = User.find(1)
759 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
759 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
760 result = find_issues_with_query(query)
760 result = find_issues_with_query(query)
761 assert_not_nil result
761 assert_not_nil result
762 assert !result.empty?
762 assert !result.empty?
763 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
763 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
764 User.current = nil
764 User.current = nil
765 end
765 end
766
766
767 def test_filter_unwatched_issues
767 def test_filter_unwatched_issues
768 User.current = User.find(1)
768 User.current = User.find(1)
769 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
769 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
770 result = find_issues_with_query(query)
770 result = find_issues_with_query(query)
771 assert_not_nil result
771 assert_not_nil result
772 assert !result.empty?
772 assert !result.empty?
773 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
773 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
774 User.current = nil
774 User.current = nil
775 end
775 end
776
776
777 def test_filter_on_custom_field_should_ignore_projects_with_field_disabled
777 def test_filter_on_custom_field_should_ignore_projects_with_field_disabled
778 field = IssueCustomField.generate!(:trackers => Tracker.all, :project_ids => [1, 3, 4], :is_for_all => false, :is_filter => true)
778 field = IssueCustomField.generate!(:trackers => Tracker.all, :project_ids => [1, 3, 4], :is_for_all => false, :is_filter => true)
779 Issue.generate!(:project_id => 3, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
779 Issue.generate!(:project_id => 3, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
780 Issue.generate!(:project_id => 4, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
780 Issue.generate!(:project_id => 4, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
781
781
782 query = IssueQuery.new(:name => '_', :project => Project.find(1))
782 query = IssueQuery.new(:name => '_', :project => Project.find(1))
783 query.filters = {"cf_#{field.id}" => {:operator => '=', :values => ['Foo']}}
783 query.filters = {"cf_#{field.id}" => {:operator => '=', :values => ['Foo']}}
784 assert_equal 2, find_issues_with_query(query).size
784 assert_equal 2, find_issues_with_query(query).size
785
785
786 field.project_ids = [1, 3] # Disable the field for project 4
786 field.project_ids = [1, 3] # Disable the field for project 4
787 field.save!
787 field.save!
788 assert_equal 1, find_issues_with_query(query).size
788 assert_equal 1, find_issues_with_query(query).size
789 end
789 end
790
790
791 def test_filter_on_custom_field_should_ignore_trackers_with_field_disabled
791 def test_filter_on_custom_field_should_ignore_trackers_with_field_disabled
792 field = IssueCustomField.generate!(:tracker_ids => [1, 2], :is_for_all => true, :is_filter => true)
792 field = IssueCustomField.generate!(:tracker_ids => [1, 2], :is_for_all => true, :is_filter => true)
793 Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => 'Foo'})
793 Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => 'Foo'})
794 Issue.generate!(:project_id => 1, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
794 Issue.generate!(:project_id => 1, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
795
795
796 query = IssueQuery.new(:name => '_', :project => Project.find(1))
796 query = IssueQuery.new(:name => '_', :project => Project.find(1))
797 query.filters = {"cf_#{field.id}" => {:operator => '=', :values => ['Foo']}}
797 query.filters = {"cf_#{field.id}" => {:operator => '=', :values => ['Foo']}}
798 assert_equal 2, find_issues_with_query(query).size
798 assert_equal 2, find_issues_with_query(query).size
799
799
800 field.tracker_ids = [1] # Disable the field for tracker 2
800 field.tracker_ids = [1] # Disable the field for tracker 2
801 field.save!
801 field.save!
802 assert_equal 1, find_issues_with_query(query).size
802 assert_equal 1, find_issues_with_query(query).size
803 end
803 end
804
804
805 def test_filter_on_project_custom_field
805 def test_filter_on_project_custom_field
806 field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
806 field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
807 CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
807 CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
808 CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo')
808 CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo')
809
809
810 query = IssueQuery.new(:name => '_')
810 query = IssueQuery.new(:name => '_')
811 filter_name = "project.cf_#{field.id}"
811 filter_name = "project.cf_#{field.id}"
812 assert_include filter_name, query.available_filters.keys
812 assert_include filter_name, query.available_filters.keys
813 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
813 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
814 assert_equal [3, 5], find_issues_with_query(query).map(&:project_id).uniq.sort
814 assert_equal [3, 5], find_issues_with_query(query).map(&:project_id).uniq.sort
815 end
815 end
816
816
817 def test_filter_on_author_custom_field
817 def test_filter_on_author_custom_field
818 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
818 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
819 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
819 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
820
820
821 query = IssueQuery.new(:name => '_')
821 query = IssueQuery.new(:name => '_')
822 filter_name = "author.cf_#{field.id}"
822 filter_name = "author.cf_#{field.id}"
823 assert_include filter_name, query.available_filters.keys
823 assert_include filter_name, query.available_filters.keys
824 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
824 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
825 assert_equal [3], find_issues_with_query(query).map(&:author_id).uniq.sort
825 assert_equal [3], find_issues_with_query(query).map(&:author_id).uniq.sort
826 end
826 end
827
827
828 def test_filter_on_assigned_to_custom_field
828 def test_filter_on_assigned_to_custom_field
829 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
829 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
830 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
830 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
831
831
832 query = IssueQuery.new(:name => '_')
832 query = IssueQuery.new(:name => '_')
833 filter_name = "assigned_to.cf_#{field.id}"
833 filter_name = "assigned_to.cf_#{field.id}"
834 assert_include filter_name, query.available_filters.keys
834 assert_include filter_name, query.available_filters.keys
835 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
835 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
836 assert_equal [3], find_issues_with_query(query).map(&:assigned_to_id).uniq.sort
836 assert_equal [3], find_issues_with_query(query).map(&:assigned_to_id).uniq.sort
837 end
837 end
838
838
839 def test_filter_on_fixed_version_custom_field
839 def test_filter_on_fixed_version_custom_field
840 field = VersionCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
840 field = VersionCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
841 CustomValue.create!(:custom_field => field, :customized => Version.find(2), :value => 'Foo')
841 CustomValue.create!(:custom_field => field, :customized => Version.find(2), :value => 'Foo')
842
842
843 query = IssueQuery.new(:name => '_')
843 query = IssueQuery.new(:name => '_')
844 filter_name = "fixed_version.cf_#{field.id}"
844 filter_name = "fixed_version.cf_#{field.id}"
845 assert_include filter_name, query.available_filters.keys
845 assert_include filter_name, query.available_filters.keys
846 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
846 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
847 assert_equal [2], find_issues_with_query(query).map(&:fixed_version_id).uniq.sort
847 assert_equal [2], find_issues_with_query(query).map(&:fixed_version_id).uniq.sort
848 end
848 end
849
849
850 def test_filter_on_fixed_version_due_date
850 def test_filter_on_fixed_version_due_date
851 query = IssueQuery.new(:name => '_')
851 query = IssueQuery.new(:name => '_')
852 filter_name = "fixed_version.due_date"
852 filter_name = "fixed_version.due_date"
853 assert_include filter_name, query.available_filters.keys
853 assert_include filter_name, query.available_filters.keys
854 query.filters = {filter_name => {:operator => '=', :values => [20.day.from_now.to_date.to_s(:db)]}}
854 query.filters = {filter_name => {:operator => '=', :values => [20.day.from_now.to_date.to_s(:db)]}}
855 issues = find_issues_with_query(query)
855 issues = find_issues_with_query(query)
856 assert_equal [2], issues.map(&:fixed_version_id).uniq.sort
856 assert_equal [2], issues.map(&:fixed_version_id).uniq.sort
857 assert_equal [2, 12], issues.map(&:id).sort
857 assert_equal [2, 12], issues.map(&:id).sort
858
858
859 query = IssueQuery.new(:name => '_')
859 query = IssueQuery.new(:name => '_')
860 query.filters = {filter_name => {:operator => '>=', :values => [21.day.from_now.to_date.to_s(:db)]}}
860 query.filters = {filter_name => {:operator => '>=', :values => [21.day.from_now.to_date.to_s(:db)]}}
861 assert_equal 0, find_issues_with_query(query).size
861 assert_equal 0, find_issues_with_query(query).size
862 end
862 end
863
863
864 def test_filter_on_fixed_version_status
864 def test_filter_on_fixed_version_status
865 query = IssueQuery.new(:name => '_')
865 query = IssueQuery.new(:name => '_')
866 filter_name = "fixed_version.status"
866 filter_name = "fixed_version.status"
867 assert_include filter_name, query.available_filters.keys
867 assert_include filter_name, query.available_filters.keys
868 query.filters = {filter_name => {:operator => '=', :values => ['closed']}}
868 query.filters = {filter_name => {:operator => '=', :values => ['closed']}}
869 issues = find_issues_with_query(query)
869 issues = find_issues_with_query(query)
870
870
871 assert_equal [1], issues.map(&:fixed_version_id).sort
871 assert_equal [1], issues.map(&:fixed_version_id).sort
872 assert_equal [11], issues.map(&:id).sort
872 assert_equal [11], issues.map(&:id).sort
873
873
874 # "is not" operator should include issues without target version
874 # "is not" operator should include issues without target version
875 query = IssueQuery.new(:name => '_')
875 query = IssueQuery.new(:name => '_')
876 query.filters = {filter_name => {:operator => '!', :values => ['open', 'closed', 'locked']}, "project_id" => {:operator => '=', :values => [1]}}
876 query.filters = {filter_name => {:operator => '!', :values => ['open', 'closed', 'locked']}, "project_id" => {:operator => '=', :values => [1]}}
877 assert_equal [1, 3, 7, 8], find_issues_with_query(query).map(&:id).uniq.sort
877 assert_equal [1, 3, 7, 8], find_issues_with_query(query).map(&:id).uniq.sort
878 end
878 end
879
879
880 def test_filter_on_version_custom_field
880 def test_filter_on_version_custom_field
881 field = IssueCustomField.generate!(:field_format => 'version', :is_filter => true)
881 field = IssueCustomField.generate!(:field_format => 'version', :is_filter => true)
882 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => '2'})
882 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => '2'})
883
883
884 query = IssueQuery.new(:name => '_')
884 query = IssueQuery.new(:name => '_')
885 filter_name = "cf_#{field.id}"
885 filter_name = "cf_#{field.id}"
886 assert_include filter_name, query.available_filters.keys
886 assert_include filter_name, query.available_filters.keys
887
887
888 query.filters = {filter_name => {:operator => '=', :values => ['2']}}
888 query.filters = {filter_name => {:operator => '=', :values => ['2']}}
889 issues = find_issues_with_query(query)
889 issues = find_issues_with_query(query)
890 assert_equal [issue.id], issues.map(&:id).sort
890 assert_equal [issue.id], issues.map(&:id).sort
891 end
891 end
892
892
893 def test_filter_on_attribute_of_version_custom_field
893 def test_filter_on_attribute_of_version_custom_field
894 field = IssueCustomField.generate!(:field_format => 'version', :is_filter => true)
894 field = IssueCustomField.generate!(:field_format => 'version', :is_filter => true)
895 version = Version.generate!(:effective_date => '2017-01-14')
895 version = Version.generate!(:effective_date => '2017-01-14')
896 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => version.id.to_s})
896 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => version.id.to_s})
897
897
898 query = IssueQuery.new(:name => '_')
898 query = IssueQuery.new(:name => '_')
899 filter_name = "cf_#{field.id}.due_date"
899 filter_name = "cf_#{field.id}.due_date"
900 assert_include filter_name, query.available_filters.keys
900 assert_include filter_name, query.available_filters.keys
901
901
902 query.filters = {filter_name => {:operator => '=', :values => ['2017-01-14']}}
902 query.filters = {filter_name => {:operator => '=', :values => ['2017-01-14']}}
903 issues = find_issues_with_query(query)
903 issues = find_issues_with_query(query)
904 assert_equal [issue.id], issues.map(&:id).sort
904 assert_equal [issue.id], issues.map(&:id).sort
905 end
905 end
906
906
907 def test_filter_on_custom_field_of_version_custom_field
907 def test_filter_on_custom_field_of_version_custom_field
908 field = IssueCustomField.generate!(:field_format => 'version', :is_filter => true)
908 field = IssueCustomField.generate!(:field_format => 'version', :is_filter => true)
909 attr = VersionCustomField.generate!(:field_format => 'string', :is_filter => true)
909 attr = VersionCustomField.generate!(:field_format => 'string', :is_filter => true)
910
910
911 version = Version.generate!(:custom_field_values => {attr.id.to_s => 'ABC'})
911 version = Version.generate!(:custom_field_values => {attr.id.to_s => 'ABC'})
912 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => version.id.to_s})
912 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => version.id.to_s})
913
913
914 query = IssueQuery.new(:name => '_')
914 query = IssueQuery.new(:name => '_')
915 filter_name = "cf_#{field.id}.cf_#{attr.id}"
915 filter_name = "cf_#{field.id}.cf_#{attr.id}"
916 assert_include filter_name, query.available_filters.keys
916 assert_include filter_name, query.available_filters.keys
917
917
918 query.filters = {filter_name => {:operator => '=', :values => ['ABC']}}
918 query.filters = {filter_name => {:operator => '=', :values => ['ABC']}}
919 issues = find_issues_with_query(query)
919 issues = find_issues_with_query(query)
920 assert_equal [issue.id], issues.map(&:id).sort
920 assert_equal [issue.id], issues.map(&:id).sort
921 end
921 end
922
922
923 def test_filter_on_relations_with_a_specific_issue
923 def test_filter_on_relations_with_a_specific_issue
924 IssueRelation.delete_all
924 IssueRelation.delete_all
925 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
925 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
926 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
926 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
927
927
928 query = IssueQuery.new(:name => '_')
928 query = IssueQuery.new(:name => '_')
929 query.filters = {"relates" => {:operator => '=', :values => ['1']}}
929 query.filters = {"relates" => {:operator => '=', :values => ['1']}}
930 assert_equal [2, 3], find_issues_with_query(query).map(&:id).sort
930 assert_equal [2, 3], find_issues_with_query(query).map(&:id).sort
931
931
932 query = IssueQuery.new(:name => '_')
932 query = IssueQuery.new(:name => '_')
933 query.filters = {"relates" => {:operator => '=', :values => ['2']}}
933 query.filters = {"relates" => {:operator => '=', :values => ['2']}}
934 assert_equal [1], find_issues_with_query(query).map(&:id).sort
934 assert_equal [1], find_issues_with_query(query).map(&:id).sort
935 end
935 end
936
936
937 def test_filter_on_relations_with_any_issues_in_a_project
937 def test_filter_on_relations_with_any_issues_in_a_project
938 IssueRelation.delete_all
938 IssueRelation.delete_all
939 with_settings :cross_project_issue_relations => '1' do
939 with_settings :cross_project_issue_relations => '1' do
940 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
940 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
941 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(2).issues.first)
941 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(2).issues.first)
942 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
942 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
943 end
943 end
944
944
945 query = IssueQuery.new(:name => '_')
945 query = IssueQuery.new(:name => '_')
946 query.filters = {"relates" => {:operator => '=p', :values => ['2']}}
946 query.filters = {"relates" => {:operator => '=p', :values => ['2']}}
947 assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort
947 assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort
948
948
949 query = IssueQuery.new(:name => '_')
949 query = IssueQuery.new(:name => '_')
950 query.filters = {"relates" => {:operator => '=p', :values => ['3']}}
950 query.filters = {"relates" => {:operator => '=p', :values => ['3']}}
951 assert_equal [1], find_issues_with_query(query).map(&:id).sort
951 assert_equal [1], find_issues_with_query(query).map(&:id).sort
952
952
953 query = IssueQuery.new(:name => '_')
953 query = IssueQuery.new(:name => '_')
954 query.filters = {"relates" => {:operator => '=p', :values => ['4']}}
954 query.filters = {"relates" => {:operator => '=p', :values => ['4']}}
955 assert_equal [], find_issues_with_query(query).map(&:id).sort
955 assert_equal [], find_issues_with_query(query).map(&:id).sort
956 end
956 end
957
957
958 def test_filter_on_relations_with_any_issues_not_in_a_project
958 def test_filter_on_relations_with_any_issues_not_in_a_project
959 IssueRelation.delete_all
959 IssueRelation.delete_all
960 with_settings :cross_project_issue_relations => '1' do
960 with_settings :cross_project_issue_relations => '1' do
961 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
961 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
962 #IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(1).issues.first)
962 #IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(1).issues.first)
963 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
963 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
964 end
964 end
965
965
966 query = IssueQuery.new(:name => '_')
966 query = IssueQuery.new(:name => '_')
967 query.filters = {"relates" => {:operator => '=!p', :values => ['1']}}
967 query.filters = {"relates" => {:operator => '=!p', :values => ['1']}}
968 assert_equal [1], find_issues_with_query(query).map(&:id).sort
968 assert_equal [1], find_issues_with_query(query).map(&:id).sort
969 end
969 end
970
970
971 def test_filter_on_relations_with_no_issues_in_a_project
971 def test_filter_on_relations_with_no_issues_in_a_project
972 IssueRelation.delete_all
972 IssueRelation.delete_all
973 with_settings :cross_project_issue_relations => '1' do
973 with_settings :cross_project_issue_relations => '1' do
974 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
974 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
975 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(3).issues.first)
975 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(3).issues.first)
976 IssueRelation.create!(:relation_type => "relates", :issue_to => Project.find(2).issues.first, :issue_from => Issue.find(3))
976 IssueRelation.create!(:relation_type => "relates", :issue_to => Project.find(2).issues.first, :issue_from => Issue.find(3))
977 end
977 end
978
978
979 query = IssueQuery.new(:name => '_')
979 query = IssueQuery.new(:name => '_')
980 query.filters = {"relates" => {:operator => '!p', :values => ['2']}}
980 query.filters = {"relates" => {:operator => '!p', :values => ['2']}}
981 ids = find_issues_with_query(query).map(&:id).sort
981 ids = find_issues_with_query(query).map(&:id).sort
982 assert_include 2, ids
982 assert_include 2, ids
983 assert_not_include 1, ids
983 assert_not_include 1, ids
984 assert_not_include 3, ids
984 assert_not_include 3, ids
985 end
985 end
986
986
987 def test_filter_on_relations_with_any_open_issues
987 def test_filter_on_relations_with_any_open_issues
988 IssueRelation.delete_all
988 IssueRelation.delete_all
989 # Issue 1 is blocked by 8, which is closed
989 # Issue 1 is blocked by 8, which is closed
990 IssueRelation.create!(:relation_type => "blocked", :issue_from => Issue.find(1), :issue_to => Issue.find(8))
990 IssueRelation.create!(:relation_type => "blocked", :issue_from => Issue.find(1), :issue_to => Issue.find(8))
991 # Issue 2 is blocked by 3, which is open
991 # Issue 2 is blocked by 3, which is open
992 IssueRelation.create!(:relation_type => "blocked", :issue_from => Issue.find(2), :issue_to => Issue.find(3))
992 IssueRelation.create!(:relation_type => "blocked", :issue_from => Issue.find(2), :issue_to => Issue.find(3))
993
993
994 query = IssueQuery.new(:name => '_')
994 query = IssueQuery.new(:name => '_')
995 query.filters = {"blocked" => {:operator => "*o", :values => ['']}}
995 query.filters = {"blocked" => {:operator => "*o", :values => ['']}}
996 ids = find_issues_with_query(query).map(&:id)
996 ids = find_issues_with_query(query).map(&:id)
997 assert_equal [], ids & [1]
997 assert_equal [], ids & [1]
998 assert_include 2, ids
998 assert_include 2, ids
999 end
999 end
1000
1000
1001 def test_filter_on_relations_with_no_open_issues
1001 def test_filter_on_relations_with_no_open_issues
1002 IssueRelation.delete_all
1002 IssueRelation.delete_all
1003 # Issue 1 is blocked by 8, which is closed
1003 # Issue 1 is blocked by 8, which is closed
1004 IssueRelation.create!(:relation_type => "blocked", :issue_from => Issue.find(1), :issue_to => Issue.find(8))
1004 IssueRelation.create!(:relation_type => "blocked", :issue_from => Issue.find(1), :issue_to => Issue.find(8))
1005 # Issue 2 is blocked by 3, which is open
1005 # Issue 2 is blocked by 3, which is open
1006 IssueRelation.create!(:relation_type => "blocked", :issue_from => Issue.find(2), :issue_to => Issue.find(3))
1006 IssueRelation.create!(:relation_type => "blocked", :issue_from => Issue.find(2), :issue_to => Issue.find(3))
1007
1007
1008 query = IssueQuery.new(:name => '_')
1008 query = IssueQuery.new(:name => '_')
1009 query.filters = {"blocked" => {:operator => "!o", :values => ['']}}
1009 query.filters = {"blocked" => {:operator => "!o", :values => ['']}}
1010 ids = find_issues_with_query(query).map(&:id)
1010 ids = find_issues_with_query(query).map(&:id)
1011 assert_equal [], ids & [2]
1011 assert_equal [], ids & [2]
1012 assert_include 1, ids
1012 assert_include 1, ids
1013 end
1013 end
1014
1014
1015 def test_filter_on_relations_with_no_issues
1015 def test_filter_on_relations_with_no_issues
1016 IssueRelation.delete_all
1016 IssueRelation.delete_all
1017 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
1017 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
1018 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
1018 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
1019
1019
1020 query = IssueQuery.new(:name => '_')
1020 query = IssueQuery.new(:name => '_')
1021 query.filters = {"relates" => {:operator => '!*', :values => ['']}}
1021 query.filters = {"relates" => {:operator => '!*', :values => ['']}}
1022 ids = find_issues_with_query(query).map(&:id)
1022 ids = find_issues_with_query(query).map(&:id)
1023 assert_equal [], ids & [1, 2, 3]
1023 assert_equal [], ids & [1, 2, 3]
1024 assert_include 4, ids
1024 assert_include 4, ids
1025 end
1025 end
1026
1026
1027 def test_filter_on_relations_with_any_issues
1027 def test_filter_on_relations_with_any_issues
1028 IssueRelation.delete_all
1028 IssueRelation.delete_all
1029 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
1029 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
1030 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
1030 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
1031
1031
1032 query = IssueQuery.new(:name => '_')
1032 query = IssueQuery.new(:name => '_')
1033 query.filters = {"relates" => {:operator => '*', :values => ['']}}
1033 query.filters = {"relates" => {:operator => '*', :values => ['']}}
1034 assert_equal [1, 2, 3], find_issues_with_query(query).map(&:id).sort
1034 assert_equal [1, 2, 3], find_issues_with_query(query).map(&:id).sort
1035 end
1035 end
1036
1036
1037 def test_filter_on_relations_should_not_ignore_other_filter
1037 def test_filter_on_relations_should_not_ignore_other_filter
1038 issue = Issue.generate!
1038 issue = Issue.generate!
1039 issue1 = Issue.generate!(:status_id => 1)
1039 issue1 = Issue.generate!(:status_id => 1)
1040 issue2 = Issue.generate!(:status_id => 2)
1040 issue2 = Issue.generate!(:status_id => 2)
1041 IssueRelation.create!(:relation_type => "relates", :issue_from => issue, :issue_to => issue1)
1041 IssueRelation.create!(:relation_type => "relates", :issue_from => issue, :issue_to => issue1)
1042 IssueRelation.create!(:relation_type => "relates", :issue_from => issue, :issue_to => issue2)
1042 IssueRelation.create!(:relation_type => "relates", :issue_from => issue, :issue_to => issue2)
1043
1043
1044 query = IssueQuery.new(:name => '_')
1044 query = IssueQuery.new(:name => '_')
1045 query.filters = {
1045 query.filters = {
1046 "status_id" => {:operator => '=', :values => ['1']},
1046 "status_id" => {:operator => '=', :values => ['1']},
1047 "relates" => {:operator => '=', :values => [issue.id.to_s]}
1047 "relates" => {:operator => '=', :values => [issue.id.to_s]}
1048 }
1048 }
1049 assert_equal [issue1], find_issues_with_query(query)
1049 assert_equal [issue1], find_issues_with_query(query)
1050 end
1050 end
1051
1051
1052 def test_filter_on_parent
1052 def test_filter_on_parent
1053 Issue.delete_all
1053 Issue.delete_all
1054 parent = Issue.generate_with_descendants!
1054 parent = Issue.generate_with_descendants!
1055
1055
1056
1056
1057 query = IssueQuery.new(:name => '_')
1057 query = IssueQuery.new(:name => '_')
1058 query.filters = {"parent_id" => {:operator => '=', :values => [parent.id.to_s]}}
1058 query.filters = {"parent_id" => {:operator => '=', :values => [parent.id.to_s]}}
1059 assert_equal parent.children.map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1059 assert_equal parent.children.map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1060
1060
1061 query.filters = {"parent_id" => {:operator => '~', :values => [parent.id.to_s]}}
1061 query.filters = {"parent_id" => {:operator => '~', :values => [parent.id.to_s]}}
1062 assert_equal parent.descendants.map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1062 assert_equal parent.descendants.map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1063
1063
1064 query.filters = {"parent_id" => {:operator => '*', :values => ['']}}
1064 query.filters = {"parent_id" => {:operator => '*', :values => ['']}}
1065 assert_equal parent.descendants.map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1065 assert_equal parent.descendants.map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1066
1066
1067 query.filters = {"parent_id" => {:operator => '!*', :values => ['']}}
1067 query.filters = {"parent_id" => {:operator => '!*', :values => ['']}}
1068 assert_equal [parent.id], find_issues_with_query(query).map(&:id).sort
1068 assert_equal [parent.id], find_issues_with_query(query).map(&:id).sort
1069 end
1069 end
1070
1070
1071 def test_filter_on_invalid_parent_should_return_no_results
1071 def test_filter_on_invalid_parent_should_return_no_results
1072 query = IssueQuery.new(:name => '_')
1072 query = IssueQuery.new(:name => '_')
1073 query.filters = {"parent_id" => {:operator => '=', :values => '99999999999'}}
1073 query.filters = {"parent_id" => {:operator => '=', :values => '99999999999'}}
1074 assert_equal [], find_issues_with_query(query).map(&:id).sort
1074 assert_equal [], find_issues_with_query(query).map(&:id).sort
1075
1075
1076 query.filters = {"parent_id" => {:operator => '~', :values => '99999999999'}}
1076 query.filters = {"parent_id" => {:operator => '~', :values => '99999999999'}}
1077 assert_equal [], find_issues_with_query(query)
1077 assert_equal [], find_issues_with_query(query)
1078 end
1078 end
1079
1079
1080 def test_filter_on_child
1080 def test_filter_on_child
1081 Issue.delete_all
1081 Issue.delete_all
1082 parent = Issue.generate_with_descendants!
1082 parent = Issue.generate_with_descendants!
1083 child, leaf = parent.children.sort_by(&:id)
1083 child, leaf = parent.children.sort_by(&:id)
1084 grandchild = child.children.first
1084 grandchild = child.children.first
1085
1085
1086
1086
1087 query = IssueQuery.new(:name => '_')
1087 query = IssueQuery.new(:name => '_')
1088 query.filters = {"child_id" => {:operator => '=', :values => [grandchild.id.to_s]}}
1088 query.filters = {"child_id" => {:operator => '=', :values => [grandchild.id.to_s]}}
1089 assert_equal [child.id], find_issues_with_query(query).map(&:id).sort
1089 assert_equal [child.id], find_issues_with_query(query).map(&:id).sort
1090
1090
1091 query.filters = {"child_id" => {:operator => '~', :values => [grandchild.id.to_s]}}
1091 query.filters = {"child_id" => {:operator => '~', :values => [grandchild.id.to_s]}}
1092 assert_equal [parent, child].map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1092 assert_equal [parent, child].map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1093
1093
1094 query.filters = {"child_id" => {:operator => '*', :values => ['']}}
1094 query.filters = {"child_id" => {:operator => '*', :values => ['']}}
1095 assert_equal [parent, child].map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1095 assert_equal [parent, child].map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1096
1096
1097 query.filters = {"child_id" => {:operator => '!*', :values => ['']}}
1097 query.filters = {"child_id" => {:operator => '!*', :values => ['']}}
1098 assert_equal [grandchild, leaf].map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1098 assert_equal [grandchild, leaf].map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1099 end
1099 end
1100
1100
1101 def test_filter_on_invalid_child_should_return_no_results
1101 def test_filter_on_invalid_child_should_return_no_results
1102 query = IssueQuery.new(:name => '_')
1102 query = IssueQuery.new(:name => '_')
1103 query.filters = {"child_id" => {:operator => '=', :values => '99999999999'}}
1103 query.filters = {"child_id" => {:operator => '=', :values => '99999999999'}}
1104 assert_equal [], find_issues_with_query(query)
1104 assert_equal [], find_issues_with_query(query)
1105
1105
1106 query.filters = {"child_id" => {:operator => '~', :values => '99999999999'}}
1106 query.filters = {"child_id" => {:operator => '~', :values => '99999999999'}}
1107 assert_equal [].map(&:id).sort, find_issues_with_query(query)
1107 assert_equal [].map(&:id).sort, find_issues_with_query(query)
1108 end
1108 end
1109
1109
1110 def test_statement_should_be_nil_with_no_filters
1110 def test_statement_should_be_nil_with_no_filters
1111 q = IssueQuery.new(:name => '_')
1111 q = IssueQuery.new(:name => '_')
1112 q.filters = {}
1112 q.filters = {}
1113
1113
1114 assert q.valid?
1114 assert q.valid?
1115 assert_nil q.statement
1115 assert_nil q.statement
1116 end
1116 end
1117
1117
1118 def test_available_filters_as_json_should_include_missing_assigned_to_id_values
1118 def test_available_filters_as_json_should_include_missing_assigned_to_id_values
1119 user = User.generate!
1119 user = User.generate!
1120 with_current_user User.find(1) do
1120 with_current_user User.find(1) do
1121 q = IssueQuery.new
1121 q = IssueQuery.new
1122 q.filters = {"assigned_to_id" => {:operator => '=', :values => user.id.to_s}}
1122 q.filters = {"assigned_to_id" => {:operator => '=', :values => user.id.to_s}}
1123
1123
1124 filters = q.available_filters_as_json
1124 filters = q.available_filters_as_json
1125 assert_include [user.name, user.id.to_s], filters['assigned_to_id']['values']
1125 assert_include [user.name, user.id.to_s], filters['assigned_to_id']['values']
1126 end
1126 end
1127 end
1127 end
1128
1128
1129 def test_available_filters_as_json_should_include_missing_author_id_values
1129 def test_available_filters_as_json_should_include_missing_author_id_values
1130 user = User.generate!
1130 user = User.generate!
1131 with_current_user User.find(1) do
1131 with_current_user User.find(1) do
1132 q = IssueQuery.new
1132 q = IssueQuery.new
1133 q.filters = {"author_id" => {:operator => '=', :values => user.id.to_s}}
1133 q.filters = {"author_id" => {:operator => '=', :values => user.id.to_s}}
1134
1134
1135 filters = q.available_filters_as_json
1135 filters = q.available_filters_as_json
1136 assert_include [user.name, user.id.to_s], filters['author_id']['values']
1136 assert_include [user.name, user.id.to_s], filters['author_id']['values']
1137 end
1137 end
1138 end
1138 end
1139
1139
1140 def test_default_columns
1140 def test_default_columns
1141 q = IssueQuery.new
1141 q = IssueQuery.new
1142 assert q.columns.any?
1142 assert q.columns.any?
1143 assert q.inline_columns.any?
1143 assert q.inline_columns.any?
1144 assert q.block_columns.empty?
1144 assert q.block_columns.empty?
1145 end
1145 end
1146
1146
1147 def test_set_column_names
1147 def test_set_column_names
1148 q = IssueQuery.new
1148 q = IssueQuery.new
1149 q.column_names = ['tracker', :subject, '', 'unknonw_column']
1149 q.column_names = ['tracker', :subject, '', 'unknonw_column']
1150 assert_equal [:id, :tracker, :subject], q.columns.collect {|c| c.name}
1150 assert_equal [:id, :tracker, :subject], q.columns.collect {|c| c.name}
1151 end
1151 end
1152
1152
1153 def test_has_column_should_accept_a_column_name
1153 def test_has_column_should_accept_a_column_name
1154 q = IssueQuery.new
1154 q = IssueQuery.new
1155 q.column_names = ['tracker', :subject]
1155 q.column_names = ['tracker', :subject]
1156 assert q.has_column?(:tracker)
1156 assert q.has_column?(:tracker)
1157 assert !q.has_column?(:category)
1157 assert !q.has_column?(:category)
1158 end
1158 end
1159
1159
1160 def test_has_column_should_accept_a_column
1160 def test_has_column_should_accept_a_column
1161 q = IssueQuery.new
1161 q = IssueQuery.new
1162 q.column_names = ['tracker', :subject]
1162 q.column_names = ['tracker', :subject]
1163
1163
1164 tracker_column = q.available_columns.detect {|c| c.name==:tracker}
1164 tracker_column = q.available_columns.detect {|c| c.name==:tracker}
1165 assert_kind_of QueryColumn, tracker_column
1165 assert_kind_of QueryColumn, tracker_column
1166 category_column = q.available_columns.detect {|c| c.name==:category}
1166 category_column = q.available_columns.detect {|c| c.name==:category}
1167 assert_kind_of QueryColumn, category_column
1167 assert_kind_of QueryColumn, category_column
1168
1168
1169 assert q.has_column?(tracker_column)
1169 assert q.has_column?(tracker_column)
1170 assert !q.has_column?(category_column)
1170 assert !q.has_column?(category_column)
1171 end
1171 end
1172
1172
1173 def test_has_column_should_return_true_for_default_column
1174 with_settings :issue_list_default_columns => %w(tracker subject) do
1175 q = IssueQuery.new
1176 assert q.has_column?(:tracker)
1177 assert !q.has_column?(:category)
1178 end
1179 end
1180
1173 def test_inline_and_block_columns
1181 def test_inline_and_block_columns
1174 q = IssueQuery.new
1182 q = IssueQuery.new
1175 q.column_names = ['subject', 'description', 'tracker']
1183 q.column_names = ['subject', 'description', 'tracker']
1176
1184
1177 assert_equal [:id, :subject, :tracker], q.inline_columns.map(&:name)
1185 assert_equal [:id, :subject, :tracker], q.inline_columns.map(&:name)
1178 assert_equal [:description], q.block_columns.map(&:name)
1186 assert_equal [:description], q.block_columns.map(&:name)
1179 end
1187 end
1180
1188
1181 def test_custom_field_columns_should_be_inline
1189 def test_custom_field_columns_should_be_inline
1182 q = IssueQuery.new
1190 q = IssueQuery.new
1183 columns = q.available_columns.select {|column| column.is_a? QueryCustomFieldColumn}
1191 columns = q.available_columns.select {|column| column.is_a? QueryCustomFieldColumn}
1184 assert columns.any?
1192 assert columns.any?
1185 assert_nil columns.detect {|column| !column.inline?}
1193 assert_nil columns.detect {|column| !column.inline?}
1186 end
1194 end
1187
1195
1188 def test_query_should_preload_spent_hours
1196 def test_query_should_preload_spent_hours
1189 q = IssueQuery.new(:name => '_', :column_names => [:subject, :spent_hours])
1197 q = IssueQuery.new(:name => '_', :column_names => [:subject, :spent_hours])
1190 assert q.has_column?(:spent_hours)
1198 assert q.has_column?(:spent_hours)
1191 issues = q.issues
1199 issues = q.issues
1192 assert_not_nil issues.first.instance_variable_get("@spent_hours")
1200 assert_not_nil issues.first.instance_variable_get("@spent_hours")
1193 end
1201 end
1194
1202
1195 def test_groupable_columns_should_include_custom_fields
1203 def test_groupable_columns_should_include_custom_fields
1196 q = IssueQuery.new
1204 q = IssueQuery.new
1197 column = q.groupable_columns.detect {|c| c.name == :cf_1}
1205 column = q.groupable_columns.detect {|c| c.name == :cf_1}
1198 assert_not_nil column
1206 assert_not_nil column
1199 assert_kind_of QueryCustomFieldColumn, column
1207 assert_kind_of QueryCustomFieldColumn, column
1200 end
1208 end
1201
1209
1202 def test_groupable_columns_should_not_include_multi_custom_fields
1210 def test_groupable_columns_should_not_include_multi_custom_fields
1203 field = CustomField.find(1)
1211 field = CustomField.find(1)
1204 field.update_attribute :multiple, true
1212 field.update_attribute :multiple, true
1205
1213
1206 q = IssueQuery.new
1214 q = IssueQuery.new
1207 column = q.groupable_columns.detect {|c| c.name == :cf_1}
1215 column = q.groupable_columns.detect {|c| c.name == :cf_1}
1208 assert_nil column
1216 assert_nil column
1209 end
1217 end
1210
1218
1211 def test_groupable_columns_should_include_user_custom_fields
1219 def test_groupable_columns_should_include_user_custom_fields
1212 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'user')
1220 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'user')
1213
1221
1214 q = IssueQuery.new
1222 q = IssueQuery.new
1215 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
1223 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
1216 end
1224 end
1217
1225
1218 def test_groupable_columns_should_include_version_custom_fields
1226 def test_groupable_columns_should_include_version_custom_fields
1219 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'version')
1227 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'version')
1220
1228
1221 q = IssueQuery.new
1229 q = IssueQuery.new
1222 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
1230 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
1223 end
1231 end
1224
1232
1225 def test_grouped_with_valid_column
1233 def test_grouped_with_valid_column
1226 q = IssueQuery.new(:group_by => 'status')
1234 q = IssueQuery.new(:group_by => 'status')
1227 assert q.grouped?
1235 assert q.grouped?
1228 assert_not_nil q.group_by_column
1236 assert_not_nil q.group_by_column
1229 assert_equal :status, q.group_by_column.name
1237 assert_equal :status, q.group_by_column.name
1230 assert_not_nil q.group_by_statement
1238 assert_not_nil q.group_by_statement
1231 assert_equal 'status', q.group_by_statement
1239 assert_equal 'status', q.group_by_statement
1232 end
1240 end
1233
1241
1234 def test_grouped_with_invalid_column
1242 def test_grouped_with_invalid_column
1235 q = IssueQuery.new(:group_by => 'foo')
1243 q = IssueQuery.new(:group_by => 'foo')
1236 assert !q.grouped?
1244 assert !q.grouped?
1237 assert_nil q.group_by_column
1245 assert_nil q.group_by_column
1238 assert_nil q.group_by_statement
1246 assert_nil q.group_by_statement
1239 end
1247 end
1240
1248
1241 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
1249 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
1242 with_settings :user_format => 'lastname_comma_firstname' do
1250 with_settings :user_format => 'lastname_comma_firstname' do
1243 q = IssueQuery.new
1251 q = IssueQuery.new
1244 assert q.sortable_columns.has_key?('assigned_to')
1252 assert q.sortable_columns.has_key?('assigned_to')
1245 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
1253 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
1246 end
1254 end
1247 end
1255 end
1248
1256
1249 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
1257 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
1250 with_settings :user_format => 'lastname_comma_firstname' do
1258 with_settings :user_format => 'lastname_comma_firstname' do
1251 q = IssueQuery.new
1259 q = IssueQuery.new
1252 assert q.sortable_columns.has_key?('author')
1260 assert q.sortable_columns.has_key?('author')
1253 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
1261 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
1254 end
1262 end
1255 end
1263 end
1256
1264
1257 def test_sortable_columns_should_include_custom_field
1265 def test_sortable_columns_should_include_custom_field
1258 q = IssueQuery.new
1266 q = IssueQuery.new
1259 assert q.sortable_columns['cf_1']
1267 assert q.sortable_columns['cf_1']
1260 end
1268 end
1261
1269
1262 def test_sortable_columns_should_not_include_multi_custom_field
1270 def test_sortable_columns_should_not_include_multi_custom_field
1263 field = CustomField.find(1)
1271 field = CustomField.find(1)
1264 field.update_attribute :multiple, true
1272 field.update_attribute :multiple, true
1265
1273
1266 q = IssueQuery.new
1274 q = IssueQuery.new
1267 assert !q.sortable_columns['cf_1']
1275 assert !q.sortable_columns['cf_1']
1268 end
1276 end
1269
1277
1270 def test_default_sort
1278 def test_default_sort
1271 q = IssueQuery.new
1279 q = IssueQuery.new
1272 assert_equal [], q.sort_criteria
1280 assert_equal [], q.sort_criteria
1273 end
1281 end
1274
1282
1275 def test_set_sort_criteria_with_hash
1283 def test_set_sort_criteria_with_hash
1276 q = IssueQuery.new
1284 q = IssueQuery.new
1277 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
1285 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
1278 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1286 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1279 end
1287 end
1280
1288
1281 def test_set_sort_criteria_with_array
1289 def test_set_sort_criteria_with_array
1282 q = IssueQuery.new
1290 q = IssueQuery.new
1283 q.sort_criteria = [['priority', 'desc'], 'tracker']
1291 q.sort_criteria = [['priority', 'desc'], 'tracker']
1284 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1292 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1285 end
1293 end
1286
1294
1287 def test_create_query_with_sort
1295 def test_create_query_with_sort
1288 q = IssueQuery.new(:name => 'Sorted')
1296 q = IssueQuery.new(:name => 'Sorted')
1289 q.sort_criteria = [['priority', 'desc'], 'tracker']
1297 q.sort_criteria = [['priority', 'desc'], 'tracker']
1290 assert q.save
1298 assert q.save
1291 q.reload
1299 q.reload
1292 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1300 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1293 end
1301 end
1294
1302
1295 def test_sort_by_string_custom_field_asc
1303 def test_sort_by_string_custom_field_asc
1296 q = IssueQuery.new
1304 q = IssueQuery.new
1297 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
1305 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
1298 assert c
1306 assert c
1299 assert c.sortable
1307 assert c.sortable
1300 issues = q.issues(:order => "#{c.sortable} ASC")
1308 issues = q.issues(:order => "#{c.sortable} ASC")
1301 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
1309 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
1302 assert !values.empty?
1310 assert !values.empty?
1303 assert_equal values.sort, values
1311 assert_equal values.sort, values
1304 end
1312 end
1305
1313
1306 def test_sort_by_string_custom_field_desc
1314 def test_sort_by_string_custom_field_desc
1307 q = IssueQuery.new
1315 q = IssueQuery.new
1308 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
1316 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
1309 assert c
1317 assert c
1310 assert c.sortable
1318 assert c.sortable
1311 issues = q.issues(:order => "#{c.sortable} DESC")
1319 issues = q.issues(:order => "#{c.sortable} DESC")
1312 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
1320 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
1313 assert !values.empty?
1321 assert !values.empty?
1314 assert_equal values.sort.reverse, values
1322 assert_equal values.sort.reverse, values
1315 end
1323 end
1316
1324
1317 def test_sort_by_float_custom_field_asc
1325 def test_sort_by_float_custom_field_asc
1318 q = IssueQuery.new
1326 q = IssueQuery.new
1319 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
1327 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
1320 assert c
1328 assert c
1321 assert c.sortable
1329 assert c.sortable
1322 issues = q.issues(:order => "#{c.sortable} ASC")
1330 issues = q.issues(:order => "#{c.sortable} ASC")
1323 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
1331 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
1324 assert !values.empty?
1332 assert !values.empty?
1325 assert_equal values.sort, values
1333 assert_equal values.sort, values
1326 end
1334 end
1327
1335
1328 def test_set_totalable_names
1336 def test_set_totalable_names
1329 q = IssueQuery.new
1337 q = IssueQuery.new
1330 q.totalable_names = ['estimated_hours', :spent_hours, '']
1338 q.totalable_names = ['estimated_hours', :spent_hours, '']
1331 assert_equal [:estimated_hours, :spent_hours], q.totalable_columns.map(&:name)
1339 assert_equal [:estimated_hours, :spent_hours], q.totalable_columns.map(&:name)
1332 end
1340 end
1333
1341
1334 def test_totalable_columns_should_default_to_settings
1342 def test_totalable_columns_should_default_to_settings
1335 with_settings :issue_list_default_totals => ['estimated_hours'] do
1343 with_settings :issue_list_default_totals => ['estimated_hours'] do
1336 q = IssueQuery.new
1344 q = IssueQuery.new
1337 assert_equal [:estimated_hours], q.totalable_columns.map(&:name)
1345 assert_equal [:estimated_hours], q.totalable_columns.map(&:name)
1338 end
1346 end
1339 end
1347 end
1340
1348
1341 def test_available_totalable_columns_should_include_estimated_hours
1349 def test_available_totalable_columns_should_include_estimated_hours
1342 q = IssueQuery.new
1350 q = IssueQuery.new
1343 assert_include :estimated_hours, q.available_totalable_columns.map(&:name)
1351 assert_include :estimated_hours, q.available_totalable_columns.map(&:name)
1344 end
1352 end
1345
1353
1346 def test_available_totalable_columns_should_include_spent_hours
1354 def test_available_totalable_columns_should_include_spent_hours
1347 User.current = User.find(1)
1355 User.current = User.find(1)
1348
1356
1349 q = IssueQuery.new
1357 q = IssueQuery.new
1350 assert_include :spent_hours, q.available_totalable_columns.map(&:name)
1358 assert_include :spent_hours, q.available_totalable_columns.map(&:name)
1351 end
1359 end
1352
1360
1353 def test_available_totalable_columns_should_include_int_custom_field
1361 def test_available_totalable_columns_should_include_int_custom_field
1354 field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
1362 field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
1355 q = IssueQuery.new
1363 q = IssueQuery.new
1356 assert_include "cf_#{field.id}".to_sym, q.available_totalable_columns.map(&:name)
1364 assert_include "cf_#{field.id}".to_sym, q.available_totalable_columns.map(&:name)
1357 end
1365 end
1358
1366
1359 def test_available_totalable_columns_should_include_float_custom_field
1367 def test_available_totalable_columns_should_include_float_custom_field
1360 field = IssueCustomField.generate!(:field_format => 'float', :is_for_all => true)
1368 field = IssueCustomField.generate!(:field_format => 'float', :is_for_all => true)
1361 q = IssueQuery.new
1369 q = IssueQuery.new
1362 assert_include "cf_#{field.id}".to_sym, q.available_totalable_columns.map(&:name)
1370 assert_include "cf_#{field.id}".to_sym, q.available_totalable_columns.map(&:name)
1363 end
1371 end
1364
1372
1365 def test_total_for_estimated_hours
1373 def test_total_for_estimated_hours
1366 Issue.delete_all
1374 Issue.delete_all
1367 Issue.generate!(:estimated_hours => 5.5)
1375 Issue.generate!(:estimated_hours => 5.5)
1368 Issue.generate!(:estimated_hours => 1.1)
1376 Issue.generate!(:estimated_hours => 1.1)
1369 Issue.generate!
1377 Issue.generate!
1370
1378
1371 q = IssueQuery.new
1379 q = IssueQuery.new
1372 assert_equal 6.6, q.total_for(:estimated_hours)
1380 assert_equal 6.6, q.total_for(:estimated_hours)
1373 end
1381 end
1374
1382
1375 def test_total_by_group_for_estimated_hours
1383 def test_total_by_group_for_estimated_hours
1376 Issue.delete_all
1384 Issue.delete_all
1377 Issue.generate!(:estimated_hours => 5.5, :assigned_to_id => 2)
1385 Issue.generate!(:estimated_hours => 5.5, :assigned_to_id => 2)
1378 Issue.generate!(:estimated_hours => 1.1, :assigned_to_id => 3)
1386 Issue.generate!(:estimated_hours => 1.1, :assigned_to_id => 3)
1379 Issue.generate!(:estimated_hours => 3.5)
1387 Issue.generate!(:estimated_hours => 3.5)
1380
1388
1381 q = IssueQuery.new(:group_by => 'assigned_to')
1389 q = IssueQuery.new(:group_by => 'assigned_to')
1382 assert_equal(
1390 assert_equal(
1383 {nil => 3.5, User.find(2) => 5.5, User.find(3) => 1.1},
1391 {nil => 3.5, User.find(2) => 5.5, User.find(3) => 1.1},
1384 q.total_by_group_for(:estimated_hours)
1392 q.total_by_group_for(:estimated_hours)
1385 )
1393 )
1386 end
1394 end
1387
1395
1388 def test_total_for_spent_hours
1396 def test_total_for_spent_hours
1389 TimeEntry.delete_all
1397 TimeEntry.delete_all
1390 TimeEntry.generate!(:hours => 5.5)
1398 TimeEntry.generate!(:hours => 5.5)
1391 TimeEntry.generate!(:hours => 1.1)
1399 TimeEntry.generate!(:hours => 1.1)
1392
1400
1393 q = IssueQuery.new
1401 q = IssueQuery.new
1394 assert_equal 6.6, q.total_for(:spent_hours)
1402 assert_equal 6.6, q.total_for(:spent_hours)
1395 end
1403 end
1396
1404
1397 def test_total_by_group_for_spent_hours
1405 def test_total_by_group_for_spent_hours
1398 TimeEntry.delete_all
1406 TimeEntry.delete_all
1399 TimeEntry.generate!(:hours => 5.5, :issue_id => 1)
1407 TimeEntry.generate!(:hours => 5.5, :issue_id => 1)
1400 TimeEntry.generate!(:hours => 1.1, :issue_id => 2)
1408 TimeEntry.generate!(:hours => 1.1, :issue_id => 2)
1401 Issue.where(:id => 1).update_all(:assigned_to_id => 2)
1409 Issue.where(:id => 1).update_all(:assigned_to_id => 2)
1402 Issue.where(:id => 2).update_all(:assigned_to_id => 3)
1410 Issue.where(:id => 2).update_all(:assigned_to_id => 3)
1403
1411
1404 q = IssueQuery.new(:group_by => 'assigned_to')
1412 q = IssueQuery.new(:group_by => 'assigned_to')
1405 assert_equal(
1413 assert_equal(
1406 {User.find(2) => 5.5, User.find(3) => 1.1},
1414 {User.find(2) => 5.5, User.find(3) => 1.1},
1407 q.total_by_group_for(:spent_hours)
1415 q.total_by_group_for(:spent_hours)
1408 )
1416 )
1409 end
1417 end
1410
1418
1411 def test_total_by_project_group_for_spent_hours
1419 def test_total_by_project_group_for_spent_hours
1412 TimeEntry.delete_all
1420 TimeEntry.delete_all
1413 TimeEntry.generate!(:hours => 5.5, :issue_id => 1)
1421 TimeEntry.generate!(:hours => 5.5, :issue_id => 1)
1414 TimeEntry.generate!(:hours => 1.1, :issue_id => 2)
1422 TimeEntry.generate!(:hours => 1.1, :issue_id => 2)
1415 Issue.where(:id => 1).update_all(:assigned_to_id => 2)
1423 Issue.where(:id => 1).update_all(:assigned_to_id => 2)
1416 Issue.where(:id => 2).update_all(:assigned_to_id => 3)
1424 Issue.where(:id => 2).update_all(:assigned_to_id => 3)
1417
1425
1418 q = IssueQuery.new(:group_by => 'project')
1426 q = IssueQuery.new(:group_by => 'project')
1419 assert_equal(
1427 assert_equal(
1420 {Project.find(1) => 6.6},
1428 {Project.find(1) => 6.6},
1421 q.total_by_group_for(:spent_hours)
1429 q.total_by_group_for(:spent_hours)
1422 )
1430 )
1423 end
1431 end
1424
1432
1425 def test_total_for_int_custom_field
1433 def test_total_for_int_custom_field
1426 field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
1434 field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
1427 CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2')
1435 CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2')
1428 CustomValue.create!(:customized => Issue.find(2), :custom_field => field, :value => '7')
1436 CustomValue.create!(:customized => Issue.find(2), :custom_field => field, :value => '7')
1429 CustomValue.create!(:customized => Issue.find(3), :custom_field => field, :value => '')
1437 CustomValue.create!(:customized => Issue.find(3), :custom_field => field, :value => '')
1430
1438
1431 q = IssueQuery.new
1439 q = IssueQuery.new
1432 assert_equal 9, q.total_for("cf_#{field.id}")
1440 assert_equal 9, q.total_for("cf_#{field.id}")
1433 end
1441 end
1434
1442
1435 def test_total_by_group_for_int_custom_field
1443 def test_total_by_group_for_int_custom_field
1436 field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
1444 field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
1437 CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2')
1445 CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2')
1438 CustomValue.create!(:customized => Issue.find(2), :custom_field => field, :value => '7')
1446 CustomValue.create!(:customized => Issue.find(2), :custom_field => field, :value => '7')
1439 Issue.where(:id => 1).update_all(:assigned_to_id => 2)
1447 Issue.where(:id => 1).update_all(:assigned_to_id => 2)
1440 Issue.where(:id => 2).update_all(:assigned_to_id => 3)
1448 Issue.where(:id => 2).update_all(:assigned_to_id => 3)
1441
1449
1442 q = IssueQuery.new(:group_by => 'assigned_to')
1450 q = IssueQuery.new(:group_by => 'assigned_to')
1443 assert_equal(
1451 assert_equal(
1444 {User.find(2) => 2, User.find(3) => 7},
1452 {User.find(2) => 2, User.find(3) => 7},
1445 q.total_by_group_for("cf_#{field.id}")
1453 q.total_by_group_for("cf_#{field.id}")
1446 )
1454 )
1447 end
1455 end
1448
1456
1449 def test_total_for_float_custom_field
1457 def test_total_for_float_custom_field
1450 field = IssueCustomField.generate!(:field_format => 'float', :is_for_all => true)
1458 field = IssueCustomField.generate!(:field_format => 'float', :is_for_all => true)
1451 CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2.3')
1459 CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2.3')
1452 CustomValue.create!(:customized => Issue.find(2), :custom_field => field, :value => '7')
1460 CustomValue.create!(:customized => Issue.find(2), :custom_field => field, :value => '7')
1453 CustomValue.create!(:customized => Issue.find(3), :custom_field => field, :value => '')
1461 CustomValue.create!(:customized => Issue.find(3), :custom_field => field, :value => '')
1454
1462
1455 q = IssueQuery.new
1463 q = IssueQuery.new
1456 assert_equal 9.3, q.total_for("cf_#{field.id}")
1464 assert_equal 9.3, q.total_for("cf_#{field.id}")
1457 end
1465 end
1458
1466
1459 def test_invalid_query_should_raise_query_statement_invalid_error
1467 def test_invalid_query_should_raise_query_statement_invalid_error
1460 q = IssueQuery.new
1468 q = IssueQuery.new
1461 assert_raise Query::StatementInvalid do
1469 assert_raise Query::StatementInvalid do
1462 q.issues(:conditions => "foo = 1")
1470 q.issues(:conditions => "foo = 1")
1463 end
1471 end
1464 end
1472 end
1465
1473
1466 def test_issue_count
1474 def test_issue_count
1467 q = IssueQuery.new(:name => '_')
1475 q = IssueQuery.new(:name => '_')
1468 issue_count = q.issue_count
1476 issue_count = q.issue_count
1469 assert_equal q.issues.size, issue_count
1477 assert_equal q.issues.size, issue_count
1470 end
1478 end
1471
1479
1472 def test_issue_count_with_archived_issues
1480 def test_issue_count_with_archived_issues
1473 p = Project.generate! do |project|
1481 p = Project.generate! do |project|
1474 project.status = Project::STATUS_ARCHIVED
1482 project.status = Project::STATUS_ARCHIVED
1475 end
1483 end
1476 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
1484 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
1477 assert !i.visible?
1485 assert !i.visible?
1478
1486
1479 test_issue_count
1487 test_issue_count
1480 end
1488 end
1481
1489
1482 def test_issue_count_by_association_group
1490 def test_issue_count_by_association_group
1483 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
1491 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
1484 count_by_group = q.issue_count_by_group
1492 count_by_group = q.issue_count_by_group
1485 assert_kind_of Hash, count_by_group
1493 assert_kind_of Hash, count_by_group
1486 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1494 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1487 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1495 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1488 assert count_by_group.has_key?(User.find(3))
1496 assert count_by_group.has_key?(User.find(3))
1489 end
1497 end
1490
1498
1491 def test_issue_count_by_list_custom_field_group
1499 def test_issue_count_by_list_custom_field_group
1492 q = IssueQuery.new(:name => '_', :group_by => 'cf_1')
1500 q = IssueQuery.new(:name => '_', :group_by => 'cf_1')
1493 count_by_group = q.issue_count_by_group
1501 count_by_group = q.issue_count_by_group
1494 assert_kind_of Hash, count_by_group
1502 assert_kind_of Hash, count_by_group
1495 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1503 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1496 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1504 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1497 assert count_by_group.has_key?('MySQL')
1505 assert count_by_group.has_key?('MySQL')
1498 end
1506 end
1499
1507
1500 def test_issue_count_by_date_custom_field_group
1508 def test_issue_count_by_date_custom_field_group
1501 q = IssueQuery.new(:name => '_', :group_by => 'cf_8')
1509 q = IssueQuery.new(:name => '_', :group_by => 'cf_8')
1502 count_by_group = q.issue_count_by_group
1510 count_by_group = q.issue_count_by_group
1503 assert_kind_of Hash, count_by_group
1511 assert_kind_of Hash, count_by_group
1504 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1512 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1505 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1513 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1506 end
1514 end
1507
1515
1508 def test_issue_count_with_nil_group_only
1516 def test_issue_count_with_nil_group_only
1509 Issue.update_all("assigned_to_id = NULL")
1517 Issue.update_all("assigned_to_id = NULL")
1510
1518
1511 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
1519 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
1512 count_by_group = q.issue_count_by_group
1520 count_by_group = q.issue_count_by_group
1513 assert_kind_of Hash, count_by_group
1521 assert_kind_of Hash, count_by_group
1514 assert_equal 1, count_by_group.keys.size
1522 assert_equal 1, count_by_group.keys.size
1515 assert_nil count_by_group.keys.first
1523 assert_nil count_by_group.keys.first
1516 end
1524 end
1517
1525
1518 def test_issue_ids
1526 def test_issue_ids
1519 q = IssueQuery.new(:name => '_')
1527 q = IssueQuery.new(:name => '_')
1520 order = "issues.subject, issues.id"
1528 order = "issues.subject, issues.id"
1521 issues = q.issues(:order => order)
1529 issues = q.issues(:order => order)
1522 assert_equal issues.map(&:id), q.issue_ids(:order => order)
1530 assert_equal issues.map(&:id), q.issue_ids(:order => order)
1523 end
1531 end
1524
1532
1525 def test_label_for
1533 def test_label_for
1526 set_language_if_valid 'en'
1534 set_language_if_valid 'en'
1527 q = IssueQuery.new
1535 q = IssueQuery.new
1528 assert_equal 'Assignee', q.label_for('assigned_to_id')
1536 assert_equal 'Assignee', q.label_for('assigned_to_id')
1529 end
1537 end
1530
1538
1531 def test_label_for_fr
1539 def test_label_for_fr
1532 set_language_if_valid 'fr'
1540 set_language_if_valid 'fr'
1533 q = IssueQuery.new
1541 q = IssueQuery.new
1534 assert_equal "Assign\xc3\xa9 \xc3\xa0".force_encoding('UTF-8'), q.label_for('assigned_to_id')
1542 assert_equal "Assign\xc3\xa9 \xc3\xa0".force_encoding('UTF-8'), q.label_for('assigned_to_id')
1535 end
1543 end
1536
1544
1537 def test_editable_by
1545 def test_editable_by
1538 admin = User.find(1)
1546 admin = User.find(1)
1539 manager = User.find(2)
1547 manager = User.find(2)
1540 developer = User.find(3)
1548 developer = User.find(3)
1541
1549
1542 # Public query on project 1
1550 # Public query on project 1
1543 q = IssueQuery.find(1)
1551 q = IssueQuery.find(1)
1544 assert q.editable_by?(admin)
1552 assert q.editable_by?(admin)
1545 assert q.editable_by?(manager)
1553 assert q.editable_by?(manager)
1546 assert !q.editable_by?(developer)
1554 assert !q.editable_by?(developer)
1547
1555
1548 # Private query on project 1
1556 # Private query on project 1
1549 q = IssueQuery.find(2)
1557 q = IssueQuery.find(2)
1550 assert q.editable_by?(admin)
1558 assert q.editable_by?(admin)
1551 assert !q.editable_by?(manager)
1559 assert !q.editable_by?(manager)
1552 assert q.editable_by?(developer)
1560 assert q.editable_by?(developer)
1553
1561
1554 # Private query for all projects
1562 # Private query for all projects
1555 q = IssueQuery.find(3)
1563 q = IssueQuery.find(3)
1556 assert q.editable_by?(admin)
1564 assert q.editable_by?(admin)
1557 assert !q.editable_by?(manager)
1565 assert !q.editable_by?(manager)
1558 assert q.editable_by?(developer)
1566 assert q.editable_by?(developer)
1559
1567
1560 # Public query for all projects
1568 # Public query for all projects
1561 q = IssueQuery.find(4)
1569 q = IssueQuery.find(4)
1562 assert q.editable_by?(admin)
1570 assert q.editable_by?(admin)
1563 assert !q.editable_by?(manager)
1571 assert !q.editable_by?(manager)
1564 assert !q.editable_by?(developer)
1572 assert !q.editable_by?(developer)
1565 end
1573 end
1566
1574
1567 def test_visible_scope
1575 def test_visible_scope
1568 query_ids = IssueQuery.visible(User.anonymous).map(&:id)
1576 query_ids = IssueQuery.visible(User.anonymous).map(&:id)
1569
1577
1570 assert query_ids.include?(1), 'public query on public project was not visible'
1578 assert query_ids.include?(1), 'public query on public project was not visible'
1571 assert query_ids.include?(4), 'public query for all projects was not visible'
1579 assert query_ids.include?(4), 'public query for all projects was not visible'
1572 assert !query_ids.include?(2), 'private query on public project was visible'
1580 assert !query_ids.include?(2), 'private query on public project was visible'
1573 assert !query_ids.include?(3), 'private query for all projects was visible'
1581 assert !query_ids.include?(3), 'private query for all projects was visible'
1574 assert !query_ids.include?(7), 'public query on private project was visible'
1582 assert !query_ids.include?(7), 'public query on private project was visible'
1575 end
1583 end
1576
1584
1577 def test_query_with_public_visibility_should_be_visible_to_anyone
1585 def test_query_with_public_visibility_should_be_visible_to_anyone
1578 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_PUBLIC)
1586 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_PUBLIC)
1579
1587
1580 assert q.visible?(User.anonymous)
1588 assert q.visible?(User.anonymous)
1581 assert IssueQuery.visible(User.anonymous).find_by_id(q.id)
1589 assert IssueQuery.visible(User.anonymous).find_by_id(q.id)
1582
1590
1583 assert q.visible?(User.find(7))
1591 assert q.visible?(User.find(7))
1584 assert IssueQuery.visible(User.find(7)).find_by_id(q.id)
1592 assert IssueQuery.visible(User.find(7)).find_by_id(q.id)
1585
1593
1586 assert q.visible?(User.find(2))
1594 assert q.visible?(User.find(2))
1587 assert IssueQuery.visible(User.find(2)).find_by_id(q.id)
1595 assert IssueQuery.visible(User.find(2)).find_by_id(q.id)
1588
1596
1589 assert q.visible?(User.find(1))
1597 assert q.visible?(User.find(1))
1590 assert IssueQuery.visible(User.find(1)).find_by_id(q.id)
1598 assert IssueQuery.visible(User.find(1)).find_by_id(q.id)
1591 end
1599 end
1592
1600
1593 def test_query_with_roles_visibility_should_be_visible_to_user_with_role
1601 def test_query_with_roles_visibility_should_be_visible_to_user_with_role
1594 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES, :role_ids => [1,2])
1602 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES, :role_ids => [1,2])
1595
1603
1596 assert !q.visible?(User.anonymous)
1604 assert !q.visible?(User.anonymous)
1597 assert_nil IssueQuery.visible(User.anonymous).find_by_id(q.id)
1605 assert_nil IssueQuery.visible(User.anonymous).find_by_id(q.id)
1598
1606
1599 assert !q.visible?(User.find(7))
1607 assert !q.visible?(User.find(7))
1600 assert_nil IssueQuery.visible(User.find(7)).find_by_id(q.id)
1608 assert_nil IssueQuery.visible(User.find(7)).find_by_id(q.id)
1601
1609
1602 assert q.visible?(User.find(2))
1610 assert q.visible?(User.find(2))
1603 assert IssueQuery.visible(User.find(2)).find_by_id(q.id)
1611 assert IssueQuery.visible(User.find(2)).find_by_id(q.id)
1604
1612
1605 assert q.visible?(User.find(1))
1613 assert q.visible?(User.find(1))
1606 assert IssueQuery.visible(User.find(1)).find_by_id(q.id)
1614 assert IssueQuery.visible(User.find(1)).find_by_id(q.id)
1607 end
1615 end
1608
1616
1609 def test_query_with_private_visibility_should_be_visible_to_owner
1617 def test_query_with_private_visibility_should_be_visible_to_owner
1610 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_PRIVATE, :user => User.find(7))
1618 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_PRIVATE, :user => User.find(7))
1611
1619
1612 assert !q.visible?(User.anonymous)
1620 assert !q.visible?(User.anonymous)
1613 assert_nil IssueQuery.visible(User.anonymous).find_by_id(q.id)
1621 assert_nil IssueQuery.visible(User.anonymous).find_by_id(q.id)
1614
1622
1615 assert q.visible?(User.find(7))
1623 assert q.visible?(User.find(7))
1616 assert IssueQuery.visible(User.find(7)).find_by_id(q.id)
1624 assert IssueQuery.visible(User.find(7)).find_by_id(q.id)
1617
1625
1618 assert !q.visible?(User.find(2))
1626 assert !q.visible?(User.find(2))
1619 assert_nil IssueQuery.visible(User.find(2)).find_by_id(q.id)
1627 assert_nil IssueQuery.visible(User.find(2)).find_by_id(q.id)
1620
1628
1621 assert q.visible?(User.find(1))
1629 assert q.visible?(User.find(1))
1622 assert_nil IssueQuery.visible(User.find(1)).find_by_id(q.id)
1630 assert_nil IssueQuery.visible(User.find(1)).find_by_id(q.id)
1623 end
1631 end
1624
1632
1625 test "#available_filters should include users of visible projects in cross-project view" do
1633 test "#available_filters should include users of visible projects in cross-project view" do
1626 users = IssueQuery.new.available_filters["assigned_to_id"]
1634 users = IssueQuery.new.available_filters["assigned_to_id"]
1627 assert_not_nil users
1635 assert_not_nil users
1628 assert users[:values].map{|u|u[1]}.include?("3")
1636 assert users[:values].map{|u|u[1]}.include?("3")
1629 end
1637 end
1630
1638
1631 test "#available_filters should include users of subprojects" do
1639 test "#available_filters should include users of subprojects" do
1632 user1 = User.generate!
1640 user1 = User.generate!
1633 user2 = User.generate!
1641 user2 = User.generate!
1634 project = Project.find(1)
1642 project = Project.find(1)
1635 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
1643 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
1636
1644
1637 users = IssueQuery.new(:project => project).available_filters["assigned_to_id"]
1645 users = IssueQuery.new(:project => project).available_filters["assigned_to_id"]
1638 assert_not_nil users
1646 assert_not_nil users
1639 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
1647 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
1640 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
1648 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
1641 end
1649 end
1642
1650
1643 test "#available_filters should include visible projects in cross-project view" do
1651 test "#available_filters should include visible projects in cross-project view" do
1644 projects = IssueQuery.new.available_filters["project_id"]
1652 projects = IssueQuery.new.available_filters["project_id"]
1645 assert_not_nil projects
1653 assert_not_nil projects
1646 assert projects[:values].map{|u|u[1]}.include?("1")
1654 assert projects[:values].map{|u|u[1]}.include?("1")
1647 end
1655 end
1648
1656
1649 test "#available_filters should include 'member_of_group' filter" do
1657 test "#available_filters should include 'member_of_group' filter" do
1650 query = IssueQuery.new
1658 query = IssueQuery.new
1651 assert query.available_filters.keys.include?("member_of_group")
1659 assert query.available_filters.keys.include?("member_of_group")
1652 assert_equal :list_optional, query.available_filters["member_of_group"][:type]
1660 assert_equal :list_optional, query.available_filters["member_of_group"][:type]
1653 assert query.available_filters["member_of_group"][:values].present?
1661 assert query.available_filters["member_of_group"][:values].present?
1654 assert_equal Group.givable.sort.map {|g| [g.name, g.id.to_s]},
1662 assert_equal Group.givable.sort.map {|g| [g.name, g.id.to_s]},
1655 query.available_filters["member_of_group"][:values].sort
1663 query.available_filters["member_of_group"][:values].sort
1656 end
1664 end
1657
1665
1658 test "#available_filters should include 'assigned_to_role' filter" do
1666 test "#available_filters should include 'assigned_to_role' filter" do
1659 query = IssueQuery.new
1667 query = IssueQuery.new
1660 assert query.available_filters.keys.include?("assigned_to_role")
1668 assert query.available_filters.keys.include?("assigned_to_role")
1661 assert_equal :list_optional, query.available_filters["assigned_to_role"][:type]
1669 assert_equal :list_optional, query.available_filters["assigned_to_role"][:type]
1662
1670
1663 assert query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
1671 assert query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
1664 assert query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
1672 assert query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
1665 assert query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
1673 assert query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
1666
1674
1667 assert ! query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
1675 assert ! query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
1668 assert ! query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
1676 assert ! query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
1669 end
1677 end
1670
1678
1671 def test_available_filters_should_include_custom_field_according_to_user_visibility
1679 def test_available_filters_should_include_custom_field_according_to_user_visibility
1672 visible_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => true)
1680 visible_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => true)
1673 hidden_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => false, :role_ids => [1])
1681 hidden_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => false, :role_ids => [1])
1674
1682
1675 with_current_user User.find(3) do
1683 with_current_user User.find(3) do
1676 query = IssueQuery.new
1684 query = IssueQuery.new
1677 assert_include "cf_#{visible_field.id}", query.available_filters.keys
1685 assert_include "cf_#{visible_field.id}", query.available_filters.keys
1678 assert_not_include "cf_#{hidden_field.id}", query.available_filters.keys
1686 assert_not_include "cf_#{hidden_field.id}", query.available_filters.keys
1679 end
1687 end
1680 end
1688 end
1681
1689
1682 def test_available_columns_should_include_custom_field_according_to_user_visibility
1690 def test_available_columns_should_include_custom_field_according_to_user_visibility
1683 visible_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => true)
1691 visible_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => true)
1684 hidden_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => false, :role_ids => [1])
1692 hidden_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => false, :role_ids => [1])
1685
1693
1686 with_current_user User.find(3) do
1694 with_current_user User.find(3) do
1687 query = IssueQuery.new
1695 query = IssueQuery.new
1688 assert_include :"cf_#{visible_field.id}", query.available_columns.map(&:name)
1696 assert_include :"cf_#{visible_field.id}", query.available_columns.map(&:name)
1689 assert_not_include :"cf_#{hidden_field.id}", query.available_columns.map(&:name)
1697 assert_not_include :"cf_#{hidden_field.id}", query.available_columns.map(&:name)
1690 end
1698 end
1691 end
1699 end
1692
1700
1693 def setup_member_of_group
1701 def setup_member_of_group
1694 Group.destroy_all # No fixtures
1702 Group.destroy_all # No fixtures
1695 @user_in_group = User.generate!
1703 @user_in_group = User.generate!
1696 @second_user_in_group = User.generate!
1704 @second_user_in_group = User.generate!
1697 @user_in_group2 = User.generate!
1705 @user_in_group2 = User.generate!
1698 @user_not_in_group = User.generate!
1706 @user_not_in_group = User.generate!
1699
1707
1700 @group = Group.generate!.reload
1708 @group = Group.generate!.reload
1701 @group.users << @user_in_group
1709 @group.users << @user_in_group
1702 @group.users << @second_user_in_group
1710 @group.users << @second_user_in_group
1703
1711
1704 @group2 = Group.generate!.reload
1712 @group2 = Group.generate!.reload
1705 @group2.users << @user_in_group2
1713 @group2.users << @user_in_group2
1706
1714
1707 @query = IssueQuery.new(:name => '_')
1715 @query = IssueQuery.new(:name => '_')
1708 end
1716 end
1709
1717
1710 test "member_of_group filter should search assigned to for users in the group" do
1718 test "member_of_group filter should search assigned to for users in the group" do
1711 setup_member_of_group
1719 setup_member_of_group
1712 @query.add_filter('member_of_group', '=', [@group.id.to_s])
1720 @query.add_filter('member_of_group', '=', [@group.id.to_s])
1713
1721
1714 assert_find_issues_with_query_is_successful @query
1722 assert_find_issues_with_query_is_successful @query
1715 end
1723 end
1716
1724
1717 test "member_of_group filter should search not assigned to any group member (none)" do
1725 test "member_of_group filter should search not assigned to any group member (none)" do
1718 setup_member_of_group
1726 setup_member_of_group
1719 @query.add_filter('member_of_group', '!*', [''])
1727 @query.add_filter('member_of_group', '!*', [''])
1720
1728
1721 assert_find_issues_with_query_is_successful @query
1729 assert_find_issues_with_query_is_successful @query
1722 end
1730 end
1723
1731
1724 test "member_of_group filter should search assigned to any group member (all)" do
1732 test "member_of_group filter should search assigned to any group member (all)" do
1725 setup_member_of_group
1733 setup_member_of_group
1726 @query.add_filter('member_of_group', '*', [''])
1734 @query.add_filter('member_of_group', '*', [''])
1727
1735
1728 assert_find_issues_with_query_is_successful @query
1736 assert_find_issues_with_query_is_successful @query
1729 end
1737 end
1730
1738
1731 test "member_of_group filter should return an empty set with = empty group" do
1739 test "member_of_group filter should return an empty set with = empty group" do
1732 setup_member_of_group
1740 setup_member_of_group
1733 @empty_group = Group.generate!
1741 @empty_group = Group.generate!
1734 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
1742 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
1735
1743
1736 assert_equal [], find_issues_with_query(@query)
1744 assert_equal [], find_issues_with_query(@query)
1737 end
1745 end
1738
1746
1739 test "member_of_group filter should return issues with ! empty group" do
1747 test "member_of_group filter should return issues with ! empty group" do
1740 setup_member_of_group
1748 setup_member_of_group
1741 @empty_group = Group.generate!
1749 @empty_group = Group.generate!
1742 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
1750 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
1743
1751
1744 assert_find_issues_with_query_is_successful @query
1752 assert_find_issues_with_query_is_successful @query
1745 end
1753 end
1746
1754
1747 def setup_assigned_to_role
1755 def setup_assigned_to_role
1748 @manager_role = Role.find_by_name('Manager')
1756 @manager_role = Role.find_by_name('Manager')
1749 @developer_role = Role.find_by_name('Developer')
1757 @developer_role = Role.find_by_name('Developer')
1750
1758
1751 @project = Project.generate!
1759 @project = Project.generate!
1752 @manager = User.generate!
1760 @manager = User.generate!
1753 @developer = User.generate!
1761 @developer = User.generate!
1754 @boss = User.generate!
1762 @boss = User.generate!
1755 @guest = User.generate!
1763 @guest = User.generate!
1756 User.add_to_project(@manager, @project, @manager_role)
1764 User.add_to_project(@manager, @project, @manager_role)
1757 User.add_to_project(@developer, @project, @developer_role)
1765 User.add_to_project(@developer, @project, @developer_role)
1758 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
1766 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
1759
1767
1760 @issue1 = Issue.generate!(:project => @project, :assigned_to_id => @manager.id)
1768 @issue1 = Issue.generate!(:project => @project, :assigned_to_id => @manager.id)
1761 @issue2 = Issue.generate!(:project => @project, :assigned_to_id => @developer.id)
1769 @issue2 = Issue.generate!(:project => @project, :assigned_to_id => @developer.id)
1762 @issue3 = Issue.generate!(:project => @project, :assigned_to_id => @boss.id)
1770 @issue3 = Issue.generate!(:project => @project, :assigned_to_id => @boss.id)
1763 @issue4 = Issue.generate!(:project => @project, :author_id => @guest.id, :assigned_to_id => @guest.id)
1771 @issue4 = Issue.generate!(:project => @project, :author_id => @guest.id, :assigned_to_id => @guest.id)
1764 @issue5 = Issue.generate!(:project => @project)
1772 @issue5 = Issue.generate!(:project => @project)
1765
1773
1766 @query = IssueQuery.new(:name => '_', :project => @project)
1774 @query = IssueQuery.new(:name => '_', :project => @project)
1767 end
1775 end
1768
1776
1769 test "assigned_to_role filter should search assigned to for users with the Role" do
1777 test "assigned_to_role filter should search assigned to for users with the Role" do
1770 setup_assigned_to_role
1778 setup_assigned_to_role
1771 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1779 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1772
1780
1773 assert_query_result [@issue1, @issue3], @query
1781 assert_query_result [@issue1, @issue3], @query
1774 end
1782 end
1775
1783
1776 test "assigned_to_role filter should search assigned to for users with the Role on the issue project" do
1784 test "assigned_to_role filter should search assigned to for users with the Role on the issue project" do
1777 setup_assigned_to_role
1785 setup_assigned_to_role
1778 other_project = Project.generate!
1786 other_project = Project.generate!
1779 User.add_to_project(@developer, other_project, @manager_role)
1787 User.add_to_project(@developer, other_project, @manager_role)
1780 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1788 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1781
1789
1782 assert_query_result [@issue1, @issue3], @query
1790 assert_query_result [@issue1, @issue3], @query
1783 end
1791 end
1784
1792
1785 test "assigned_to_role filter should return an empty set with empty role" do
1793 test "assigned_to_role filter should return an empty set with empty role" do
1786 setup_assigned_to_role
1794 setup_assigned_to_role
1787 @empty_role = Role.generate!
1795 @empty_role = Role.generate!
1788 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
1796 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
1789
1797
1790 assert_query_result [], @query
1798 assert_query_result [], @query
1791 end
1799 end
1792
1800
1793 test "assigned_to_role filter should search assigned to for users without the Role" do
1801 test "assigned_to_role filter should search assigned to for users without the Role" do
1794 setup_assigned_to_role
1802 setup_assigned_to_role
1795 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
1803 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
1796
1804
1797 assert_query_result [@issue2, @issue4, @issue5], @query
1805 assert_query_result [@issue2, @issue4, @issue5], @query
1798 end
1806 end
1799
1807
1800 test "assigned_to_role filter should search assigned to for users not assigned to any Role (none)" do
1808 test "assigned_to_role filter should search assigned to for users not assigned to any Role (none)" do
1801 setup_assigned_to_role
1809 setup_assigned_to_role
1802 @query.add_filter('assigned_to_role', '!*', [''])
1810 @query.add_filter('assigned_to_role', '!*', [''])
1803
1811
1804 assert_query_result [@issue4, @issue5], @query
1812 assert_query_result [@issue4, @issue5], @query
1805 end
1813 end
1806
1814
1807 test "assigned_to_role filter should search assigned to for users assigned to any Role (all)" do
1815 test "assigned_to_role filter should search assigned to for users assigned to any Role (all)" do
1808 setup_assigned_to_role
1816 setup_assigned_to_role
1809 @query.add_filter('assigned_to_role', '*', [''])
1817 @query.add_filter('assigned_to_role', '*', [''])
1810
1818
1811 assert_query_result [@issue1, @issue2, @issue3], @query
1819 assert_query_result [@issue1, @issue2, @issue3], @query
1812 end
1820 end
1813
1821
1814 test "assigned_to_role filter should return issues with ! empty role" do
1822 test "assigned_to_role filter should return issues with ! empty role" do
1815 setup_assigned_to_role
1823 setup_assigned_to_role
1816 @empty_role = Role.generate!
1824 @empty_role = Role.generate!
1817 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
1825 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
1818
1826
1819 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
1827 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
1820 end
1828 end
1821
1829
1822 def test_query_column_should_accept_a_symbol_as_caption
1830 def test_query_column_should_accept_a_symbol_as_caption
1823 set_language_if_valid 'en'
1831 set_language_if_valid 'en'
1824 c = QueryColumn.new('foo', :caption => :general_text_Yes)
1832 c = QueryColumn.new('foo', :caption => :general_text_Yes)
1825 assert_equal 'Yes', c.caption
1833 assert_equal 'Yes', c.caption
1826 end
1834 end
1827
1835
1828 def test_query_column_should_accept_a_proc_as_caption
1836 def test_query_column_should_accept_a_proc_as_caption
1829 c = QueryColumn.new('foo', :caption => lambda {'Foo'})
1837 c = QueryColumn.new('foo', :caption => lambda {'Foo'})
1830 assert_equal 'Foo', c.caption
1838 assert_equal 'Foo', c.caption
1831 end
1839 end
1832
1840
1833 def test_date_clause_should_respect_user_time_zone_with_local_default
1841 def test_date_clause_should_respect_user_time_zone_with_local_default
1834 @query = IssueQuery.new(:name => '_')
1842 @query = IssueQuery.new(:name => '_')
1835
1843
1836 # user is in Hawaii (-10)
1844 # user is in Hawaii (-10)
1837 User.current = users(:users_001)
1845 User.current = users(:users_001)
1838 User.current.pref.update_attribute :time_zone, 'Hawaii'
1846 User.current.pref.update_attribute :time_zone, 'Hawaii'
1839
1847
1840 # assume timestamps are stored in server local time
1848 # assume timestamps are stored in server local time
1841 local_zone = Time.zone
1849 local_zone = Time.zone
1842
1850
1843 from = Date.parse '2016-03-20'
1851 from = Date.parse '2016-03-20'
1844 to = Date.parse '2016-03-22'
1852 to = Date.parse '2016-03-22'
1845 assert c = @query.send(:date_clause, 'table', 'field', from, to, false)
1853 assert c = @query.send(:date_clause, 'table', 'field', from, to, false)
1846
1854
1847 # the dates should have been interpreted in the user's time zone and
1855 # the dates should have been interpreted in the user's time zone and
1848 # converted to local time
1856 # converted to local time
1849 # what we get exactly in the sql depends on the local time zone, therefore
1857 # what we get exactly in the sql depends on the local time zone, therefore
1850 # it's computed here.
1858 # it's computed here.
1851 f = User.current.time_zone.local(from.year, from.month, from.day).yesterday.end_of_day.in_time_zone(local_zone)
1859 f = User.current.time_zone.local(from.year, from.month, from.day).yesterday.end_of_day.in_time_zone(local_zone)
1852 t = User.current.time_zone.local(to.year, to.month, to.day).end_of_day.in_time_zone(local_zone)
1860 t = User.current.time_zone.local(to.year, to.month, to.day).end_of_day.in_time_zone(local_zone)
1853 assert_equal "table.field > '#{Query.connection.quoted_date f}' AND table.field <= '#{Query.connection.quoted_date t}'", c
1861 assert_equal "table.field > '#{Query.connection.quoted_date f}' AND table.field <= '#{Query.connection.quoted_date t}'", c
1854 end
1862 end
1855
1863
1856 def test_date_clause_should_respect_user_time_zone_with_utc_default
1864 def test_date_clause_should_respect_user_time_zone_with_utc_default
1857 @query = IssueQuery.new(:name => '_')
1865 @query = IssueQuery.new(:name => '_')
1858
1866
1859 # user is in Hawaii (-10)
1867 # user is in Hawaii (-10)
1860 User.current = users(:users_001)
1868 User.current = users(:users_001)
1861 User.current.pref.update_attribute :time_zone, 'Hawaii'
1869 User.current.pref.update_attribute :time_zone, 'Hawaii'
1862
1870
1863 # assume timestamps are stored as utc
1871 # assume timestamps are stored as utc
1864 ActiveRecord::Base.default_timezone = :utc
1872 ActiveRecord::Base.default_timezone = :utc
1865
1873
1866 from = Date.parse '2016-03-20'
1874 from = Date.parse '2016-03-20'
1867 to = Date.parse '2016-03-22'
1875 to = Date.parse '2016-03-22'
1868 assert c = @query.send(:date_clause, 'table', 'field', from, to, false)
1876 assert c = @query.send(:date_clause, 'table', 'field', from, to, false)
1869 # the dates should have been interpreted in the user's time zone and
1877 # the dates should have been interpreted in the user's time zone and
1870 # converted to utc. March 20 in Hawaii begins at 10am UTC.
1878 # converted to utc. March 20 in Hawaii begins at 10am UTC.
1871 f = Time.new(2016, 3, 20, 9, 59, 59, 0).end_of_hour
1879 f = Time.new(2016, 3, 20, 9, 59, 59, 0).end_of_hour
1872 t = Time.new(2016, 3, 23, 9, 59, 59, 0).end_of_hour
1880 t = Time.new(2016, 3, 23, 9, 59, 59, 0).end_of_hour
1873 assert_equal "table.field > '#{Query.connection.quoted_date f}' AND table.field <= '#{Query.connection.quoted_date t}'", c
1881 assert_equal "table.field > '#{Query.connection.quoted_date f}' AND table.field <= '#{Query.connection.quoted_date t}'", c
1874 ensure
1882 ensure
1875 ActiveRecord::Base.default_timezone = :local # restore Redmine default
1883 ActiveRecord::Base.default_timezone = :local # restore Redmine default
1876 end
1884 end
1877
1885
1878 def test_filter_on_subprojects
1886 def test_filter_on_subprojects
1879 query = IssueQuery.new(:name => '_', :project => Project.find(1))
1887 query = IssueQuery.new(:name => '_', :project => Project.find(1))
1880 filter_name = "subproject_id"
1888 filter_name = "subproject_id"
1881 assert_include filter_name, query.available_filters.keys
1889 assert_include filter_name, query.available_filters.keys
1882
1890
1883 # "is" operator should include issues of parent project + issues of the selected subproject
1891 # "is" operator should include issues of parent project + issues of the selected subproject
1884 query.filters = {filter_name => {:operator => '=', :values => ['3']}}
1892 query.filters = {filter_name => {:operator => '=', :values => ['3']}}
1885 issues = find_issues_with_query(query)
1893 issues = find_issues_with_query(query)
1886 assert_equal [1, 2, 3, 5, 7, 8, 11, 12, 13, 14], issues.map(&:id).sort
1894 assert_equal [1, 2, 3, 5, 7, 8, 11, 12, 13, 14], issues.map(&:id).sort
1887
1895
1888 # "is not" operator should include issues of parent project + issues of all active subprojects - issues of the selected subprojects
1896 # "is not" operator should include issues of parent project + issues of all active subprojects - issues of the selected subprojects
1889 query = IssueQuery.new(:name => '_', :project => Project.find(1))
1897 query = IssueQuery.new(:name => '_', :project => Project.find(1))
1890 query.filters = {filter_name => {:operator => '!', :values => ['3']}}
1898 query.filters = {filter_name => {:operator => '!', :values => ['3']}}
1891 issues = find_issues_with_query(query)
1899 issues = find_issues_with_query(query)
1892 assert_equal [1, 2, 3, 6, 7, 8, 9, 10, 11, 12], issues.map(&:id).sort
1900 assert_equal [1, 2, 3, 6, 7, 8, 9, 10, 11, 12], issues.map(&:id).sort
1893 end
1901 end
1894
1902
1895 end
1903 end
General Comments 0
You need to be logged in to leave comments. Login now