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