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