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