##// END OF EJS Templates
Filter out illegal values to prevent raise of PostgreSQL exceptions, restored last test part removed with r7625 (#8371)....
Etienne Massip -
r7508:33f65c5a4845
parent child
Show More
@@ -1,790 +1,796
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 validate :validate_query_filters
96 96
97 97 @@operators = { "=" => :label_equals,
98 98 "!" => :label_not_equals,
99 99 "o" => :label_open_issues,
100 100 "c" => :label_closed_issues,
101 101 "!*" => :label_none,
102 102 "*" => :label_all,
103 103 ">=" => :label_greater_or_equal,
104 104 "<=" => :label_less_or_equal,
105 105 "><" => :label_between,
106 106 "<t+" => :label_in_less_than,
107 107 ">t+" => :label_in_more_than,
108 108 "t+" => :label_in,
109 109 "t" => :label_today,
110 110 "w" => :label_this_week,
111 111 ">t-" => :label_less_than_ago,
112 112 "<t-" => :label_more_than_ago,
113 113 "t-" => :label_ago,
114 114 "~" => :label_contains,
115 115 "!~" => :label_not_contains }
116 116
117 117 cattr_reader :operators
118 118
119 119 @@operators_by_filter_type = { :list => [ "=", "!" ],
120 120 :list_status => [ "o", "=", "!", "c", "*" ],
121 121 :list_optional => [ "=", "!", "!*", "*" ],
122 122 :list_subprojects => [ "*", "!*", "=" ],
123 123 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
124 124 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w" ],
125 125 :string => [ "=", "~", "!", "!~" ],
126 126 :text => [ "~", "!~" ],
127 127 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
128 128 :float => [ "=", ">=", "<=", "><", "!*", "*" ] }
129 129
130 130 cattr_reader :operators_by_filter_type
131 131
132 132 @@available_columns = [
133 133 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
134 134 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
135 135 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
136 136 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
137 137 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
138 138 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
139 139 QueryColumn.new(:author),
140 140 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
141 141 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
142 142 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
143 143 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
144 144 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
145 145 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
146 146 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
147 147 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
148 148 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
149 149 ]
150 150 cattr_reader :available_columns
151 151
152 152 named_scope :visible, lambda {|*args|
153 153 user = args.shift || User.current
154 154 base = Project.allowed_to_condition(user, :view_issues, *args)
155 155 user_id = user.logged? ? user.id : 0
156 156 {
157 157 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
158 158 :include => :project
159 159 }
160 160 }
161 161
162 162 def initialize(attributes = nil)
163 163 super attributes
164 164 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
165 165 end
166 166
167 167 def after_initialize
168 168 # Store the fact that project is nil (used in #editable_by?)
169 169 @is_for_all = project.nil?
170 170 end
171 171
172 172 def validate_query_filters
173 173 filters.each_key do |field|
174 174 if values_for(field)
175 175 case type_for(field)
176 176 when :integer
177 177 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
178 178 when :float
179 179 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+(\.\d*)?$/) }
180 180 when :date, :date_past
181 181 case operator_for(field)
182 182 when "=", ">=", "<=", "><"
183 183 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?) }
184 184 when ">t-", "<t-", "t-"
185 185 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
186 186 end
187 187 end
188 188 end
189 189
190 190 errors.add label_for(field), :blank unless
191 191 # filter requires one or more values
192 192 (values_for(field) and !values_for(field).first.blank?) or
193 193 # filter doesn't require any value
194 194 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
195 195 end if filters
196 196 end
197 197
198 198 # Returns true if the query is visible to +user+ or the current user.
199 199 def visible?(user=User.current)
200 200 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
201 201 end
202 202
203 203 def editable_by?(user)
204 204 return false unless user
205 205 # Admin can edit them all and regular users can edit their private queries
206 206 return true if user.admin? || (!is_public && self.user_id == user.id)
207 207 # Members can not edit public queries that are for all project (only admin is allowed to)
208 208 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
209 209 end
210 210
211 211 def available_filters
212 212 return @available_filters if @available_filters
213 213
214 214 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
215 215
216 216 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
217 217 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
218 218 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
219 219 "subject" => { :type => :text, :order => 8 },
220 220 "created_on" => { :type => :date_past, :order => 9 },
221 221 "updated_on" => { :type => :date_past, :order => 10 },
222 222 "start_date" => { :type => :date, :order => 11 },
223 223 "due_date" => { :type => :date, :order => 12 },
224 224 "estimated_hours" => { :type => :float, :order => 13 },
225 225 "done_ratio" => { :type => :integer, :order => 14 }}
226 226
227 227 principals = []
228 228 if project
229 229 principals += project.principals.sort
230 230 else
231 231 all_projects = Project.visible.all
232 232 if all_projects.any?
233 233 # members of visible projects
234 234 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
235 235
236 236 # project filter
237 237 project_values = []
238 238 Project.project_tree(all_projects) do |p, level|
239 239 prefix = (level > 0 ? ('--' * level + ' ') : '')
240 240 project_values << ["#{prefix}#{p.name}", p.id.to_s]
241 241 end
242 242 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
243 243 end
244 244 end
245 245 users = principals.select {|p| p.is_a?(User)}
246 246
247 247 assigned_to_values = []
248 248 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
249 249 assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
250 250 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => assigned_to_values } unless assigned_to_values.empty?
251 251
252 252 author_values = []
253 253 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
254 254 author_values += users.collect{|s| [s.name, s.id.to_s] }
255 255 @available_filters["author_id"] = { :type => :list, :order => 5, :values => author_values } unless author_values.empty?
256 256
257 257 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
258 258 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
259 259
260 260 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
261 261 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
262 262
263 263 if User.current.logged?
264 264 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
265 265 end
266 266
267 267 if project
268 268 # project specific filters
269 269 categories = @project.issue_categories.all
270 270 unless categories.empty?
271 271 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
272 272 end
273 273 versions = @project.shared_versions.all
274 274 unless versions.empty?
275 275 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
276 276 end
277 277 unless @project.leaf?
278 278 subprojects = @project.descendants.visible.all
279 279 unless subprojects.empty?
280 280 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
281 281 end
282 282 end
283 283 add_custom_fields_filters(@project.all_issue_custom_fields)
284 284 else
285 285 # global filters for cross project issue list
286 286 system_shared_versions = Version.visible.find_all_by_sharing('system')
287 287 unless system_shared_versions.empty?
288 288 @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] } }
289 289 end
290 290 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
291 291 end
292 292 @available_filters
293 293 end
294 294
295 295 def add_filter(field, operator, values)
296 296 # values must be an array
297 297 return unless values.nil? || values.is_a?(Array)
298 298 # check if field is defined as an available filter
299 299 if available_filters.has_key? field
300 300 filter_options = available_filters[field]
301 301 # check if operator is allowed for that filter
302 302 #if @@operators_by_filter_type[filter_options[:type]].include? operator
303 303 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
304 304 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
305 305 #end
306 306 filters[field] = {:operator => operator, :values => (values || [''])}
307 307 end
308 308 end
309 309
310 310 def add_short_filter(field, expression)
311 311 return unless expression && available_filters.has_key?(field)
312 312 field_type = available_filters[field][:type]
313 313 @@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
314 314 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
315 315 add_filter field, operator, $1.present? ? $1.split('|') : ['']
316 316 end || add_filter(field, '=', expression.split('|'))
317 317 end
318 318
319 319 # Add multiple filters using +add_filter+
320 320 def add_filters(fields, operators, values)
321 321 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
322 322 fields.each do |field|
323 323 add_filter(field, operators[field], values && values[field])
324 324 end
325 325 end
326 326 end
327 327
328 328 def has_filter?(field)
329 329 filters and filters[field]
330 330 end
331 331
332 332 def type_for(field)
333 333 available_filters[field][:type] if available_filters.has_key?(field)
334 334 end
335 335
336 def allowed_values_for(field)
337 available_filters[field][:values] if available_filters.has_key?(field)
338 end
339
336 340 def operator_for(field)
337 341 has_filter?(field) ? filters[field][:operator] : nil
338 342 end
339 343
340 344 def values_for(field)
341 345 has_filter?(field) ? filters[field][:values] : nil
342 346 end
343 347
344 348 def value_for(field, index=0)
345 349 (values_for(field) || [])[index]
346 350 end
347 351
348 352 def label_for(field)
349 353 label = available_filters[field][:name] if available_filters.has_key?(field)
350 354 label ||= field.gsub(/\_id$/, "")
351 355 end
352 356
353 357 def available_columns
354 358 return @available_columns if @available_columns
355 359 @available_columns = Query.available_columns
356 360 @available_columns += (project ?
357 361 project.all_issue_custom_fields :
358 362 IssueCustomField.find(:all)
359 363 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
360 364 end
361 365
362 366 def self.available_columns=(v)
363 367 self.available_columns = (v)
364 368 end
365 369
366 370 def self.add_available_column(column)
367 371 self.available_columns << (column) if column.is_a?(QueryColumn)
368 372 end
369 373
370 374 # Returns an array of columns that can be used to group the results
371 375 def groupable_columns
372 376 available_columns.select {|c| c.groupable}
373 377 end
374 378
375 379 # Returns a Hash of columns and the key for sorting
376 380 def sortable_columns
377 381 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
378 382 h[column.name.to_s] = column.sortable
379 383 h
380 384 })
381 385 end
382 386
383 387 def columns
384 388 # preserve the column_names order
385 389 (has_default_columns? ? default_columns_names : column_names).collect do |name|
386 390 available_columns.find { |col| col.name == name }
387 391 end.compact
388 392 end
389 393
390 394 def default_columns_names
391 395 @default_columns_names ||= begin
392 396 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
393 397
394 398 project.present? ? default_columns : [:project] | default_columns
395 399 end
396 400 end
397 401
398 402 def column_names=(names)
399 403 if names
400 404 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
401 405 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
402 406 # Set column_names to nil if default columns
403 407 if names == default_columns_names
404 408 names = nil
405 409 end
406 410 end
407 411 write_attribute(:column_names, names)
408 412 end
409 413
410 414 def has_column?(column)
411 415 column_names && column_names.include?(column.name)
412 416 end
413 417
414 418 def has_default_columns?
415 419 column_names.nil? || column_names.empty?
416 420 end
417 421
418 422 def sort_criteria=(arg)
419 423 c = []
420 424 if arg.is_a?(Hash)
421 425 arg = arg.keys.sort.collect {|k| arg[k]}
422 426 end
423 427 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
424 428 write_attribute(:sort_criteria, c)
425 429 end
426 430
427 431 def sort_criteria
428 432 read_attribute(:sort_criteria) || []
429 433 end
430 434
431 435 def sort_criteria_key(arg)
432 436 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
433 437 end
434 438
435 439 def sort_criteria_order(arg)
436 440 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
437 441 end
438 442
439 443 # Returns the SQL sort order that should be prepended for grouping
440 444 def group_by_sort_order
441 445 if grouped? && (column = group_by_column)
442 446 column.sortable.is_a?(Array) ?
443 447 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
444 448 "#{column.sortable} #{column.default_order}"
445 449 end
446 450 end
447 451
448 452 # Returns true if the query is a grouped query
449 453 def grouped?
450 454 !group_by_column.nil?
451 455 end
452 456
453 457 def group_by_column
454 458 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
455 459 end
456 460
457 461 def group_by_statement
458 462 group_by_column.try(:groupable)
459 463 end
460 464
461 465 def project_statement
462 466 project_clauses = []
463 467 if project && !@project.descendants.active.empty?
464 468 ids = [project.id]
465 469 if has_filter?("subproject_id")
466 470 case operator_for("subproject_id")
467 471 when '='
468 472 # include the selected subprojects
469 473 ids += values_for("subproject_id").each(&:to_i)
470 474 when '!*'
471 475 # main project only
472 476 else
473 477 # all subprojects
474 478 ids += project.descendants.collect(&:id)
475 479 end
476 480 elsif Setting.display_subprojects_issues?
477 481 ids += project.descendants.collect(&:id)
478 482 end
479 483 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
480 484 elsif project
481 485 project_clauses << "#{Project.table_name}.id = %d" % project.id
482 486 end
483 487 project_clauses.any? ? project_clauses.join(' AND ') : nil
484 488 end
485 489
486 490 def statement
487 491 # filters clauses
488 492 filters_clauses = []
489 493 filters.each_key do |field|
490 494 next if field == "subproject_id"
491 495 v = values_for(field).clone
492 496 next unless v and !v.empty?
493 497 operator = operator_for(field)
494 498
495 499 # "me" value subsitution
496 500 if %w(assigned_to_id author_id watcher_id).include?(field)
497 501 if v.delete("me")
498 502 if User.current.logged?
499 503 v.push(User.current.id.to_s)
500 504 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
501 505 else
502 506 v.push("0")
503 507 end
504 508 end
505 509 end
506 510
507 511 if field =~ /^cf_(\d+)$/
508 512 # custom field
509 513 filters_clauses << sql_for_custom_field(field, operator, v, $1)
510 514 elsif respond_to?("sql_for_#{field}_field")
511 515 # specific statement
512 516 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
513 517 else
514 518 # regular field
515 519 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
516 520 end
517 521 end if filters and valid?
518 522
519 523 filters_clauses << project_statement
520 524 filters_clauses.reject!(&:blank?)
521 525
522 526 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
523 527 end
524 528
525 529 # Returns the issue count
526 530 def issue_count
527 531 Issue.visible.count(:include => [:status, :project], :conditions => statement)
528 532 rescue ::ActiveRecord::StatementInvalid => e
529 533 raise StatementInvalid.new(e.message)
530 534 end
531 535
532 536 # Returns the issue count by group or nil if query is not grouped
533 537 def issue_count_by_group
534 538 r = nil
535 539 if grouped?
536 540 begin
537 541 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
538 542 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
539 543 rescue ActiveRecord::RecordNotFound
540 544 r = {nil => issue_count}
541 545 end
542 546 c = group_by_column
543 547 if c.is_a?(QueryCustomFieldColumn)
544 548 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
545 549 end
546 550 end
547 551 r
548 552 rescue ::ActiveRecord::StatementInvalid => e
549 553 raise StatementInvalid.new(e.message)
550 554 end
551 555
552 556 # Returns the issues
553 557 # Valid options are :order, :offset, :limit, :include, :conditions
554 558 def issues(options={})
555 559 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
556 560 order_option = nil if order_option.blank?
557 561
558 562 Issue.visible.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
559 563 :conditions => Query.merge_conditions(statement, options[:conditions]),
560 564 :order => order_option,
561 565 :limit => options[:limit],
562 566 :offset => options[:offset]
563 567 rescue ::ActiveRecord::StatementInvalid => e
564 568 raise StatementInvalid.new(e.message)
565 569 end
566 570
567 571 # Returns the journals
568 572 # Valid options are :order, :offset, :limit
569 573 def journals(options={})
570 574 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
571 575 :conditions => statement,
572 576 :order => options[:order],
573 577 :limit => options[:limit],
574 578 :offset => options[:offset]
575 579 rescue ::ActiveRecord::StatementInvalid => e
576 580 raise StatementInvalid.new(e.message)
577 581 end
578 582
579 583 # Returns the versions
580 584 # Valid options are :conditions
581 585 def versions(options={})
582 586 Version.visible.find :all, :include => :project,
583 587 :conditions => Query.merge_conditions(project_statement, options[:conditions])
584 588 rescue ::ActiveRecord::StatementInvalid => e
585 589 raise StatementInvalid.new(e.message)
586 590 end
587 591
588 592 def sql_for_watcher_id_field(field, operator, value)
589 593 db_table = Watcher.table_name
590 594 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
591 595 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
592 596 end
593 597
594 598 def sql_for_member_of_group_field(field, operator, value)
595 599 if operator == '*' # Any group
596 600 groups = Group.all
597 601 operator = '=' # Override the operator since we want to find by assigned_to
598 602 elsif operator == "!*"
599 603 groups = Group.all
600 604 operator = '!' # Override the operator since we want to find by assigned_to
601 605 else
602 606 groups = Group.find_all_by_id(value)
603 607 end
604 608 groups ||= []
605 609
606 610 members_of_groups = groups.inject([]) {|user_ids, group|
607 611 if group && group.user_ids.present?
608 612 user_ids << group.user_ids
609 613 end
610 614 user_ids.flatten.uniq.compact
611 615 }.sort.collect(&:to_s)
612 616
613 617 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
614 618 end
615 619
616 620 def sql_for_assigned_to_role_field(field, operator, value)
617 621 if operator == "*" # Any Role
618 622 roles = Role.givable
619 623 operator = '=' # Override the operator since we want to find by assigned_to
620 624 elsif operator == "!*" # No role
621 625 roles = Role.givable
622 626 operator = '!' # Override the operator since we want to find by assigned_to
623 627 else
624 628 roles = Role.givable.find_all_by_id(value)
625 629 end
626 630 roles ||= []
627 631
628 632 members_of_roles = roles.inject([]) {|user_ids, role|
629 633 if role && role.members
630 634 user_ids << role.members.collect(&:user_id)
631 635 end
632 636 user_ids.flatten.uniq.compact
633 637 }.sort.collect(&:to_s)
634 638
635 639 '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
636 640 end
637 641
638 642 private
639 643
640 644 def sql_for_custom_field(field, operator, value, custom_field_id)
641 645 db_table = CustomValue.table_name
642 646 db_field = 'value'
643 647 "#{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 " +
644 648 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
645 649 end
646 650
647 651 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
648 652 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
649 653 sql = ''
650 654 case operator
651 655 when "="
656 value &= allowed_values_for(field).collect {|val| val[1]} if value.present? && allowed_values_for(field).present?
652 657 if value.any?
653 658 case type_for(field)
654 659 when :date, :date_past
655 660 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
656 661 when :integer
657 662 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
658 663 when :float
659 664 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
660 665 else
661 666 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
662 667 end
663 668 else
664 669 # IN an empty set
665 670 sql = "1=0"
666 671 end
667 672 when "!"
673 value &= allowed_values_for(field).collect {|val| val[1]} if value.present? && allowed_values_for(field).present?
668 674 if value.any?
669 675 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
670 676 else
671 677 # NOT IN an empty set
672 678 sql = "1=1"
673 679 end
674 680 when "!*"
675 681 sql = "#{db_table}.#{db_field} IS NULL"
676 682 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
677 683 when "*"
678 684 sql = "#{db_table}.#{db_field} IS NOT NULL"
679 685 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
680 686 when ">="
681 687 if [:date, :date_past].include?(type_for(field))
682 688 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
683 689 else
684 690 if is_custom_filter
685 691 sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f}"
686 692 else
687 693 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
688 694 end
689 695 end
690 696 when "<="
691 697 if [:date, :date_past].include?(type_for(field))
692 698 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
693 699 else
694 700 if is_custom_filter
695 701 sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f}"
696 702 else
697 703 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
698 704 end
699 705 end
700 706 when "><"
701 707 if [:date, :date_past].include?(type_for(field))
702 708 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
703 709 else
704 710 if is_custom_filter
705 711 sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
706 712 else
707 713 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
708 714 end
709 715 end
710 716 when "o"
711 717 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
712 718 when "c"
713 719 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
714 720 when ">t-"
715 721 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
716 722 when "<t-"
717 723 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
718 724 when "t-"
719 725 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
720 726 when ">t+"
721 727 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
722 728 when "<t+"
723 729 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
724 730 when "t+"
725 731 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
726 732 when "t"
727 733 sql = relative_date_clause(db_table, db_field, 0, 0)
728 734 when "w"
729 735 first_day_of_week = l(:general_first_day_of_week).to_i
730 736 day_of_week = Date.today.cwday
731 737 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
732 738 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
733 739 when "~"
734 740 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
735 741 when "!~"
736 742 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
737 743 else
738 744 raise "Unknown query operator #{operator}"
739 745 end
740 746
741 747 return sql
742 748 end
743 749
744 750 def add_custom_fields_filters(custom_fields)
745 751 @available_filters ||= {}
746 752
747 753 custom_fields.select(&:is_filter?).each do |field|
748 754 case field.field_format
749 755 when "text"
750 756 options = { :type => :text, :order => 20 }
751 757 when "list"
752 758 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
753 759 when "date"
754 760 options = { :type => :date, :order => 20 }
755 761 when "bool"
756 762 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
757 763 when "int"
758 764 options = { :type => :integer, :order => 20 }
759 765 when "float"
760 766 options = { :type => :float, :order => 20 }
761 767 when "user", "version"
762 768 next unless project
763 769 options = { :type => :list_optional, :values => field.possible_values_options(project), :order => 20}
764 770 else
765 771 options = { :type => :string, :order => 20 }
766 772 end
767 773 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
768 774 end
769 775 end
770 776
771 777 # Returns a SQL clause for a date or datetime field.
772 778 def date_clause(table, field, from, to)
773 779 s = []
774 780 if from
775 781 from_yesterday = from - 1
776 782 from_yesterday_utc = Time.gm(from_yesterday.year, from_yesterday.month, from_yesterday.day)
777 783 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_utc.end_of_day)])
778 784 end
779 785 if to
780 786 to_utc = Time.gm(to.year, to.month, to.day)
781 787 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_utc.end_of_day)])
782 788 end
783 789 s.join(' AND ')
784 790 end
785 791
786 792 # Returns a SQL clause for a date or datetime field using relative dates.
787 793 def relative_date_clause(table, field, days_from, days_to)
788 794 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
789 795 end
790 796 end
@@ -1,1707 +1,1709
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 require 'issues_controller'
20 20
21 21 class IssuesControllerTest < ActionController::TestCase
22 22 fixtures :projects,
23 23 :users,
24 24 :roles,
25 25 :members,
26 26 :member_roles,
27 27 :issues,
28 28 :issue_statuses,
29 29 :versions,
30 30 :trackers,
31 31 :projects_trackers,
32 32 :issue_categories,
33 33 :enabled_modules,
34 34 :enumerations,
35 35 :attachments,
36 36 :workflows,
37 37 :custom_fields,
38 38 :custom_values,
39 39 :custom_fields_projects,
40 40 :custom_fields_trackers,
41 41 :time_entries,
42 42 :journals,
43 43 :journal_details,
44 44 :queries
45 45
46 46 def setup
47 47 @controller = IssuesController.new
48 48 @request = ActionController::TestRequest.new
49 49 @response = ActionController::TestResponse.new
50 50 User.current = nil
51 51 end
52 52
53 53 def test_index
54 54 Setting.default_language = 'en'
55 55
56 56 get :index
57 57 assert_response :success
58 58 assert_template 'index'
59 59 assert_not_nil assigns(:issues)
60 60 assert_nil assigns(:project)
61 61 assert_tag :tag => 'a', :content => /Can't print recipes/
62 62 assert_tag :tag => 'a', :content => /Subproject issue/
63 63 # private projects hidden
64 64 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
65 65 assert_no_tag :tag => 'a', :content => /Issue on project 2/
66 66 # project column
67 67 assert_tag :tag => 'th', :content => /Project/
68 68 end
69 69
70 70 def test_index_should_not_list_issues_when_module_disabled
71 71 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
72 72 get :index
73 73 assert_response :success
74 74 assert_template 'index'
75 75 assert_not_nil assigns(:issues)
76 76 assert_nil assigns(:project)
77 77 assert_no_tag :tag => 'a', :content => /Can't print recipes/
78 78 assert_tag :tag => 'a', :content => /Subproject issue/
79 79 end
80 80
81 81 def test_index_should_list_visible_issues_only
82 82 get :index, :per_page => 100
83 83 assert_response :success
84 84 assert_not_nil assigns(:issues)
85 85 assert_nil assigns(:issues).detect {|issue| !issue.visible?}
86 86 end
87 87
88 88 def test_index_with_project
89 89 Setting.display_subprojects_issues = 0
90 90 get :index, :project_id => 1
91 91 assert_response :success
92 92 assert_template 'index'
93 93 assert_not_nil assigns(:issues)
94 94 assert_tag :tag => 'a', :content => /Can't print recipes/
95 95 assert_no_tag :tag => 'a', :content => /Subproject issue/
96 96 end
97 97
98 98 def test_index_with_project_and_subprojects
99 99 Setting.display_subprojects_issues = 1
100 100 get :index, :project_id => 1
101 101 assert_response :success
102 102 assert_template 'index'
103 103 assert_not_nil assigns(:issues)
104 104 assert_tag :tag => 'a', :content => /Can't print recipes/
105 105 assert_tag :tag => 'a', :content => /Subproject issue/
106 106 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
107 107 end
108 108
109 109 def test_index_with_project_and_subprojects_should_show_private_subprojects
110 110 @request.session[:user_id] = 2
111 111 Setting.display_subprojects_issues = 1
112 112 get :index, :project_id => 1
113 113 assert_response :success
114 114 assert_template 'index'
115 115 assert_not_nil assigns(:issues)
116 116 assert_tag :tag => 'a', :content => /Can't print recipes/
117 117 assert_tag :tag => 'a', :content => /Subproject issue/
118 118 assert_tag :tag => 'a', :content => /Issue of a private subproject/
119 119 end
120 120
121 121 def test_index_with_project_and_default_filter
122 122 get :index, :project_id => 1, :set_filter => 1
123 123 assert_response :success
124 124 assert_template 'index'
125 125 assert_not_nil assigns(:issues)
126 126
127 127 query = assigns(:query)
128 128 assert_not_nil query
129 129 # default filter
130 130 assert_equal({'status_id' => {:operator => 'o', :values => ['']}}, query.filters)
131 131 end
132 132
133 133 def test_index_with_project_and_filter
134 134 get :index, :project_id => 1, :set_filter => 1,
135 135 :f => ['tracker_id'],
136 136 :op => {'tracker_id' => '='},
137 137 :v => {'tracker_id' => ['1']}
138 138 assert_response :success
139 139 assert_template 'index'
140 140 assert_not_nil assigns(:issues)
141 141
142 142 query = assigns(:query)
143 143 assert_not_nil query
144 144 assert_equal({'tracker_id' => {:operator => '=', :values => ['1']}}, query.filters)
145 145 end
146 146
147 147 def test_index_with_short_filters
148 148
149 149 to_test = {
150 150 'status_id' => {
151 151 'o' => { :op => 'o', :values => [''] },
152 152 'c' => { :op => 'c', :values => [''] },
153 153 '7' => { :op => '=', :values => ['7'] },
154 154 '7|3|4' => { :op => '=', :values => ['7', '3', '4'] },
155 155 '=7' => { :op => '=', :values => ['7'] },
156 156 '!3' => { :op => '!', :values => ['3'] },
157 157 '!7|3|4' => { :op => '!', :values => ['7', '3', '4'] }},
158 158 'subject' => {
159 159 'This is a subject' => { :op => '=', :values => ['This is a subject'] },
160 160 'o' => { :op => '=', :values => ['o'] },
161 161 '~This is part of a subject' => { :op => '~', :values => ['This is part of a subject'] },
162 162 '!~This is part of a subject' => { :op => '!~', :values => ['This is part of a subject'] }},
163 163 'tracker_id' => {
164 164 '3' => { :op => '=', :values => ['3'] },
165 '=3' => { :op => '=', :values => ['3'] }},
165 '=3' => { :op => '=', :values => ['3'] },
166 '*' => { :op => '=', :values => ['*'] },
167 '!*' => { :op => '!', :values => ['*'] }},
166 168 'start_date' => {
167 169 '2011-10-12' => { :op => '=', :values => ['2011-10-12'] },
168 170 '=2011-10-12' => { :op => '=', :values => ['2011-10-12'] },
169 171 '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] },
170 172 '<=2011-10-12' => { :op => '<=', :values => ['2011-10-12'] },
171 173 '><2011-10-01|2011-10-30' => { :op => '><', :values => ['2011-10-01', '2011-10-30'] },
172 174 '<t+2' => { :op => '<t+', :values => ['2'] },
173 175 '>t+2' => { :op => '>t+', :values => ['2'] },
174 176 't+2' => { :op => 't+', :values => ['2'] },
175 177 't' => { :op => 't', :values => [''] },
176 178 'w' => { :op => 'w', :values => [''] },
177 179 '>t-2' => { :op => '>t-', :values => ['2'] },
178 180 '<t-2' => { :op => '<t-', :values => ['2'] },
179 181 't-2' => { :op => 't-', :values => ['2'] }},
180 182 'created_on' => {
181 183 '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] },
182 184 '<t+2' => { :op => '=', :values => ['<t+2'] },
183 185 '>t+2' => { :op => '=', :values => ['>t+2'] },
184 186 't+2' => { :op => 't', :values => ['+2'] }},
185 187 'cf_1' => {
186 188 'c' => { :op => '=', :values => ['c'] },
187 189 '!c' => { :op => '!', :values => ['c'] },
188 190 '!*' => { :op => '!*', :values => [''] },
189 191 '*' => { :op => '*', :values => [''] }},
190 192 'estimated_hours' => {
191 193 '=13.4' => { :op => '=', :values => ['13.4'] },
192 194 '>=45' => { :op => '>=', :values => ['45'] },
193 195 '<=125' => { :op => '<=', :values => ['125'] },
194 196 '><10.5|20.5' => { :op => '><', :values => ['10.5', '20.5'] },
195 197 '!*' => { :op => '!*', :values => [''] },
196 198 '*' => { :op => '*', :values => [''] }}
197 199 }
198 200
199 201 default_filter = { 'status_id' => {:operator => 'o', :values => [''] }}
200 202
201 203 to_test.each do |field, expression_and_expected|
202 204 expression_and_expected.each do |filter_expression, expected|
203 205
204 206 get :index, :set_filter => 1, field => filter_expression
205 207
206 208 assert_response :success
207 209 assert_template 'index'
208 210 assert_not_nil assigns(:issues)
209 211
210 212 query = assigns(:query)
211 213 assert_not_nil query
212 214 assert query.has_filter?(field)
213 215 assert_equal(default_filter.merge({field => {:operator => expected[:op], :values => expected[:values]}}), query.filters)
214 216 end
215 217 end
216 218
217 219 end
218 220
219 221 def test_index_with_project_and_empty_filters
220 222 get :index, :project_id => 1, :set_filter => 1, :fields => ['']
221 223 assert_response :success
222 224 assert_template 'index'
223 225 assert_not_nil assigns(:issues)
224 226
225 227 query = assigns(:query)
226 228 assert_not_nil query
227 229 # no filter
228 230 assert_equal({}, query.filters)
229 231 end
230 232
231 233 def test_index_with_query
232 234 get :index, :project_id => 1, :query_id => 5
233 235 assert_response :success
234 236 assert_template 'index'
235 237 assert_not_nil assigns(:issues)
236 238 assert_nil assigns(:issue_count_by_group)
237 239 end
238 240
239 241 def test_index_with_query_grouped_by_tracker
240 242 get :index, :project_id => 1, :query_id => 6
241 243 assert_response :success
242 244 assert_template 'index'
243 245 assert_not_nil assigns(:issues)
244 246 assert_not_nil assigns(:issue_count_by_group)
245 247 end
246 248
247 249 def test_index_with_query_grouped_by_list_custom_field
248 250 get :index, :project_id => 1, :query_id => 9
249 251 assert_response :success
250 252 assert_template 'index'
251 253 assert_not_nil assigns(:issues)
252 254 assert_not_nil assigns(:issue_count_by_group)
253 255 end
254 256
255 257 def test_private_query_should_not_be_available_to_other_users
256 258 q = Query.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil)
257 259 @request.session[:user_id] = 3
258 260
259 261 get :index, :query_id => q.id
260 262 assert_response 403
261 263 end
262 264
263 265 def test_private_query_should_be_available_to_its_user
264 266 q = Query.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil)
265 267 @request.session[:user_id] = 2
266 268
267 269 get :index, :query_id => q.id
268 270 assert_response :success
269 271 end
270 272
271 273 def test_public_query_should_be_available_to_other_users
272 274 q = Query.create!(:name => "private", :user => User.find(2), :is_public => true, :project => nil)
273 275 @request.session[:user_id] = 3
274 276
275 277 get :index, :query_id => q.id
276 278 assert_response :success
277 279 end
278 280
279 281 def test_index_sort_by_field_not_included_in_columns
280 282 Setting.issue_list_default_columns = %w(subject author)
281 283 get :index, :sort => 'tracker'
282 284 end
283 285
284 286 def test_index_csv_with_project
285 287 Setting.default_language = 'en'
286 288
287 289 get :index, :format => 'csv'
288 290 assert_response :success
289 291 assert_not_nil assigns(:issues)
290 292 assert_equal 'text/csv', @response.content_type
291 293 assert @response.body.starts_with?("#,")
292 294
293 295 get :index, :project_id => 1, :format => 'csv'
294 296 assert_response :success
295 297 assert_not_nil assigns(:issues)
296 298 assert_equal 'text/csv', @response.content_type
297 299 end
298 300
299 301 def test_index_pdf
300 302 get :index, :format => 'pdf'
301 303 assert_response :success
302 304 assert_not_nil assigns(:issues)
303 305 assert_equal 'application/pdf', @response.content_type
304 306
305 307 get :index, :project_id => 1, :format => 'pdf'
306 308 assert_response :success
307 309 assert_not_nil assigns(:issues)
308 310 assert_equal 'application/pdf', @response.content_type
309 311
310 312 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
311 313 assert_response :success
312 314 assert_not_nil assigns(:issues)
313 315 assert_equal 'application/pdf', @response.content_type
314 316 end
315 317
316 318 def test_index_pdf_with_query_grouped_by_list_custom_field
317 319 get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
318 320 assert_response :success
319 321 assert_not_nil assigns(:issues)
320 322 assert_not_nil assigns(:issue_count_by_group)
321 323 assert_equal 'application/pdf', @response.content_type
322 324 end
323 325
324 326 def test_index_sort
325 327 get :index, :sort => 'tracker,id:desc'
326 328 assert_response :success
327 329
328 330 sort_params = @request.session['issues_index_sort']
329 331 assert sort_params.is_a?(String)
330 332 assert_equal 'tracker,id:desc', sort_params
331 333
332 334 issues = assigns(:issues)
333 335 assert_not_nil issues
334 336 assert !issues.empty?
335 337 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
336 338 end
337 339
338 340 def test_index_with_columns
339 341 columns = ['tracker', 'subject', 'assigned_to']
340 342 get :index, :set_filter => 1, :c => columns
341 343 assert_response :success
342 344
343 345 # query should use specified columns
344 346 query = assigns(:query)
345 347 assert_kind_of Query, query
346 348 assert_equal columns, query.column_names.map(&:to_s)
347 349
348 350 # columns should be stored in session
349 351 assert_kind_of Hash, session[:query]
350 352 assert_kind_of Array, session[:query][:column_names]
351 353 assert_equal columns, session[:query][:column_names].map(&:to_s)
352 354
353 355 # ensure only these columns are kept in the selected columns list
354 356 assert_tag :tag => 'select', :attributes => { :id => 'selected_columns' },
355 357 :children => { :count => 3 }
356 358 assert_no_tag :tag => 'option', :attributes => { :value => 'project' },
357 359 :parent => { :tag => 'select', :attributes => { :id => "selected_columns" } }
358 360 end
359 361
360 362 def test_index_without_project_should_implicitly_add_project_column_to_default_columns
361 363 Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to']
362 364 get :index, :set_filter => 1
363 365
364 366 # query should use specified columns
365 367 query = assigns(:query)
366 368 assert_kind_of Query, query
367 369 assert_equal [:project, :tracker, :subject, :assigned_to], query.columns.map(&:name)
368 370 end
369 371
370 372 def test_index_without_project_and_explicit_default_columns_should_not_add_project_column
371 373 Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to']
372 374 columns = ['tracker', 'subject', 'assigned_to']
373 375 get :index, :set_filter => 1, :c => columns
374 376
375 377 # query should use specified columns
376 378 query = assigns(:query)
377 379 assert_kind_of Query, query
378 380 assert_equal columns.map(&:to_sym), query.columns.map(&:name)
379 381 end
380 382
381 383 def test_index_with_custom_field_column
382 384 columns = %w(tracker subject cf_2)
383 385 get :index, :set_filter => 1, :c => columns
384 386 assert_response :success
385 387
386 388 # query should use specified columns
387 389 query = assigns(:query)
388 390 assert_kind_of Query, query
389 391 assert_equal columns, query.column_names.map(&:to_s)
390 392
391 393 assert_tag :td,
392 394 :attributes => {:class => 'cf_2 string'},
393 395 :ancestor => {:tag => 'table', :attributes => {:class => /issues/}}
394 396 end
395 397
396 398 def test_index_send_html_if_query_is_invalid
397 399 get :index, :f => ['start_date'], :op => {:start_date => '='}
398 400 assert_equal 'text/html', @response.content_type
399 401 assert_template 'index'
400 402 end
401 403
402 404 def test_index_send_nothing_if_query_is_invalid
403 405 get :index, :f => ['start_date'], :op => {:start_date => '='}, :format => 'csv'
404 406 assert_equal 'text/csv', @response.content_type
405 407 assert @response.body.blank?
406 408 end
407 409
408 410 def test_show_by_anonymous
409 411 get :show, :id => 1
410 412 assert_response :success
411 413 assert_template 'show'
412 414 assert_not_nil assigns(:issue)
413 415 assert_equal Issue.find(1), assigns(:issue)
414 416
415 417 # anonymous role is allowed to add a note
416 418 assert_tag :tag => 'form',
417 419 :descendant => { :tag => 'fieldset',
418 420 :child => { :tag => 'legend',
419 421 :content => /Notes/ } }
420 422 end
421 423
422 424 def test_show_by_manager
423 425 @request.session[:user_id] = 2
424 426 get :show, :id => 1
425 427 assert_response :success
426 428
427 429 assert_tag :tag => 'a',
428 430 :content => /Quote/
429 431
430 432 assert_tag :tag => 'form',
431 433 :descendant => { :tag => 'fieldset',
432 434 :child => { :tag => 'legend',
433 435 :content => /Change properties/ } },
434 436 :descendant => { :tag => 'fieldset',
435 437 :child => { :tag => 'legend',
436 438 :content => /Log time/ } },
437 439 :descendant => { :tag => 'fieldset',
438 440 :child => { :tag => 'legend',
439 441 :content => /Notes/ } }
440 442 end
441 443
442 444 def test_update_form_should_not_display_inactive_enumerations
443 445 @request.session[:user_id] = 2
444 446 get :show, :id => 1
445 447 assert_response :success
446 448
447 449 assert ! IssuePriority.find(15).active?
448 450 assert_no_tag :option, :attributes => {:value => '15'},
449 451 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
450 452 end
451 453
452 454 def test_update_form_should_allow_attachment_upload
453 455 @request.session[:user_id] = 2
454 456 get :show, :id => 1
455 457
456 458 assert_tag :tag => 'form',
457 459 :attributes => {:id => 'issue-form', :method => 'post', :enctype => 'multipart/form-data'},
458 460 :descendant => {
459 461 :tag => 'input',
460 462 :attributes => {:type => 'file', :name => 'attachments[1][file]'}
461 463 }
462 464 end
463 465
464 466 def test_show_should_deny_anonymous_access_without_permission
465 467 Role.anonymous.remove_permission!(:view_issues)
466 468 get :show, :id => 1
467 469 assert_response :redirect
468 470 end
469 471
470 472 def test_show_should_deny_anonymous_access_to_private_issue
471 473 Issue.update_all(["is_private = ?", true], "id = 1")
472 474 get :show, :id => 1
473 475 assert_response :redirect
474 476 end
475 477
476 478 def test_show_should_deny_non_member_access_without_permission
477 479 Role.non_member.remove_permission!(:view_issues)
478 480 @request.session[:user_id] = 9
479 481 get :show, :id => 1
480 482 assert_response 403
481 483 end
482 484
483 485 def test_show_should_deny_non_member_access_to_private_issue
484 486 Issue.update_all(["is_private = ?", true], "id = 1")
485 487 @request.session[:user_id] = 9
486 488 get :show, :id => 1
487 489 assert_response 403
488 490 end
489 491
490 492 def test_show_should_deny_member_access_without_permission
491 493 Role.find(1).remove_permission!(:view_issues)
492 494 @request.session[:user_id] = 2
493 495 get :show, :id => 1
494 496 assert_response 403
495 497 end
496 498
497 499 def test_show_should_deny_member_access_to_private_issue_without_permission
498 500 Issue.update_all(["is_private = ?", true], "id = 1")
499 501 @request.session[:user_id] = 3
500 502 get :show, :id => 1
501 503 assert_response 403
502 504 end
503 505
504 506 def test_show_should_allow_author_access_to_private_issue
505 507 Issue.update_all(["is_private = ?, author_id = 3", true], "id = 1")
506 508 @request.session[:user_id] = 3
507 509 get :show, :id => 1
508 510 assert_response :success
509 511 end
510 512
511 513 def test_show_should_allow_assignee_access_to_private_issue
512 514 Issue.update_all(["is_private = ?, assigned_to_id = 3", true], "id = 1")
513 515 @request.session[:user_id] = 3
514 516 get :show, :id => 1
515 517 assert_response :success
516 518 end
517 519
518 520 def test_show_should_allow_member_access_to_private_issue_with_permission
519 521 Issue.update_all(["is_private = ?", true], "id = 1")
520 522 User.find(3).roles_for_project(Project.find(1)).first.update_attribute :issues_visibility, 'all'
521 523 @request.session[:user_id] = 3
522 524 get :show, :id => 1
523 525 assert_response :success
524 526 end
525 527
526 528 def test_show_should_not_disclose_relations_to_invisible_issues
527 529 Setting.cross_project_issue_relations = '1'
528 530 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
529 531 # Relation to a private project issue
530 532 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
531 533
532 534 get :show, :id => 1
533 535 assert_response :success
534 536
535 537 assert_tag :div, :attributes => { :id => 'relations' },
536 538 :descendant => { :tag => 'a', :content => /#2$/ }
537 539 assert_no_tag :div, :attributes => { :id => 'relations' },
538 540 :descendant => { :tag => 'a', :content => /#4$/ }
539 541 end
540 542
541 543 def test_show_atom
542 544 get :show, :id => 2, :format => 'atom'
543 545 assert_response :success
544 546 assert_template 'journals/index'
545 547 # Inline image
546 548 assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10'))
547 549 end
548 550
549 551 def test_show_export_to_pdf
550 552 get :show, :id => 3, :format => 'pdf'
551 553 assert_response :success
552 554 assert_equal 'application/pdf', @response.content_type
553 555 assert @response.body.starts_with?('%PDF')
554 556 assert_not_nil assigns(:issue)
555 557 end
556 558
557 559 def test_get_new
558 560 @request.session[:user_id] = 2
559 561 get :new, :project_id => 1, :tracker_id => 1
560 562 assert_response :success
561 563 assert_template 'new'
562 564
563 565 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
564 566 :value => 'Default string' }
565 567
566 568 # Be sure we don't display inactive IssuePriorities
567 569 assert ! IssuePriority.find(15).active?
568 570 assert_no_tag :option, :attributes => {:value => '15'},
569 571 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
570 572 end
571 573
572 574 def test_get_new_form_should_allow_attachment_upload
573 575 @request.session[:user_id] = 2
574 576 get :new, :project_id => 1, :tracker_id => 1
575 577
576 578 assert_tag :tag => 'form',
577 579 :attributes => {:id => 'issue-form', :method => 'post', :enctype => 'multipart/form-data'},
578 580 :descendant => {
579 581 :tag => 'input',
580 582 :attributes => {:type => 'file', :name => 'attachments[1][file]'}
581 583 }
582 584 end
583 585
584 586 def test_get_new_without_tracker_id
585 587 @request.session[:user_id] = 2
586 588 get :new, :project_id => 1
587 589 assert_response :success
588 590 assert_template 'new'
589 591
590 592 issue = assigns(:issue)
591 593 assert_not_nil issue
592 594 assert_equal Project.find(1).trackers.first, issue.tracker
593 595 end
594 596
595 597 def test_get_new_with_no_default_status_should_display_an_error
596 598 @request.session[:user_id] = 2
597 599 IssueStatus.delete_all
598 600
599 601 get :new, :project_id => 1
600 602 assert_response 500
601 603 assert_error_tag :content => /No default issue/
602 604 end
603 605
604 606 def test_get_new_with_no_tracker_should_display_an_error
605 607 @request.session[:user_id] = 2
606 608 Tracker.delete_all
607 609
608 610 get :new, :project_id => 1
609 611 assert_response 500
610 612 assert_error_tag :content => /No tracker/
611 613 end
612 614
613 615 def test_update_new_form
614 616 @request.session[:user_id] = 2
615 617 xhr :post, :new, :project_id => 1,
616 618 :issue => {:tracker_id => 2,
617 619 :subject => 'This is the test_new issue',
618 620 :description => 'This is the description',
619 621 :priority_id => 5}
620 622 assert_response :success
621 623 assert_template 'attributes'
622 624
623 625 issue = assigns(:issue)
624 626 assert_kind_of Issue, issue
625 627 assert_equal 1, issue.project_id
626 628 assert_equal 2, issue.tracker_id
627 629 assert_equal 'This is the test_new issue', issue.subject
628 630 end
629 631
630 632 def test_post_create
631 633 @request.session[:user_id] = 2
632 634 assert_difference 'Issue.count' do
633 635 post :create, :project_id => 1,
634 636 :issue => {:tracker_id => 3,
635 637 :status_id => 2,
636 638 :subject => 'This is the test_new issue',
637 639 :description => 'This is the description',
638 640 :priority_id => 5,
639 641 :start_date => '2010-11-07',
640 642 :estimated_hours => '',
641 643 :custom_field_values => {'2' => 'Value for field 2'}}
642 644 end
643 645 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
644 646
645 647 issue = Issue.find_by_subject('This is the test_new issue')
646 648 assert_not_nil issue
647 649 assert_equal 2, issue.author_id
648 650 assert_equal 3, issue.tracker_id
649 651 assert_equal 2, issue.status_id
650 652 assert_equal Date.parse('2010-11-07'), issue.start_date
651 653 assert_nil issue.estimated_hours
652 654 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
653 655 assert_not_nil v
654 656 assert_equal 'Value for field 2', v.value
655 657 end
656 658
657 659 def test_post_new_with_group_assignment
658 660 group = Group.find(11)
659 661 project = Project.find(1)
660 662 project.members << Member.new(:principal => group, :roles => [Role.first])
661 663
662 664 with_settings :issue_group_assignment => '1' do
663 665 @request.session[:user_id] = 2
664 666 assert_difference 'Issue.count' do
665 667 post :create, :project_id => project.id,
666 668 :issue => {:tracker_id => 3,
667 669 :status_id => 1,
668 670 :subject => 'This is the test_new_with_group_assignment issue',
669 671 :assigned_to_id => group.id}
670 672 end
671 673 end
672 674 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
673 675
674 676 issue = Issue.find_by_subject('This is the test_new_with_group_assignment issue')
675 677 assert_not_nil issue
676 678 assert_equal group, issue.assigned_to
677 679 end
678 680
679 681 def test_post_create_without_start_date
680 682 @request.session[:user_id] = 2
681 683 assert_difference 'Issue.count' do
682 684 post :create, :project_id => 1,
683 685 :issue => {:tracker_id => 3,
684 686 :status_id => 2,
685 687 :subject => 'This is the test_new issue',
686 688 :description => 'This is the description',
687 689 :priority_id => 5,
688 690 :start_date => '',
689 691 :estimated_hours => '',
690 692 :custom_field_values => {'2' => 'Value for field 2'}}
691 693 end
692 694 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
693 695
694 696 issue = Issue.find_by_subject('This is the test_new issue')
695 697 assert_not_nil issue
696 698 assert_nil issue.start_date
697 699 end
698 700
699 701 def test_post_create_and_continue
700 702 @request.session[:user_id] = 2
701 703 assert_difference 'Issue.count' do
702 704 post :create, :project_id => 1,
703 705 :issue => {:tracker_id => 3, :subject => 'This is first issue', :priority_id => 5},
704 706 :continue => ''
705 707 end
706 708
707 709 issue = Issue.first(:order => 'id DESC')
708 710 assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook', :issue => {:tracker_id => 3}
709 711 assert_not_nil flash[:notice], "flash was not set"
710 712 assert flash[:notice].include?("<a href='/issues/#{issue.id}'>##{issue.id}</a>"), "issue link not found in flash: #{flash[:notice]}"
711 713 end
712 714
713 715 def test_post_create_without_custom_fields_param
714 716 @request.session[:user_id] = 2
715 717 assert_difference 'Issue.count' do
716 718 post :create, :project_id => 1,
717 719 :issue => {:tracker_id => 1,
718 720 :subject => 'This is the test_new issue',
719 721 :description => 'This is the description',
720 722 :priority_id => 5}
721 723 end
722 724 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
723 725 end
724 726
725 727 def test_post_create_with_required_custom_field_and_without_custom_fields_param
726 728 field = IssueCustomField.find_by_name('Database')
727 729 field.update_attribute(:is_required, true)
728 730
729 731 @request.session[:user_id] = 2
730 732 post :create, :project_id => 1,
731 733 :issue => {:tracker_id => 1,
732 734 :subject => 'This is the test_new issue',
733 735 :description => 'This is the description',
734 736 :priority_id => 5}
735 737 assert_response :success
736 738 assert_template 'new'
737 739 issue = assigns(:issue)
738 740 assert_not_nil issue
739 741 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
740 742 end
741 743
742 744 def test_post_create_with_watchers
743 745 @request.session[:user_id] = 2
744 746 ActionMailer::Base.deliveries.clear
745 747
746 748 assert_difference 'Watcher.count', 2 do
747 749 post :create, :project_id => 1,
748 750 :issue => {:tracker_id => 1,
749 751 :subject => 'This is a new issue with watchers',
750 752 :description => 'This is the description',
751 753 :priority_id => 5,
752 754 :watcher_user_ids => ['2', '3']}
753 755 end
754 756 issue = Issue.find_by_subject('This is a new issue with watchers')
755 757 assert_not_nil issue
756 758 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
757 759
758 760 # Watchers added
759 761 assert_equal [2, 3], issue.watcher_user_ids.sort
760 762 assert issue.watched_by?(User.find(3))
761 763 # Watchers notified
762 764 mail = ActionMailer::Base.deliveries.last
763 765 assert_kind_of TMail::Mail, mail
764 766 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
765 767 end
766 768
767 769 def test_post_create_subissue
768 770 @request.session[:user_id] = 2
769 771
770 772 assert_difference 'Issue.count' do
771 773 post :create, :project_id => 1,
772 774 :issue => {:tracker_id => 1,
773 775 :subject => 'This is a child issue',
774 776 :parent_issue_id => 2}
775 777 end
776 778 issue = Issue.find_by_subject('This is a child issue')
777 779 assert_not_nil issue
778 780 assert_equal Issue.find(2), issue.parent
779 781 end
780 782
781 783 def test_post_create_subissue_with_non_numeric_parent_id
782 784 @request.session[:user_id] = 2
783 785
784 786 assert_difference 'Issue.count' do
785 787 post :create, :project_id => 1,
786 788 :issue => {:tracker_id => 1,
787 789 :subject => 'This is a child issue',
788 790 :parent_issue_id => 'ABC'}
789 791 end
790 792 issue = Issue.find_by_subject('This is a child issue')
791 793 assert_not_nil issue
792 794 assert_nil issue.parent
793 795 end
794 796
795 797 def test_post_create_private
796 798 @request.session[:user_id] = 2
797 799
798 800 assert_difference 'Issue.count' do
799 801 post :create, :project_id => 1,
800 802 :issue => {:tracker_id => 1,
801 803 :subject => 'This is a private issue',
802 804 :is_private => '1'}
803 805 end
804 806 issue = Issue.first(:order => 'id DESC')
805 807 assert issue.is_private?
806 808 end
807 809
808 810 def test_post_create_private_with_set_own_issues_private_permission
809 811 role = Role.find(1)
810 812 role.remove_permission! :set_issues_private
811 813 role.add_permission! :set_own_issues_private
812 814
813 815 @request.session[:user_id] = 2
814 816
815 817 assert_difference 'Issue.count' do
816 818 post :create, :project_id => 1,
817 819 :issue => {:tracker_id => 1,
818 820 :subject => 'This is a private issue',
819 821 :is_private => '1'}
820 822 end
821 823 issue = Issue.first(:order => 'id DESC')
822 824 assert issue.is_private?
823 825 end
824 826
825 827 def test_post_create_should_send_a_notification
826 828 ActionMailer::Base.deliveries.clear
827 829 @request.session[:user_id] = 2
828 830 assert_difference 'Issue.count' do
829 831 post :create, :project_id => 1,
830 832 :issue => {:tracker_id => 3,
831 833 :subject => 'This is the test_new issue',
832 834 :description => 'This is the description',
833 835 :priority_id => 5,
834 836 :estimated_hours => '',
835 837 :custom_field_values => {'2' => 'Value for field 2'}}
836 838 end
837 839 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
838 840
839 841 assert_equal 1, ActionMailer::Base.deliveries.size
840 842 end
841 843
842 844 def test_post_create_should_preserve_fields_values_on_validation_failure
843 845 @request.session[:user_id] = 2
844 846 post :create, :project_id => 1,
845 847 :issue => {:tracker_id => 1,
846 848 # empty subject
847 849 :subject => '',
848 850 :description => 'This is a description',
849 851 :priority_id => 6,
850 852 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
851 853 assert_response :success
852 854 assert_template 'new'
853 855
854 856 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
855 857 :content => 'This is a description'
856 858 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
857 859 :child => { :tag => 'option', :attributes => { :selected => 'selected',
858 860 :value => '6' },
859 861 :content => 'High' }
860 862 # Custom fields
861 863 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
862 864 :child => { :tag => 'option', :attributes => { :selected => 'selected',
863 865 :value => 'Oracle' },
864 866 :content => 'Oracle' }
865 867 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
866 868 :value => 'Value for field 2'}
867 869 end
868 870
869 871 def test_post_create_should_ignore_non_safe_attributes
870 872 @request.session[:user_id] = 2
871 873 assert_nothing_raised do
872 874 post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
873 875 end
874 876 end
875 877
876 878 def test_post_create_with_attachment
877 879 set_tmp_attachments_directory
878 880 @request.session[:user_id] = 2
879 881
880 882 assert_difference 'Issue.count' do
881 883 assert_difference 'Attachment.count' do
882 884 post :create, :project_id => 1,
883 885 :issue => { :tracker_id => '1', :subject => 'With attachment' },
884 886 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
885 887 end
886 888 end
887 889
888 890 issue = Issue.first(:order => 'id DESC')
889 891 attachment = Attachment.first(:order => 'id DESC')
890 892
891 893 assert_equal issue, attachment.container
892 894 assert_equal 2, attachment.author_id
893 895 assert_equal 'testfile.txt', attachment.filename
894 896 assert_equal 'text/plain', attachment.content_type
895 897 assert_equal 'test file', attachment.description
896 898 assert_equal 59, attachment.filesize
897 899 assert File.exists?(attachment.diskfile)
898 900 assert_equal 59, File.size(attachment.diskfile)
899 901 end
900 902
901 903 context "without workflow privilege" do
902 904 setup do
903 905 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
904 906 Role.anonymous.add_permission! :add_issues, :add_issue_notes
905 907 end
906 908
907 909 context "#new" do
908 910 should "propose default status only" do
909 911 get :new, :project_id => 1
910 912 assert_response :success
911 913 assert_template 'new'
912 914 assert_tag :tag => 'select',
913 915 :attributes => {:name => 'issue[status_id]'},
914 916 :children => {:count => 1},
915 917 :child => {:tag => 'option', :attributes => {:value => IssueStatus.default.id.to_s}}
916 918 end
917 919
918 920 should "accept default status" do
919 921 assert_difference 'Issue.count' do
920 922 post :create, :project_id => 1,
921 923 :issue => {:tracker_id => 1,
922 924 :subject => 'This is an issue',
923 925 :status_id => 1}
924 926 end
925 927 issue = Issue.last(:order => 'id')
926 928 assert_equal IssueStatus.default, issue.status
927 929 end
928 930
929 931 should "ignore unauthorized status" do
930 932 assert_difference 'Issue.count' do
931 933 post :create, :project_id => 1,
932 934 :issue => {:tracker_id => 1,
933 935 :subject => 'This is an issue',
934 936 :status_id => 3}
935 937 end
936 938 issue = Issue.last(:order => 'id')
937 939 assert_equal IssueStatus.default, issue.status
938 940 end
939 941 end
940 942
941 943 context "#update" do
942 944 should "ignore status change" do
943 945 assert_difference 'Journal.count' do
944 946 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
945 947 end
946 948 assert_equal 1, Issue.find(1).status_id
947 949 end
948 950
949 951 should "ignore attributes changes" do
950 952 assert_difference 'Journal.count' do
951 953 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
952 954 end
953 955 issue = Issue.find(1)
954 956 assert_equal "Can't print recipes", issue.subject
955 957 assert_nil issue.assigned_to
956 958 end
957 959 end
958 960 end
959 961
960 962 context "with workflow privilege" do
961 963 setup do
962 964 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
963 965 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3)
964 966 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4)
965 967 Role.anonymous.add_permission! :add_issues, :add_issue_notes
966 968 end
967 969
968 970 context "#update" do
969 971 should "accept authorized status" do
970 972 assert_difference 'Journal.count' do
971 973 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
972 974 end
973 975 assert_equal 3, Issue.find(1).status_id
974 976 end
975 977
976 978 should "ignore unauthorized status" do
977 979 assert_difference 'Journal.count' do
978 980 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
979 981 end
980 982 assert_equal 1, Issue.find(1).status_id
981 983 end
982 984
983 985 should "accept authorized attributes changes" do
984 986 assert_difference 'Journal.count' do
985 987 put :update, :id => 1, :notes => 'just trying', :issue => {:assigned_to_id => 2}
986 988 end
987 989 issue = Issue.find(1)
988 990 assert_equal 2, issue.assigned_to_id
989 991 end
990 992
991 993 should "ignore unauthorized attributes changes" do
992 994 assert_difference 'Journal.count' do
993 995 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed'}
994 996 end
995 997 issue = Issue.find(1)
996 998 assert_equal "Can't print recipes", issue.subject
997 999 end
998 1000 end
999 1001
1000 1002 context "and :edit_issues permission" do
1001 1003 setup do
1002 1004 Role.anonymous.add_permission! :add_issues, :edit_issues
1003 1005 end
1004 1006
1005 1007 should "accept authorized status" do
1006 1008 assert_difference 'Journal.count' do
1007 1009 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
1008 1010 end
1009 1011 assert_equal 3, Issue.find(1).status_id
1010 1012 end
1011 1013
1012 1014 should "ignore unauthorized status" do
1013 1015 assert_difference 'Journal.count' do
1014 1016 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
1015 1017 end
1016 1018 assert_equal 1, Issue.find(1).status_id
1017 1019 end
1018 1020
1019 1021 should "accept authorized attributes changes" do
1020 1022 assert_difference 'Journal.count' do
1021 1023 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
1022 1024 end
1023 1025 issue = Issue.find(1)
1024 1026 assert_equal "changed", issue.subject
1025 1027 assert_equal 2, issue.assigned_to_id
1026 1028 end
1027 1029 end
1028 1030 end
1029 1031
1030 1032 def test_copy_issue
1031 1033 @request.session[:user_id] = 2
1032 1034 get :new, :project_id => 1, :copy_from => 1
1033 1035 assert_template 'new'
1034 1036 assert_not_nil assigns(:issue)
1035 1037 orig = Issue.find(1)
1036 1038 assert_equal orig.subject, assigns(:issue).subject
1037 1039 end
1038 1040
1039 1041 def test_get_edit
1040 1042 @request.session[:user_id] = 2
1041 1043 get :edit, :id => 1
1042 1044 assert_response :success
1043 1045 assert_template 'edit'
1044 1046 assert_not_nil assigns(:issue)
1045 1047 assert_equal Issue.find(1), assigns(:issue)
1046 1048
1047 1049 # Be sure we don't display inactive IssuePriorities
1048 1050 assert ! IssuePriority.find(15).active?
1049 1051 assert_no_tag :option, :attributes => {:value => '15'},
1050 1052 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
1051 1053 end
1052 1054
1053 1055 def test_get_edit_with_params
1054 1056 @request.session[:user_id] = 2
1055 1057 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 },
1056 1058 :time_entry => { :hours => '2.5', :comments => 'test_get_edit_with_params', :activity_id => TimeEntryActivity.first.id }
1057 1059 assert_response :success
1058 1060 assert_template 'edit'
1059 1061
1060 1062 issue = assigns(:issue)
1061 1063 assert_not_nil issue
1062 1064
1063 1065 assert_equal 5, issue.status_id
1064 1066 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
1065 1067 :child => { :tag => 'option',
1066 1068 :content => 'Closed',
1067 1069 :attributes => { :selected => 'selected' } }
1068 1070
1069 1071 assert_equal 7, issue.priority_id
1070 1072 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
1071 1073 :child => { :tag => 'option',
1072 1074 :content => 'Urgent',
1073 1075 :attributes => { :selected => 'selected' } }
1074 1076
1075 1077 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => '2.5' }
1076 1078 assert_tag :select, :attributes => { :name => 'time_entry[activity_id]' },
1077 1079 :child => { :tag => 'option',
1078 1080 :attributes => { :selected => 'selected', :value => TimeEntryActivity.first.id } }
1079 1081 assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => 'test_get_edit_with_params' }
1080 1082 end
1081 1083
1082 1084 def test_update_edit_form
1083 1085 @request.session[:user_id] = 2
1084 1086 xhr :post, :new, :project_id => 1,
1085 1087 :id => 1,
1086 1088 :issue => {:tracker_id => 2,
1087 1089 :subject => 'This is the test_new issue',
1088 1090 :description => 'This is the description',
1089 1091 :priority_id => 5}
1090 1092 assert_response :success
1091 1093 assert_template 'attributes'
1092 1094
1093 1095 issue = assigns(:issue)
1094 1096 assert_kind_of Issue, issue
1095 1097 assert_equal 1, issue.id
1096 1098 assert_equal 1, issue.project_id
1097 1099 assert_equal 2, issue.tracker_id
1098 1100 assert_equal 'This is the test_new issue', issue.subject
1099 1101 end
1100 1102
1101 1103 def test_update_using_invalid_http_verbs
1102 1104 @request.session[:user_id] = 2
1103 1105 subject = 'Updated by an invalid http verb'
1104 1106
1105 1107 get :update, :id => 1, :issue => {:subject => subject}
1106 1108 assert_not_equal subject, Issue.find(1).subject
1107 1109
1108 1110 post :update, :id => 1, :issue => {:subject => subject}
1109 1111 assert_not_equal subject, Issue.find(1).subject
1110 1112
1111 1113 delete :update, :id => 1, :issue => {:subject => subject}
1112 1114 assert_not_equal subject, Issue.find(1).subject
1113 1115 end
1114 1116
1115 1117 def test_put_update_without_custom_fields_param
1116 1118 @request.session[:user_id] = 2
1117 1119 ActionMailer::Base.deliveries.clear
1118 1120
1119 1121 issue = Issue.find(1)
1120 1122 assert_equal '125', issue.custom_value_for(2).value
1121 1123 old_subject = issue.subject
1122 1124 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
1123 1125
1124 1126 assert_difference('Journal.count') do
1125 1127 assert_difference('JournalDetail.count', 2) do
1126 1128 put :update, :id => 1, :issue => {:subject => new_subject,
1127 1129 :priority_id => '6',
1128 1130 :category_id => '1' # no change
1129 1131 }
1130 1132 end
1131 1133 end
1132 1134 assert_redirected_to :action => 'show', :id => '1'
1133 1135 issue.reload
1134 1136 assert_equal new_subject, issue.subject
1135 1137 # Make sure custom fields were not cleared
1136 1138 assert_equal '125', issue.custom_value_for(2).value
1137 1139
1138 1140 mail = ActionMailer::Base.deliveries.last
1139 1141 assert_kind_of TMail::Mail, mail
1140 1142 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
1141 1143 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
1142 1144 end
1143 1145
1144 1146 def test_put_update_with_custom_field_change
1145 1147 @request.session[:user_id] = 2
1146 1148 issue = Issue.find(1)
1147 1149 assert_equal '125', issue.custom_value_for(2).value
1148 1150
1149 1151 assert_difference('Journal.count') do
1150 1152 assert_difference('JournalDetail.count', 3) do
1151 1153 put :update, :id => 1, :issue => {:subject => 'Custom field change',
1152 1154 :priority_id => '6',
1153 1155 :category_id => '1', # no change
1154 1156 :custom_field_values => { '2' => 'New custom value' }
1155 1157 }
1156 1158 end
1157 1159 end
1158 1160 assert_redirected_to :action => 'show', :id => '1'
1159 1161 issue.reload
1160 1162 assert_equal 'New custom value', issue.custom_value_for(2).value
1161 1163
1162 1164 mail = ActionMailer::Base.deliveries.last
1163 1165 assert_kind_of TMail::Mail, mail
1164 1166 assert mail.body.include?("Searchable field changed from 125 to New custom value")
1165 1167 end
1166 1168
1167 1169 def test_put_update_with_status_and_assignee_change
1168 1170 issue = Issue.find(1)
1169 1171 assert_equal 1, issue.status_id
1170 1172 @request.session[:user_id] = 2
1171 1173 assert_difference('TimeEntry.count', 0) do
1172 1174 put :update,
1173 1175 :id => 1,
1174 1176 :issue => { :status_id => 2, :assigned_to_id => 3 },
1175 1177 :notes => 'Assigned to dlopper',
1176 1178 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
1177 1179 end
1178 1180 assert_redirected_to :action => 'show', :id => '1'
1179 1181 issue.reload
1180 1182 assert_equal 2, issue.status_id
1181 1183 j = Journal.find(:first, :order => 'id DESC')
1182 1184 assert_equal 'Assigned to dlopper', j.notes
1183 1185 assert_equal 2, j.details.size
1184 1186
1185 1187 mail = ActionMailer::Base.deliveries.last
1186 1188 assert mail.body.include?("Status changed from New to Assigned")
1187 1189 # subject should contain the new status
1188 1190 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
1189 1191 end
1190 1192
1191 1193 def test_put_update_with_note_only
1192 1194 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
1193 1195 # anonymous user
1194 1196 put :update,
1195 1197 :id => 1,
1196 1198 :notes => notes
1197 1199 assert_redirected_to :action => 'show', :id => '1'
1198 1200 j = Journal.find(:first, :order => 'id DESC')
1199 1201 assert_equal notes, j.notes
1200 1202 assert_equal 0, j.details.size
1201 1203 assert_equal User.anonymous, j.user
1202 1204
1203 1205 mail = ActionMailer::Base.deliveries.last
1204 1206 assert mail.body.include?(notes)
1205 1207 end
1206 1208
1207 1209 def test_put_update_with_note_and_spent_time
1208 1210 @request.session[:user_id] = 2
1209 1211 spent_hours_before = Issue.find(1).spent_hours
1210 1212 assert_difference('TimeEntry.count') do
1211 1213 put :update,
1212 1214 :id => 1,
1213 1215 :notes => '2.5 hours added',
1214 1216 :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id }
1215 1217 end
1216 1218 assert_redirected_to :action => 'show', :id => '1'
1217 1219
1218 1220 issue = Issue.find(1)
1219 1221
1220 1222 j = Journal.find(:first, :order => 'id DESC')
1221 1223 assert_equal '2.5 hours added', j.notes
1222 1224 assert_equal 0, j.details.size
1223 1225
1224 1226 t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time')
1225 1227 assert_not_nil t
1226 1228 assert_equal 2.5, t.hours
1227 1229 assert_equal spent_hours_before + 2.5, issue.spent_hours
1228 1230 end
1229 1231
1230 1232 def test_put_update_with_attachment_only
1231 1233 set_tmp_attachments_directory
1232 1234
1233 1235 # Delete all fixtured journals, a race condition can occur causing the wrong
1234 1236 # journal to get fetched in the next find.
1235 1237 Journal.delete_all
1236 1238
1237 1239 # anonymous user
1238 1240 assert_difference 'Attachment.count' do
1239 1241 put :update, :id => 1,
1240 1242 :notes => '',
1241 1243 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
1242 1244 end
1243 1245
1244 1246 assert_redirected_to :action => 'show', :id => '1'
1245 1247 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
1246 1248 assert j.notes.blank?
1247 1249 assert_equal 1, j.details.size
1248 1250 assert_equal 'testfile.txt', j.details.first.value
1249 1251 assert_equal User.anonymous, j.user
1250 1252
1251 1253 attachment = Attachment.first(:order => 'id DESC')
1252 1254 assert_equal Issue.find(1), attachment.container
1253 1255 assert_equal User.anonymous, attachment.author
1254 1256 assert_equal 'testfile.txt', attachment.filename
1255 1257 assert_equal 'text/plain', attachment.content_type
1256 1258 assert_equal 'test file', attachment.description
1257 1259 assert_equal 59, attachment.filesize
1258 1260 assert File.exists?(attachment.diskfile)
1259 1261 assert_equal 59, File.size(attachment.diskfile)
1260 1262
1261 1263 mail = ActionMailer::Base.deliveries.last
1262 1264 assert mail.body.include?('testfile.txt')
1263 1265 end
1264 1266
1265 1267 def test_put_update_with_attachment_that_fails_to_save
1266 1268 set_tmp_attachments_directory
1267 1269
1268 1270 # Delete all fixtured journals, a race condition can occur causing the wrong
1269 1271 # journal to get fetched in the next find.
1270 1272 Journal.delete_all
1271 1273
1272 1274 # Mock out the unsaved attachment
1273 1275 Attachment.any_instance.stubs(:create).returns(Attachment.new)
1274 1276
1275 1277 # anonymous user
1276 1278 put :update,
1277 1279 :id => 1,
1278 1280 :notes => '',
1279 1281 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
1280 1282 assert_redirected_to :action => 'show', :id => '1'
1281 1283 assert_equal '1 file(s) could not be saved.', flash[:warning]
1282 1284
1283 1285 end if Object.const_defined?(:Mocha)
1284 1286
1285 1287 def test_put_update_with_no_change
1286 1288 issue = Issue.find(1)
1287 1289 issue.journals.clear
1288 1290 ActionMailer::Base.deliveries.clear
1289 1291
1290 1292 put :update,
1291 1293 :id => 1,
1292 1294 :notes => ''
1293 1295 assert_redirected_to :action => 'show', :id => '1'
1294 1296
1295 1297 issue.reload
1296 1298 assert issue.journals.empty?
1297 1299 # No email should be sent
1298 1300 assert ActionMailer::Base.deliveries.empty?
1299 1301 end
1300 1302
1301 1303 def test_put_update_should_send_a_notification
1302 1304 @request.session[:user_id] = 2
1303 1305 ActionMailer::Base.deliveries.clear
1304 1306 issue = Issue.find(1)
1305 1307 old_subject = issue.subject
1306 1308 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
1307 1309
1308 1310 put :update, :id => 1, :issue => {:subject => new_subject,
1309 1311 :priority_id => '6',
1310 1312 :category_id => '1' # no change
1311 1313 }
1312 1314 assert_equal 1, ActionMailer::Base.deliveries.size
1313 1315 end
1314 1316
1315 1317 def test_put_update_with_invalid_spent_time_hours_only
1316 1318 @request.session[:user_id] = 2
1317 1319 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
1318 1320
1319 1321 assert_no_difference('Journal.count') do
1320 1322 put :update,
1321 1323 :id => 1,
1322 1324 :notes => notes,
1323 1325 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
1324 1326 end
1325 1327 assert_response :success
1326 1328 assert_template 'edit'
1327 1329
1328 1330 assert_error_tag :descendant => {:content => /Activity can't be blank/}
1329 1331 assert_tag :textarea, :attributes => { :name => 'notes' }, :content => notes
1330 1332 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
1331 1333 end
1332 1334
1333 1335 def test_put_update_with_invalid_spent_time_comments_only
1334 1336 @request.session[:user_id] = 2
1335 1337 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
1336 1338
1337 1339 assert_no_difference('Journal.count') do
1338 1340 put :update,
1339 1341 :id => 1,
1340 1342 :notes => notes,
1341 1343 :time_entry => {"comments"=>"this is my comment", "activity_id"=>"", "hours"=>""}
1342 1344 end
1343 1345 assert_response :success
1344 1346 assert_template 'edit'
1345 1347
1346 1348 assert_error_tag :descendant => {:content => /Activity can't be blank/}
1347 1349 assert_error_tag :descendant => {:content => /Hours can't be blank/}
1348 1350 assert_tag :textarea, :attributes => { :name => 'notes' }, :content => notes
1349 1351 assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => "this is my comment" }
1350 1352 end
1351 1353
1352 1354 def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject
1353 1355 issue = Issue.find(2)
1354 1356 @request.session[:user_id] = 2
1355 1357
1356 1358 put :update,
1357 1359 :id => issue.id,
1358 1360 :issue => {
1359 1361 :fixed_version_id => 4
1360 1362 }
1361 1363
1362 1364 assert_response :redirect
1363 1365 issue.reload
1364 1366 assert_equal 4, issue.fixed_version_id
1365 1367 assert_not_equal issue.project_id, issue.fixed_version.project_id
1366 1368 end
1367 1369
1368 1370 def test_put_update_should_redirect_back_using_the_back_url_parameter
1369 1371 issue = Issue.find(2)
1370 1372 @request.session[:user_id] = 2
1371 1373
1372 1374 put :update,
1373 1375 :id => issue.id,
1374 1376 :issue => {
1375 1377 :fixed_version_id => 4
1376 1378 },
1377 1379 :back_url => '/issues'
1378 1380
1379 1381 assert_response :redirect
1380 1382 assert_redirected_to '/issues'
1381 1383 end
1382 1384
1383 1385 def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1384 1386 issue = Issue.find(2)
1385 1387 @request.session[:user_id] = 2
1386 1388
1387 1389 put :update,
1388 1390 :id => issue.id,
1389 1391 :issue => {
1390 1392 :fixed_version_id => 4
1391 1393 },
1392 1394 :back_url => 'http://google.com'
1393 1395
1394 1396 assert_response :redirect
1395 1397 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
1396 1398 end
1397 1399
1398 1400 def test_get_bulk_edit
1399 1401 @request.session[:user_id] = 2
1400 1402 get :bulk_edit, :ids => [1, 2]
1401 1403 assert_response :success
1402 1404 assert_template 'bulk_edit'
1403 1405
1404 1406 assert_tag :input, :attributes => {:name => 'issue[parent_issue_id]'}
1405 1407
1406 1408 # Project specific custom field, date type
1407 1409 field = CustomField.find(9)
1408 1410 assert !field.is_for_all?
1409 1411 assert_equal 'date', field.field_format
1410 1412 assert_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
1411 1413
1412 1414 # System wide custom field
1413 1415 assert CustomField.find(1).is_for_all?
1414 1416 assert_tag :select, :attributes => {:name => 'issue[custom_field_values][1]'}
1415 1417
1416 1418 # Be sure we don't display inactive IssuePriorities
1417 1419 assert ! IssuePriority.find(15).active?
1418 1420 assert_no_tag :option, :attributes => {:value => '15'},
1419 1421 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
1420 1422 end
1421 1423
1422 1424 def test_get_bulk_edit_on_different_projects
1423 1425 @request.session[:user_id] = 2
1424 1426 get :bulk_edit, :ids => [1, 2, 6]
1425 1427 assert_response :success
1426 1428 assert_template 'bulk_edit'
1427 1429
1428 1430 # Can not set issues from different projects as children of an issue
1429 1431 assert_no_tag :input, :attributes => {:name => 'issue[parent_issue_id]'}
1430 1432
1431 1433 # Project specific custom field, date type
1432 1434 field = CustomField.find(9)
1433 1435 assert !field.is_for_all?
1434 1436 assert !field.project_ids.include?(Issue.find(6).project_id)
1435 1437 assert_no_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
1436 1438 end
1437 1439
1438 1440 def test_get_bulk_edit_with_user_custom_field
1439 1441 field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user', :is_for_all => true)
1440 1442
1441 1443 @request.session[:user_id] = 2
1442 1444 get :bulk_edit, :ids => [1, 2]
1443 1445 assert_response :success
1444 1446 assert_template 'bulk_edit'
1445 1447
1446 1448 assert_tag :select,
1447 1449 :attributes => {:name => "issue[custom_field_values][#{field.id}]"},
1448 1450 :children => {
1449 1451 :only => {:tag => 'option'},
1450 1452 :count => Project.find(1).users.count + 1
1451 1453 }
1452 1454 end
1453 1455
1454 1456 def test_get_bulk_edit_with_version_custom_field
1455 1457 field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true)
1456 1458
1457 1459 @request.session[:user_id] = 2
1458 1460 get :bulk_edit, :ids => [1, 2]
1459 1461 assert_response :success
1460 1462 assert_template 'bulk_edit'
1461 1463
1462 1464 assert_tag :select,
1463 1465 :attributes => {:name => "issue[custom_field_values][#{field.id}]"},
1464 1466 :children => {
1465 1467 :only => {:tag => 'option'},
1466 1468 :count => Project.find(1).versions.count + 1
1467 1469 }
1468 1470 end
1469 1471
1470 1472 def test_bulk_update
1471 1473 @request.session[:user_id] = 2
1472 1474 # update issues priority
1473 1475 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
1474 1476 :issue => {:priority_id => 7,
1475 1477 :assigned_to_id => '',
1476 1478 :custom_field_values => {'2' => ''}}
1477 1479
1478 1480 assert_response 302
1479 1481 # check that the issues were updated
1480 1482 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
1481 1483
1482 1484 issue = Issue.find(1)
1483 1485 journal = issue.journals.find(:first, :order => 'created_on DESC')
1484 1486 assert_equal '125', issue.custom_value_for(2).value
1485 1487 assert_equal 'Bulk editing', journal.notes
1486 1488 assert_equal 1, journal.details.size
1487 1489 end
1488 1490
1489 1491 def test_bulk_update_with_group_assignee
1490 1492 group = Group.find(11)
1491 1493 project = Project.find(1)
1492 1494 project.members << Member.new(:principal => group, :roles => [Role.first])
1493 1495
1494 1496 @request.session[:user_id] = 2
1495 1497 # update issues assignee
1496 1498 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
1497 1499 :issue => {:priority_id => '',
1498 1500 :assigned_to_id => group.id,
1499 1501 :custom_field_values => {'2' => ''}}
1500 1502
1501 1503 assert_response 302
1502 1504 assert_equal [group, group], Issue.find_all_by_id([1, 2]).collect {|i| i.assigned_to}
1503 1505 end
1504 1506
1505 1507 def test_bulk_update_on_different_projects
1506 1508 @request.session[:user_id] = 2
1507 1509 # update issues priority
1508 1510 post :bulk_update, :ids => [1, 2, 6], :notes => 'Bulk editing',
1509 1511 :issue => {:priority_id => 7,
1510 1512 :assigned_to_id => '',
1511 1513 :custom_field_values => {'2' => ''}}
1512 1514
1513 1515 assert_response 302
1514 1516 # check that the issues were updated
1515 1517 assert_equal [7, 7, 7], Issue.find([1,2,6]).map(&:priority_id)
1516 1518
1517 1519 issue = Issue.find(1)
1518 1520 journal = issue.journals.find(:first, :order => 'created_on DESC')
1519 1521 assert_equal '125', issue.custom_value_for(2).value
1520 1522 assert_equal 'Bulk editing', journal.notes
1521 1523 assert_equal 1, journal.details.size
1522 1524 end
1523 1525
1524 1526 def test_bulk_update_on_different_projects_without_rights
1525 1527 @request.session[:user_id] = 3
1526 1528 user = User.find(3)
1527 1529 action = { :controller => "issues", :action => "bulk_update" }
1528 1530 assert user.allowed_to?(action, Issue.find(1).project)
1529 1531 assert ! user.allowed_to?(action, Issue.find(6).project)
1530 1532 post :bulk_update, :ids => [1, 6], :notes => 'Bulk should fail',
1531 1533 :issue => {:priority_id => 7,
1532 1534 :assigned_to_id => '',
1533 1535 :custom_field_values => {'2' => ''}}
1534 1536 assert_response 403
1535 1537 assert_not_equal "Bulk should fail", Journal.last.notes
1536 1538 end
1537 1539
1538 1540 def test_bullk_update_should_send_a_notification
1539 1541 @request.session[:user_id] = 2
1540 1542 ActionMailer::Base.deliveries.clear
1541 1543 post(:bulk_update,
1542 1544 {
1543 1545 :ids => [1, 2],
1544 1546 :notes => 'Bulk editing',
1545 1547 :issue => {
1546 1548 :priority_id => 7,
1547 1549 :assigned_to_id => '',
1548 1550 :custom_field_values => {'2' => ''}
1549 1551 }
1550 1552 })
1551 1553
1552 1554 assert_response 302
1553 1555 assert_equal 2, ActionMailer::Base.deliveries.size
1554 1556 end
1555 1557
1556 1558 def test_bulk_update_status
1557 1559 @request.session[:user_id] = 2
1558 1560 # update issues priority
1559 1561 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status',
1560 1562 :issue => {:priority_id => '',
1561 1563 :assigned_to_id => '',
1562 1564 :status_id => '5'}
1563 1565
1564 1566 assert_response 302
1565 1567 issue = Issue.find(1)
1566 1568 assert issue.closed?
1567 1569 end
1568 1570
1569 1571 def test_bulk_update_parent_id
1570 1572 @request.session[:user_id] = 2
1571 1573 post :bulk_update, :ids => [1, 3],
1572 1574 :notes => 'Bulk editing parent',
1573 1575 :issue => {:priority_id => '', :assigned_to_id => '', :status_id => '', :parent_issue_id => '2'}
1574 1576
1575 1577 assert_response 302
1576 1578 parent = Issue.find(2)
1577 1579 assert_equal parent.id, Issue.find(1).parent_id
1578 1580 assert_equal parent.id, Issue.find(3).parent_id
1579 1581 assert_equal [1, 3], parent.children.collect(&:id).sort
1580 1582 end
1581 1583
1582 1584 def test_bulk_update_custom_field
1583 1585 @request.session[:user_id] = 2
1584 1586 # update issues priority
1585 1587 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field',
1586 1588 :issue => {:priority_id => '',
1587 1589 :assigned_to_id => '',
1588 1590 :custom_field_values => {'2' => '777'}}
1589 1591
1590 1592 assert_response 302
1591 1593
1592 1594 issue = Issue.find(1)
1593 1595 journal = issue.journals.find(:first, :order => 'created_on DESC')
1594 1596 assert_equal '777', issue.custom_value_for(2).value
1595 1597 assert_equal 1, journal.details.size
1596 1598 assert_equal '125', journal.details.first.old_value
1597 1599 assert_equal '777', journal.details.first.value
1598 1600 end
1599 1601
1600 1602 def test_bulk_update_unassign
1601 1603 assert_not_nil Issue.find(2).assigned_to
1602 1604 @request.session[:user_id] = 2
1603 1605 # unassign issues
1604 1606 post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'}
1605 1607 assert_response 302
1606 1608 # check that the issues were updated
1607 1609 assert_nil Issue.find(2).assigned_to
1608 1610 end
1609 1611
1610 1612 def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject
1611 1613 @request.session[:user_id] = 2
1612 1614
1613 1615 post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4}
1614 1616
1615 1617 assert_response :redirect
1616 1618 issues = Issue.find([1,2])
1617 1619 issues.each do |issue|
1618 1620 assert_equal 4, issue.fixed_version_id
1619 1621 assert_not_equal issue.project_id, issue.fixed_version.project_id
1620 1622 end
1621 1623 end
1622 1624
1623 1625 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
1624 1626 @request.session[:user_id] = 2
1625 1627 post :bulk_update, :ids => [1,2], :back_url => '/issues'
1626 1628
1627 1629 assert_response :redirect
1628 1630 assert_redirected_to '/issues'
1629 1631 end
1630 1632
1631 1633 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1632 1634 @request.session[:user_id] = 2
1633 1635 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
1634 1636
1635 1637 assert_response :redirect
1636 1638 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
1637 1639 end
1638 1640
1639 1641 def test_destroy_issue_with_no_time_entries
1640 1642 assert_nil TimeEntry.find_by_issue_id(2)
1641 1643 @request.session[:user_id] = 2
1642 1644 post :destroy, :id => 2
1643 1645 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1644 1646 assert_nil Issue.find_by_id(2)
1645 1647 end
1646 1648
1647 1649 def test_destroy_issues_with_time_entries
1648 1650 @request.session[:user_id] = 2
1649 1651 post :destroy, :ids => [1, 3]
1650 1652 assert_response :success
1651 1653 assert_template 'destroy'
1652 1654 assert_not_nil assigns(:hours)
1653 1655 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1654 1656 end
1655 1657
1656 1658 def test_destroy_issues_and_destroy_time_entries
1657 1659 @request.session[:user_id] = 2
1658 1660 post :destroy, :ids => [1, 3], :todo => 'destroy'
1659 1661 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1660 1662 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1661 1663 assert_nil TimeEntry.find_by_id([1, 2])
1662 1664 end
1663 1665
1664 1666 def test_destroy_issues_and_assign_time_entries_to_project
1665 1667 @request.session[:user_id] = 2
1666 1668 post :destroy, :ids => [1, 3], :todo => 'nullify'
1667 1669 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1668 1670 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1669 1671 assert_nil TimeEntry.find(1).issue_id
1670 1672 assert_nil TimeEntry.find(2).issue_id
1671 1673 end
1672 1674
1673 1675 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1674 1676 @request.session[:user_id] = 2
1675 1677 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1676 1678 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1677 1679 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1678 1680 assert_equal 2, TimeEntry.find(1).issue_id
1679 1681 assert_equal 2, TimeEntry.find(2).issue_id
1680 1682 end
1681 1683
1682 1684 def test_destroy_issues_from_different_projects
1683 1685 @request.session[:user_id] = 2
1684 1686 post :destroy, :ids => [1, 2, 6], :todo => 'destroy'
1685 1687 assert_redirected_to :controller => 'issues', :action => 'index'
1686 1688 assert !(Issue.find_by_id(1) || Issue.find_by_id(2) || Issue.find_by_id(6))
1687 1689 end
1688 1690
1689 1691 def test_destroy_parent_and_child_issues
1690 1692 parent = Issue.generate!(:project_id => 1, :tracker_id => 1)
1691 1693 child = Issue.generate!(:project_id => 1, :tracker_id => 1, :parent_issue_id => parent.id)
1692 1694 assert child.is_descendant_of?(parent.reload)
1693 1695
1694 1696 @request.session[:user_id] = 2
1695 1697 assert_difference 'Issue.count', -2 do
1696 1698 post :destroy, :ids => [parent.id, child.id], :todo => 'destroy'
1697 1699 end
1698 1700 assert_response 302
1699 1701 end
1700 1702
1701 1703 def test_default_search_scope
1702 1704 get :index
1703 1705 assert_tag :div, :attributes => {:id => 'quick-search'},
1704 1706 :child => {:tag => 'form',
1705 1707 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1706 1708 end
1707 1709 end
General Comments 0
You need to be logged in to leave comments. Login now