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