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