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