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