##// END OF EJS Templates
Remember the selected "Member of Role" and "Member of Group" options. #6467...
Eric Davis -
r4146:fef21d5aa2d6
parent child
Show More
@@ -1,643 +1,643
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 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 end
46 46
47 47 class QueryCustomFieldColumn < QueryColumn
48 48
49 49 def initialize(custom_field)
50 50 self.name = "cf_#{custom_field.id}".to_sym
51 51 self.sortable = custom_field.order_statement || false
52 52 if %w(list date bool int).include?(custom_field.field_format)
53 53 self.groupable = custom_field.order_statement
54 54 end
55 55 self.groupable ||= false
56 56 @cf = custom_field
57 57 end
58 58
59 59 def caption
60 60 @cf.name
61 61 end
62 62
63 63 def custom_field
64 64 @cf
65 65 end
66 66
67 67 def value(issue)
68 68 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
69 69 cv && @cf.cast_value(cv.value)
70 70 end
71 71 end
72 72
73 73 class Query < ActiveRecord::Base
74 74 class StatementInvalid < ::ActiveRecord::StatementInvalid
75 75 end
76 76
77 77 belongs_to :project
78 78 belongs_to :user
79 79 serialize :filters
80 80 serialize :column_names
81 81 serialize :sort_criteria, Array
82 82
83 83 attr_protected :project_id, :user_id
84 84
85 85 validates_presence_of :name, :on => :save
86 86 validates_length_of :name, :maximum => 255
87 87
88 88 @@operators = { "=" => :label_equals,
89 89 "!" => :label_not_equals,
90 90 "o" => :label_open_issues,
91 91 "c" => :label_closed_issues,
92 92 "!*" => :label_none,
93 93 "*" => :label_all,
94 94 ">=" => :label_greater_or_equal,
95 95 "<=" => :label_less_or_equal,
96 96 "<t+" => :label_in_less_than,
97 97 ">t+" => :label_in_more_than,
98 98 "t+" => :label_in,
99 99 "t" => :label_today,
100 100 "w" => :label_this_week,
101 101 ">t-" => :label_less_than_ago,
102 102 "<t-" => :label_more_than_ago,
103 103 "t-" => :label_ago,
104 104 "~" => :label_contains,
105 105 "!~" => :label_not_contains }
106 106
107 107 cattr_reader :operators
108 108
109 109 @@operators_by_filter_type = { :list => [ "=", "!" ],
110 110 :list_status => [ "o", "=", "!", "c", "*" ],
111 111 :list_optional => [ "=", "!", "!*", "*" ],
112 112 :list_subprojects => [ "*", "!*", "=" ],
113 113 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
114 114 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
115 115 :string => [ "=", "~", "!", "!~" ],
116 116 :text => [ "~", "!~" ],
117 117 :integer => [ "=", ">=", "<=", "!*", "*" ] }
118 118
119 119 cattr_reader :operators_by_filter_type
120 120
121 121 @@available_columns = [
122 122 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
123 123 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
124 124 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
125 125 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
126 126 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
127 127 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
128 128 QueryColumn.new(:author),
129 129 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
130 130 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
131 131 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
132 132 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
133 133 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
134 134 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
135 135 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
136 136 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
137 137 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
138 138 ]
139 139 cattr_reader :available_columns
140 140
141 141 def initialize(attributes = nil)
142 142 super attributes
143 143 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
144 144 end
145 145
146 146 def after_initialize
147 147 # Store the fact that project is nil (used in #editable_by?)
148 148 @is_for_all = project.nil?
149 149 end
150 150
151 151 def validate
152 152 filters.each_key do |field|
153 153 errors.add label_for(field), :blank unless
154 154 # filter requires one or more values
155 155 (values_for(field) and !values_for(field).first.blank?) or
156 156 # filter doesn't require any value
157 157 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
158 158 end if filters
159 159 end
160 160
161 161 def editable_by?(user)
162 162 return false unless user
163 163 # Admin can edit them all and regular users can edit their private queries
164 164 return true if user.admin? || (!is_public && self.user_id == user.id)
165 165 # Members can not edit public queries that are for all project (only admin is allowed to)
166 166 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
167 167 end
168 168
169 169 def available_filters
170 170 return @available_filters if @available_filters
171 171
172 172 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
173 173
174 174 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
175 175 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
176 176 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
177 177 "subject" => { :type => :text, :order => 8 },
178 178 "created_on" => { :type => :date_past, :order => 9 },
179 179 "updated_on" => { :type => :date_past, :order => 10 },
180 180 "start_date" => { :type => :date, :order => 11 },
181 181 "due_date" => { :type => :date, :order => 12 },
182 182 "estimated_hours" => { :type => :integer, :order => 13 },
183 183 "done_ratio" => { :type => :integer, :order => 14 }}
184 184
185 185 user_values = []
186 186 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
187 187 if project
188 188 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
189 189 else
190 190 project_ids = Project.all(:conditions => Project.visible_by(User.current)).collect(&:id)
191 191 if project_ids.any?
192 192 # members of the user's projects
193 193 user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", project_ids]).sort.collect{|s| [s.name, s.id.to_s] }
194 194 end
195 195 end
196 196 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
197 197 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
198 198
199 group_values = Group.all.collect {|g| [g.name, g.id] }
199 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
200 200 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
201 201
202 role_values = Role.givable.collect {|r| [r.name, r.id] }
202 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
203 203 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
204 204
205 205 if User.current.logged?
206 206 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
207 207 end
208 208
209 209 if project
210 210 # project specific filters
211 211 unless @project.issue_categories.empty?
212 212 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
213 213 end
214 214 unless @project.shared_versions.empty?
215 215 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
216 216 end
217 217 unless @project.descendants.active.empty?
218 218 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
219 219 end
220 220 add_custom_fields_filters(@project.all_issue_custom_fields)
221 221 else
222 222 # global filters for cross project issue list
223 223 system_shared_versions = Version.visible.find_all_by_sharing('system')
224 224 unless system_shared_versions.empty?
225 225 @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] } }
226 226 end
227 227 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
228 228 # project filter
229 229 project_values = Project.all(:conditions => Project.visible_by(User.current), :order => 'lft').map do |p|
230 230 pre = (p.level > 0 ? ('--' * p.level + ' ') : '')
231 231 ["#{pre}#{p.name}",p.id.to_s]
232 232 end
233 233 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values}
234 234 end
235 235 @available_filters
236 236 end
237 237
238 238 def add_filter(field, operator, values)
239 239 # values must be an array
240 240 return unless values and values.is_a? Array # and !values.first.empty?
241 241 # check if field is defined as an available filter
242 242 if available_filters.has_key? field
243 243 filter_options = available_filters[field]
244 244 # check if operator is allowed for that filter
245 245 #if @@operators_by_filter_type[filter_options[:type]].include? operator
246 246 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
247 247 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
248 248 #end
249 249 filters[field] = {:operator => operator, :values => values }
250 250 end
251 251 end
252 252
253 253 def add_short_filter(field, expression)
254 254 return unless expression
255 255 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
256 256 add_filter field, (parms[0] || "="), [parms[1] || ""]
257 257 end
258 258
259 259 # Add multiple filters using +add_filter+
260 260 def add_filters(fields, operators, values)
261 261 fields.each do |field|
262 262 add_filter(field, operators[field], values[field])
263 263 end
264 264 end
265 265
266 266 def has_filter?(field)
267 267 filters and filters[field]
268 268 end
269 269
270 270 def operator_for(field)
271 271 has_filter?(field) ? filters[field][:operator] : nil
272 272 end
273 273
274 274 def values_for(field)
275 275 has_filter?(field) ? filters[field][:values] : nil
276 276 end
277 277
278 278 def label_for(field)
279 279 label = available_filters[field][:name] if available_filters.has_key?(field)
280 280 label ||= field.gsub(/\_id$/, "")
281 281 end
282 282
283 283 def available_columns
284 284 return @available_columns if @available_columns
285 285 @available_columns = Query.available_columns
286 286 @available_columns += (project ?
287 287 project.all_issue_custom_fields :
288 288 IssueCustomField.find(:all)
289 289 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
290 290 end
291 291
292 292 def self.available_columns=(v)
293 293 self.available_columns = (v)
294 294 end
295 295
296 296 def self.add_available_column(column)
297 297 self.available_columns << (column) if column.is_a?(QueryColumn)
298 298 end
299 299
300 300 # Returns an array of columns that can be used to group the results
301 301 def groupable_columns
302 302 available_columns.select {|c| c.groupable}
303 303 end
304 304
305 305 # Returns a Hash of columns and the key for sorting
306 306 def sortable_columns
307 307 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
308 308 h[column.name.to_s] = column.sortable
309 309 h
310 310 })
311 311 end
312 312
313 313 def columns
314 314 if has_default_columns?
315 315 available_columns.select do |c|
316 316 # Adds the project column by default for cross-project lists
317 317 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
318 318 end
319 319 else
320 320 # preserve the column_names order
321 321 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
322 322 end
323 323 end
324 324
325 325 def column_names=(names)
326 326 if names
327 327 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
328 328 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
329 329 # Set column_names to nil if default columns
330 330 if names.map(&:to_s) == Setting.issue_list_default_columns
331 331 names = nil
332 332 end
333 333 end
334 334 write_attribute(:column_names, names)
335 335 end
336 336
337 337 def has_column?(column)
338 338 column_names && column_names.include?(column.name)
339 339 end
340 340
341 341 def has_default_columns?
342 342 column_names.nil? || column_names.empty?
343 343 end
344 344
345 345 def sort_criteria=(arg)
346 346 c = []
347 347 if arg.is_a?(Hash)
348 348 arg = arg.keys.sort.collect {|k| arg[k]}
349 349 end
350 350 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
351 351 write_attribute(:sort_criteria, c)
352 352 end
353 353
354 354 def sort_criteria
355 355 read_attribute(:sort_criteria) || []
356 356 end
357 357
358 358 def sort_criteria_key(arg)
359 359 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
360 360 end
361 361
362 362 def sort_criteria_order(arg)
363 363 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
364 364 end
365 365
366 366 # Returns the SQL sort order that should be prepended for grouping
367 367 def group_by_sort_order
368 368 if grouped? && (column = group_by_column)
369 369 column.sortable.is_a?(Array) ?
370 370 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
371 371 "#{column.sortable} #{column.default_order}"
372 372 end
373 373 end
374 374
375 375 # Returns true if the query is a grouped query
376 376 def grouped?
377 377 !group_by.blank?
378 378 end
379 379
380 380 def group_by_column
381 381 groupable_columns.detect {|c| c.name.to_s == group_by}
382 382 end
383 383
384 384 def group_by_statement
385 385 group_by_column.groupable
386 386 end
387 387
388 388 def project_statement
389 389 project_clauses = []
390 390 if project && !@project.descendants.active.empty?
391 391 ids = [project.id]
392 392 if has_filter?("subproject_id")
393 393 case operator_for("subproject_id")
394 394 when '='
395 395 # include the selected subprojects
396 396 ids += values_for("subproject_id").each(&:to_i)
397 397 when '!*'
398 398 # main project only
399 399 else
400 400 # all subprojects
401 401 ids += project.descendants.collect(&:id)
402 402 end
403 403 elsif Setting.display_subprojects_issues?
404 404 ids += project.descendants.collect(&:id)
405 405 end
406 406 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
407 407 elsif project
408 408 project_clauses << "#{Project.table_name}.id = %d" % project.id
409 409 end
410 410 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
411 411 project_clauses.join(' AND ')
412 412 end
413 413
414 414 def statement
415 415 # filters clauses
416 416 filters_clauses = []
417 417 filters.each_key do |field|
418 418 next if field == "subproject_id"
419 419 v = values_for(field).clone
420 420 next unless v and !v.empty?
421 421 operator = operator_for(field)
422 422
423 423 # "me" value subsitution
424 424 if %w(assigned_to_id author_id watcher_id).include?(field)
425 425 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
426 426 end
427 427
428 428 sql = ''
429 429 if field =~ /^cf_(\d+)$/
430 430 # custom field
431 431 db_table = CustomValue.table_name
432 432 db_field = 'value'
433 433 is_custom_filter = true
434 434 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 "
435 435 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
436 436 elsif field == 'watcher_id'
437 437 db_table = Watcher.table_name
438 438 db_field = 'user_id'
439 439 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
440 440 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
441 441 elsif field == "member_of_group" # named field
442 442 if operator == '*' # Any group
443 443 groups = Group.all
444 444 operator = '=' # Override the operator since we want to find by assigned_to
445 445 elsif operator == "!*"
446 446 groups = Group.all
447 447 operator = '!' # Override the operator since we want to find by assigned_to
448 448 else
449 449 groups = Group.find_all_by_id(v)
450 450 end
451 451 groups ||= []
452 452
453 453 members_of_groups = groups.inject([]) {|user_ids, group|
454 454 if group && group.user_ids.present?
455 455 user_ids << group.user_ids
456 456 end
457 457 user_ids.flatten.uniq.compact
458 458 }.sort.collect(&:to_s)
459 459
460 460 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
461 461
462 462 elsif field == "assigned_to_role" # named field
463 463 if operator == "*" # Any Role
464 464 roles = Role.givable
465 465 operator = '=' # Override the operator since we want to find by assigned_to
466 466 elsif operator == "!*" # No role
467 467 roles = Role.givable
468 468 operator = '!' # Override the operator since we want to find by assigned_to
469 469 else
470 470 roles = Role.givable.find_all_by_id(v)
471 471 end
472 472 roles ||= []
473 473
474 474 members_of_roles = roles.inject([]) {|user_ids, role|
475 475 if role && role.members
476 476 user_ids << role.members.collect(&:user_id)
477 477 end
478 478 user_ids.flatten.uniq.compact
479 479 }.sort.collect(&:to_s)
480 480
481 481 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
482 482 else
483 483 # regular field
484 484 db_table = Issue.table_name
485 485 db_field = field
486 486 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
487 487 end
488 488 filters_clauses << sql
489 489
490 490 end if filters and valid?
491 491
492 492 (filters_clauses << project_statement).join(' AND ')
493 493 end
494 494
495 495 # Returns the issue count
496 496 def issue_count
497 497 Issue.count(:include => [:status, :project], :conditions => statement)
498 498 rescue ::ActiveRecord::StatementInvalid => e
499 499 raise StatementInvalid.new(e.message)
500 500 end
501 501
502 502 # Returns the issue count by group or nil if query is not grouped
503 503 def issue_count_by_group
504 504 r = nil
505 505 if grouped?
506 506 begin
507 507 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
508 508 r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
509 509 rescue ActiveRecord::RecordNotFound
510 510 r = {nil => issue_count}
511 511 end
512 512 c = group_by_column
513 513 if c.is_a?(QueryCustomFieldColumn)
514 514 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
515 515 end
516 516 end
517 517 r
518 518 rescue ::ActiveRecord::StatementInvalid => e
519 519 raise StatementInvalid.new(e.message)
520 520 end
521 521
522 522 # Returns the issues
523 523 # Valid options are :order, :offset, :limit, :include, :conditions
524 524 def issues(options={})
525 525 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
526 526 order_option = nil if order_option.blank?
527 527
528 528 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
529 529 :conditions => Query.merge_conditions(statement, options[:conditions]),
530 530 :order => order_option,
531 531 :limit => options[:limit],
532 532 :offset => options[:offset]
533 533 rescue ::ActiveRecord::StatementInvalid => e
534 534 raise StatementInvalid.new(e.message)
535 535 end
536 536
537 537 # Returns the journals
538 538 # Valid options are :order, :offset, :limit
539 539 def journals(options={})
540 540 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
541 541 :conditions => statement,
542 542 :order => options[:order],
543 543 :limit => options[:limit],
544 544 :offset => options[:offset]
545 545 rescue ::ActiveRecord::StatementInvalid => e
546 546 raise StatementInvalid.new(e.message)
547 547 end
548 548
549 549 # Returns the versions
550 550 # Valid options are :conditions
551 551 def versions(options={})
552 552 Version.find :all, :include => :project,
553 553 :conditions => Query.merge_conditions(project_statement, options[:conditions])
554 554 rescue ::ActiveRecord::StatementInvalid => e
555 555 raise StatementInvalid.new(e.message)
556 556 end
557 557
558 558 private
559 559
560 560 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
561 561 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
562 562 sql = ''
563 563 case operator
564 564 when "="
565 565 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
566 566 when "!"
567 567 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
568 568 when "!*"
569 569 sql = "#{db_table}.#{db_field} IS NULL"
570 570 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
571 571 when "*"
572 572 sql = "#{db_table}.#{db_field} IS NOT NULL"
573 573 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
574 574 when ">="
575 575 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
576 576 when "<="
577 577 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
578 578 when "o"
579 579 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
580 580 when "c"
581 581 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
582 582 when ">t-"
583 583 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
584 584 when "<t-"
585 585 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
586 586 when "t-"
587 587 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
588 588 when ">t+"
589 589 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
590 590 when "<t+"
591 591 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
592 592 when "t+"
593 593 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
594 594 when "t"
595 595 sql = date_range_clause(db_table, db_field, 0, 0)
596 596 when "w"
597 597 from = l(:general_first_day_of_week) == '7' ?
598 598 # week starts on sunday
599 599 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
600 600 # week starts on monday (Rails default)
601 601 Time.now.at_beginning_of_week
602 602 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
603 603 when "~"
604 604 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
605 605 when "!~"
606 606 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
607 607 end
608 608
609 609 return sql
610 610 end
611 611
612 612 def add_custom_fields_filters(custom_fields)
613 613 @available_filters ||= {}
614 614
615 615 custom_fields.select(&:is_filter?).each do |field|
616 616 case field.field_format
617 617 when "text"
618 618 options = { :type => :text, :order => 20 }
619 619 when "list"
620 620 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
621 621 when "date"
622 622 options = { :type => :date, :order => 20 }
623 623 when "bool"
624 624 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
625 625 else
626 626 options = { :type => :string, :order => 20 }
627 627 end
628 628 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
629 629 end
630 630 end
631 631
632 632 # Returns a SQL clause for a date or datetime field.
633 633 def date_range_clause(table, field, from, to)
634 634 s = []
635 635 if from
636 636 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
637 637 end
638 638 if to
639 639 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
640 640 end
641 641 s.join(' AND ')
642 642 end
643 643 end
@@ -1,524 +1,524
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2008 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.dirname(__FILE__) + '/../test_helper'
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_operator_greater_than
105 105 query = Query.new(:project => Project.find(1), :name => '_')
106 106 query.add_filter('done_ratio', '>=', ['40'])
107 107 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
108 108 find_issues_with_query(query)
109 109 end
110 110
111 111 def test_operator_in_more_than
112 112 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
113 113 query = Query.new(:project => Project.find(1), :name => '_')
114 114 query.add_filter('due_date', '>t+', ['15'])
115 115 issues = find_issues_with_query(query)
116 116 assert !issues.empty?
117 117 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
118 118 end
119 119
120 120 def test_operator_in_less_than
121 121 query = Query.new(:project => Project.find(1), :name => '_')
122 122 query.add_filter('due_date', '<t+', ['15'])
123 123 issues = find_issues_with_query(query)
124 124 assert !issues.empty?
125 125 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
126 126 end
127 127
128 128 def test_operator_less_than_ago
129 129 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
130 130 query = Query.new(:project => Project.find(1), :name => '_')
131 131 query.add_filter('due_date', '>t-', ['3'])
132 132 issues = find_issues_with_query(query)
133 133 assert !issues.empty?
134 134 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
135 135 end
136 136
137 137 def test_operator_more_than_ago
138 138 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
139 139 query = Query.new(:project => Project.find(1), :name => '_')
140 140 query.add_filter('due_date', '<t-', ['10'])
141 141 assert query.statement.include?("#{Issue.table_name}.due_date <=")
142 142 issues = find_issues_with_query(query)
143 143 assert !issues.empty?
144 144 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
145 145 end
146 146
147 147 def test_operator_in
148 148 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
149 149 query = Query.new(:project => Project.find(1), :name => '_')
150 150 query.add_filter('due_date', 't+', ['2'])
151 151 issues = find_issues_with_query(query)
152 152 assert !issues.empty?
153 153 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
154 154 end
155 155
156 156 def test_operator_ago
157 157 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
158 158 query = Query.new(:project => Project.find(1), :name => '_')
159 159 query.add_filter('due_date', 't-', ['3'])
160 160 issues = find_issues_with_query(query)
161 161 assert !issues.empty?
162 162 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
163 163 end
164 164
165 165 def test_operator_today
166 166 query = Query.new(:project => Project.find(1), :name => '_')
167 167 query.add_filter('due_date', 't', [''])
168 168 issues = find_issues_with_query(query)
169 169 assert !issues.empty?
170 170 issues.each {|issue| assert_equal Date.today, issue.due_date}
171 171 end
172 172
173 173 def test_operator_this_week_on_date
174 174 query = Query.new(:project => Project.find(1), :name => '_')
175 175 query.add_filter('due_date', 'w', [''])
176 176 find_issues_with_query(query)
177 177 end
178 178
179 179 def test_operator_this_week_on_datetime
180 180 query = Query.new(:project => Project.find(1), :name => '_')
181 181 query.add_filter('created_on', 'w', [''])
182 182 find_issues_with_query(query)
183 183 end
184 184
185 185 def test_operator_contains
186 186 query = Query.new(:project => Project.find(1), :name => '_')
187 187 query.add_filter('subject', '~', ['uNable'])
188 188 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
189 189 result = find_issues_with_query(query)
190 190 assert result.empty?
191 191 result.each {|issue| assert issue.subject.downcase.include?('unable') }
192 192 end
193 193
194 194 def test_operator_does_not_contains
195 195 query = Query.new(:project => Project.find(1), :name => '_')
196 196 query.add_filter('subject', '!~', ['uNable'])
197 197 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
198 198 find_issues_with_query(query)
199 199 end
200 200
201 201 def test_filter_watched_issues
202 202 User.current = User.find(1)
203 203 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
204 204 result = find_issues_with_query(query)
205 205 assert_not_nil result
206 206 assert !result.empty?
207 207 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
208 208 User.current = nil
209 209 end
210 210
211 211 def test_filter_unwatched_issues
212 212 User.current = User.find(1)
213 213 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
214 214 result = find_issues_with_query(query)
215 215 assert_not_nil result
216 216 assert !result.empty?
217 217 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
218 218 User.current = nil
219 219 end
220 220
221 221 def test_default_columns
222 222 q = Query.new
223 223 assert !q.columns.empty?
224 224 end
225 225
226 226 def test_set_column_names
227 227 q = Query.new
228 228 q.column_names = ['tracker', :subject, '', 'unknonw_column']
229 229 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
230 230 c = q.columns.first
231 231 assert q.has_column?(c)
232 232 end
233 233
234 234 def test_groupable_columns_should_include_custom_fields
235 235 q = Query.new
236 236 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
237 237 end
238 238
239 239 def test_default_sort
240 240 q = Query.new
241 241 assert_equal [], q.sort_criteria
242 242 end
243 243
244 244 def test_set_sort_criteria_with_hash
245 245 q = Query.new
246 246 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
247 247 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
248 248 end
249 249
250 250 def test_set_sort_criteria_with_array
251 251 q = Query.new
252 252 q.sort_criteria = [['priority', 'desc'], 'tracker']
253 253 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
254 254 end
255 255
256 256 def test_create_query_with_sort
257 257 q = Query.new(:name => 'Sorted')
258 258 q.sort_criteria = [['priority', 'desc'], 'tracker']
259 259 assert q.save
260 260 q.reload
261 261 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
262 262 end
263 263
264 264 def test_sort_by_string_custom_field_asc
265 265 q = Query.new
266 266 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
267 267 assert c
268 268 assert c.sortable
269 269 issues = Issue.find :all,
270 270 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
271 271 :conditions => q.statement,
272 272 :order => "#{c.sortable} ASC"
273 273 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
274 274 assert !values.empty?
275 275 assert_equal values.sort, values
276 276 end
277 277
278 278 def test_sort_by_string_custom_field_desc
279 279 q = Query.new
280 280 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
281 281 assert c
282 282 assert c.sortable
283 283 issues = Issue.find :all,
284 284 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
285 285 :conditions => q.statement,
286 286 :order => "#{c.sortable} DESC"
287 287 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
288 288 assert !values.empty?
289 289 assert_equal values.sort.reverse, values
290 290 end
291 291
292 292 def test_sort_by_float_custom_field_asc
293 293 q = Query.new
294 294 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
295 295 assert c
296 296 assert c.sortable
297 297 issues = Issue.find :all,
298 298 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
299 299 :conditions => q.statement,
300 300 :order => "#{c.sortable} ASC"
301 301 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
302 302 assert !values.empty?
303 303 assert_equal values.sort, values
304 304 end
305 305
306 306 def test_invalid_query_should_raise_query_statement_invalid_error
307 307 q = Query.new
308 308 assert_raise Query::StatementInvalid do
309 309 q.issues(:conditions => "foo = 1")
310 310 end
311 311 end
312 312
313 313 def test_issue_count_by_association_group
314 314 q = Query.new(:name => '_', :group_by => 'assigned_to')
315 315 count_by_group = q.issue_count_by_group
316 316 assert_kind_of Hash, count_by_group
317 317 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
318 318 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
319 319 assert count_by_group.has_key?(User.find(3))
320 320 end
321 321
322 322 def test_issue_count_by_list_custom_field_group
323 323 q = Query.new(:name => '_', :group_by => 'cf_1')
324 324 count_by_group = q.issue_count_by_group
325 325 assert_kind_of Hash, count_by_group
326 326 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
327 327 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
328 328 assert count_by_group.has_key?('MySQL')
329 329 end
330 330
331 331 def test_issue_count_by_date_custom_field_group
332 332 q = Query.new(:name => '_', :group_by => 'cf_8')
333 333 count_by_group = q.issue_count_by_group
334 334 assert_kind_of Hash, count_by_group
335 335 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
336 336 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
337 337 end
338 338
339 339 def test_label_for
340 340 q = Query.new
341 341 assert_equal 'assigned_to', q.label_for('assigned_to_id')
342 342 end
343 343
344 344 def test_editable_by
345 345 admin = User.find(1)
346 346 manager = User.find(2)
347 347 developer = User.find(3)
348 348
349 349 # Public query on project 1
350 350 q = Query.find(1)
351 351 assert q.editable_by?(admin)
352 352 assert q.editable_by?(manager)
353 353 assert !q.editable_by?(developer)
354 354
355 355 # Private query on project 1
356 356 q = Query.find(2)
357 357 assert q.editable_by?(admin)
358 358 assert !q.editable_by?(manager)
359 359 assert q.editable_by?(developer)
360 360
361 361 # Private query for all projects
362 362 q = Query.find(3)
363 363 assert q.editable_by?(admin)
364 364 assert !q.editable_by?(manager)
365 365 assert q.editable_by?(developer)
366 366
367 367 # Public query for all projects
368 368 q = Query.find(4)
369 369 assert q.editable_by?(admin)
370 370 assert !q.editable_by?(manager)
371 371 assert !q.editable_by?(developer)
372 372 end
373 373
374 374 context "#available_filters" do
375 375 setup do
376 376 @query = Query.new(:name => "_")
377 377 end
378 378
379 379 should "include users of visible projects in cross-project view" do
380 380 users = @query.available_filters["assigned_to_id"]
381 381 assert_not_nil users
382 382 assert users[:values].map{|u|u[1]}.include?("3")
383 383 end
384 384
385 385 context "'member_of_group' filter" do
386 386 should "be present" do
387 387 assert @query.available_filters.keys.include?("member_of_group")
388 388 end
389 389
390 390 should "be an optional list" do
391 391 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
392 392 end
393 393
394 394 should "have a list of the groups as values" do
395 395 Group.destroy_all # No fixtures
396 396 group1 = Group.generate!.reload
397 397 group2 = Group.generate!.reload
398 398
399 399 expected_group_list = [
400 [group1.name, group1.id],
401 [group2.name, group2.id]
400 [group1.name, group1.id.to_s],
401 [group2.name, group2.id.to_s]
402 402 ]
403 403 assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort
404 404 end
405 405
406 406 end
407 407
408 408 context "'assigned_to_role' filter" do
409 409 should "be present" do
410 410 assert @query.available_filters.keys.include?("assigned_to_role")
411 411 end
412 412
413 413 should "be an optional list" do
414 414 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
415 415 end
416 416
417 417 should "have a list of the Roles as values" do
418 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager',1])
419 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer',2])
420 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter',3])
418 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
419 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
420 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
421 421 end
422 422
423 423 should "not include the built in Roles as values" do
424 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member',4])
425 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous',5])
424 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
425 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
426 426 end
427 427
428 428 end
429 429
430 430 end
431 431
432 432 context "#statement" do
433 433 context "with 'member_of_group' filter" do
434 434 setup do
435 435 Group.destroy_all # No fixtures
436 436 @user_in_group = User.generate!
437 437 @second_user_in_group = User.generate!
438 438 @user_in_group2 = User.generate!
439 439 @user_not_in_group = User.generate!
440 440
441 441 @group = Group.generate!.reload
442 442 @group.users << @user_in_group
443 443 @group.users << @second_user_in_group
444 444
445 445 @group2 = Group.generate!.reload
446 446 @group2.users << @user_in_group2
447 447
448 448 end
449 449
450 450 should "search assigned to for users in the group" do
451 451 @query = Query.new(:name => '_')
452 452 @query.add_filter('member_of_group', '=', [@group.id.to_s])
453 453
454 454 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
455 455 assert_find_issues_with_query_is_successful @query
456 456 end
457 457
458 458 should "search not assigned to any group member (none)" do
459 459 @query = Query.new(:name => '_')
460 460 @query.add_filter('member_of_group', '!*', [''])
461 461
462 462 # Users not in a group
463 463 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}')"
464 464 assert_find_issues_with_query_is_successful @query
465 465
466 466 end
467 467
468 468 should "search assigned to any group member (all)" do
469 469 @query = Query.new(:name => '_')
470 470 @query.add_filter('member_of_group', '*', [''])
471 471
472 472 # Only users in a group
473 473 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}')"
474 474 assert_find_issues_with_query_is_successful @query
475 475
476 476 end
477 477 end
478 478
479 479 context "with 'assigned_to_role' filter" do
480 480 setup do
481 481 # No fixtures
482 482 MemberRole.delete_all
483 483 Member.delete_all
484 484 Role.delete_all
485 485
486 486 @manager_role = Role.generate!(:name => 'Manager')
487 487 @developer_role = Role.generate!(:name => 'Developer')
488 488
489 489 @project = Project.generate!
490 490 @manager = User.generate!
491 491 @developer = User.generate!
492 492 @boss = User.generate!
493 493 User.add_to_project(@manager, @project, @manager_role)
494 494 User.add_to_project(@developer, @project, @developer_role)
495 495 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
496 496 end
497 497
498 498 should "search assigned to for users with the Role" do
499 499 @query = Query.new(:name => '_')
500 500 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
501 501
502 502 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@manager.id}','#{@boss.id}')"
503 503 assert_find_issues_with_query_is_successful @query
504 504 end
505 505
506 506 should "search assigned to for users not assigned to any Role (none)" do
507 507 @query = Query.new(:name => '_')
508 508 @query.add_filter('assigned_to_role', '!*', [''])
509 509
510 510 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}')"
511 511 assert_find_issues_with_query_is_successful @query
512 512 end
513 513
514 514 should "search assigned to for users assigned to any Role (all)" do
515 515 @query = Query.new(:name => '_')
516 516 @query.add_filter('assigned_to_role', '*', [''])
517 517
518 518 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@manager.id}','#{@developer.id}','#{@boss.id}')"
519 519 assert_find_issues_with_query_is_successful @query
520 520 end
521 521 end
522 522 end
523 523
524 524 end
General Comments 0
You need to be logged in to leave comments. Login now