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