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