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