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