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