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