##// END OF EJS Templates
Ability to delete multiple attachments while updating an issue (#13072)....
Jean-Philippe Lang -
r15268:19492226f7b2
parent child
Show More
@@ -1,1708 +1,1723
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 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 Issue < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 include Redmine::Utils::DateCalculation
21 21 include Redmine::I18n
22 22 before_save :set_parent_id
23 23 include Redmine::NestedSet::IssueNestedSet
24 24
25 25 belongs_to :project
26 26 belongs_to :tracker
27 27 belongs_to :status, :class_name => 'IssueStatus'
28 28 belongs_to :author, :class_name => 'User'
29 29 belongs_to :assigned_to, :class_name => 'Principal'
30 30 belongs_to :fixed_version, :class_name => 'Version'
31 31 belongs_to :priority, :class_name => 'IssuePriority'
32 32 belongs_to :category, :class_name => 'IssueCategory'
33 33
34 34 has_many :journals, :as => :journalized, :dependent => :destroy, :inverse_of => :journalized
35 35 has_many :visible_journals,
36 36 lambda {where(["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false])},
37 37 :class_name => 'Journal',
38 38 :as => :journalized
39 39
40 40 has_many :time_entries, :dependent => :destroy
41 41 has_and_belongs_to_many :changesets, lambda {order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC")}
42 42
43 43 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
44 44 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
45 45
46 46 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
47 47 acts_as_customizable
48 48 acts_as_watchable
49 49 acts_as_searchable :columns => ['subject', "#{table_name}.description"],
50 50 :preload => [:project, :status, :tracker],
51 51 :scope => lambda {|options| options[:open_issues] ? self.open : self.all}
52 52
53 53 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
54 54 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
55 55 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
56 56
57 57 acts_as_activity_provider :scope => preload(:project, :author, :tracker, :status),
58 58 :author_key => :author_id
59 59
60 60 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
61 61
62 attr_accessor :deleted_attachment_ids
62 63 attr_reader :current_journal
63 64 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
64 65
65 66 validates_presence_of :subject, :project, :tracker
66 67 validates_presence_of :priority, :if => Proc.new {|issue| issue.new_record? || issue.priority_id_changed?}
67 68 validates_presence_of :status, :if => Proc.new {|issue| issue.new_record? || issue.status_id_changed?}
68 69 validates_presence_of :author, :if => Proc.new {|issue| issue.new_record? || issue.author_id_changed?}
69 70
70 71 validates_length_of :subject, :maximum => 255
71 72 validates_inclusion_of :done_ratio, :in => 0..100
72 73 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
73 74 validates :start_date, :date => true
74 75 validates :due_date, :date => true
75 76 validate :validate_issue, :validate_required_fields
76 77 attr_protected :id
77 78
78 79 scope :visible, lambda {|*args|
79 80 joins(:project).
80 81 where(Issue.visible_condition(args.shift || User.current, *args))
81 82 }
82 83
83 84 scope :open, lambda {|*args|
84 85 is_closed = args.size > 0 ? !args.first : false
85 86 joins(:status).
86 87 where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
87 88 }
88 89
89 90 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
90 91 scope :on_active_project, lambda {
91 92 joins(:project).
92 93 where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
93 94 }
94 95 scope :fixed_version, lambda {|versions|
95 96 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
96 97 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
97 98 }
98 99 scope :assigned_to, lambda {|arg|
99 100 arg = Array(arg).uniq
100 101 ids = arg.map {|p| p.is_a?(Principal) ? p.id : p}
101 102 ids += arg.select {|p| p.is_a?(User)}.map(&:group_ids).flatten.uniq
102 103 ids.compact!
103 104 ids.any? ? where(:assigned_to_id => ids) : none
104 105 }
105 106
106 107 before_validation :clear_disabled_fields
107 108 before_create :default_assign
108 109 before_save :close_duplicates, :update_done_ratio_from_issue_status,
109 110 :force_updated_on_change, :update_closed_on, :set_assigned_to_was
110 111 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
111 112 after_save :reschedule_following_issues, :update_nested_set_attributes,
112 :update_parent_attributes, :create_journal
113 :update_parent_attributes, :delete_selected_attachments, :create_journal
113 114 # Should be after_create but would be called before previous after_save callbacks
114 115 after_save :after_create_from_copy
115 116 after_destroy :update_parent_attributes
116 117 after_create :send_notification
117 118 # Keep it at the end of after_save callbacks
118 119 after_save :clear_assigned_to_was
119 120
120 121 # Returns a SQL conditions string used to find all issues visible by the specified user
121 122 def self.visible_condition(user, options={})
122 123 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
123 124 sql = if user.id && user.logged?
124 125 case role.issues_visibility
125 126 when 'all'
126 127 '1=1'
127 128 when 'default'
128 129 user_ids = [user.id] + user.groups.map(&:id).compact
129 130 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
130 131 when 'own'
131 132 user_ids = [user.id] + user.groups.map(&:id).compact
132 133 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
133 134 else
134 135 '1=0'
135 136 end
136 137 else
137 138 "(#{table_name}.is_private = #{connection.quoted_false})"
138 139 end
139 140 unless role.permissions_all_trackers?(:view_issues)
140 141 tracker_ids = role.permissions_tracker_ids(:view_issues)
141 142 if tracker_ids.any?
142 143 sql = "(#{sql} AND #{table_name}.tracker_id IN (#{tracker_ids.join(',')}))"
143 144 else
144 145 sql = '1=0'
145 146 end
146 147 end
147 148 sql
148 149 end
149 150 end
150 151
151 152 # Returns true if usr or current user is allowed to view the issue
152 153 def visible?(usr=nil)
153 154 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
154 155 visible = if user.logged?
155 156 case role.issues_visibility
156 157 when 'all'
157 158 true
158 159 when 'default'
159 160 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
160 161 when 'own'
161 162 self.author == user || user.is_or_belongs_to?(assigned_to)
162 163 else
163 164 false
164 165 end
165 166 else
166 167 !self.is_private?
167 168 end
168 169 unless role.permissions_all_trackers?(:view_issues)
169 170 visible &&= role.permissions_tracker_ids?(:view_issues, tracker_id)
170 171 end
171 172 visible
172 173 end
173 174 end
174 175
175 176 # Returns true if user or current user is allowed to edit or add notes to the issue
176 177 def editable?(user=User.current)
177 178 attributes_editable?(user) || notes_addable?(user)
178 179 end
179 180
180 181 # Returns true if user or current user is allowed to edit the issue
181 182 def attributes_editable?(user=User.current)
182 183 user_tracker_permission?(user, :edit_issues)
183 184 end
184 185
185 186 # Overrides Redmine::Acts::Attachable::InstanceMethods#attachments_editable?
186 187 def attachments_editable?(user=User.current)
187 188 attributes_editable?(user)
188 189 end
189 190
190 191 # Returns true if user or current user is allowed to add notes to the issue
191 192 def notes_addable?(user=User.current)
192 193 user_tracker_permission?(user, :add_issue_notes)
193 194 end
194 195
195 196 # Returns true if user or current user is allowed to delete the issue
196 197 def deletable?(user=User.current)
197 198 user_tracker_permission?(user, :delete_issues)
198 199 end
199 200
200 201 def initialize(attributes=nil, *args)
201 202 super
202 203 if new_record?
203 204 # set default values for new records only
204 205 self.priority ||= IssuePriority.default
205 206 self.watcher_user_ids = []
206 207 end
207 208 end
208 209
209 210 def create_or_update
210 211 super
211 212 ensure
212 213 @status_was = nil
213 214 end
214 215 private :create_or_update
215 216
216 217 # AR#Persistence#destroy would raise and RecordNotFound exception
217 218 # if the issue was already deleted or updated (non matching lock_version).
218 219 # This is a problem when bulk deleting issues or deleting a project
219 220 # (because an issue may already be deleted if its parent was deleted
220 221 # first).
221 222 # The issue is reloaded by the nested_set before being deleted so
222 223 # the lock_version condition should not be an issue but we handle it.
223 224 def destroy
224 225 super
225 226 rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotFound
226 227 # Stale or already deleted
227 228 begin
228 229 reload
229 230 rescue ActiveRecord::RecordNotFound
230 231 # The issue was actually already deleted
231 232 @destroyed = true
232 233 return freeze
233 234 end
234 235 # The issue was stale, retry to destroy
235 236 super
236 237 end
237 238
238 239 alias :base_reload :reload
239 240 def reload(*args)
240 241 @workflow_rule_by_attribute = nil
241 242 @assignable_versions = nil
242 243 @relations = nil
243 244 @spent_hours = nil
244 245 @total_spent_hours = nil
245 246 @total_estimated_hours = nil
246 247 base_reload(*args)
247 248 end
248 249
249 250 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
250 251 def available_custom_fields
251 252 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
252 253 end
253 254
254 255 def visible_custom_field_values(user=nil)
255 256 user_real = user || User.current
256 257 custom_field_values.select do |value|
257 258 value.custom_field.visible_by?(project, user_real)
258 259 end
259 260 end
260 261
261 262 # Copies attributes from another issue, arg can be an id or an Issue
262 263 def copy_from(arg, options={})
263 264 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
264 265 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
265 266 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
266 267 self.status = issue.status
267 268 self.author = User.current
268 269 unless options[:attachments] == false
269 270 self.attachments = issue.attachments.map do |attachement|
270 271 attachement.copy(:container => self)
271 272 end
272 273 end
273 274 @copied_from = issue
274 275 @copy_options = options
275 276 self
276 277 end
277 278
278 279 # Returns an unsaved copy of the issue
279 280 def copy(attributes=nil, copy_options={})
280 281 copy = self.class.new.copy_from(self, copy_options)
281 282 copy.attributes = attributes if attributes
282 283 copy
283 284 end
284 285
285 286 # Returns true if the issue is a copy
286 287 def copy?
287 288 @copied_from.present?
288 289 end
289 290
290 291 def status_id=(status_id)
291 292 if status_id.to_s != self.status_id.to_s
292 293 self.status = (status_id.present? ? IssueStatus.find_by_id(status_id) : nil)
293 294 end
294 295 self.status_id
295 296 end
296 297
297 298 # Sets the status.
298 299 def status=(status)
299 300 if status != self.status
300 301 @workflow_rule_by_attribute = nil
301 302 end
302 303 association(:status).writer(status)
303 304 end
304 305
305 306 def priority_id=(pid)
306 307 self.priority = nil
307 308 write_attribute(:priority_id, pid)
308 309 end
309 310
310 311 def category_id=(cid)
311 312 self.category = nil
312 313 write_attribute(:category_id, cid)
313 314 end
314 315
315 316 def fixed_version_id=(vid)
316 317 self.fixed_version = nil
317 318 write_attribute(:fixed_version_id, vid)
318 319 end
319 320
320 321 def tracker_id=(tracker_id)
321 322 if tracker_id.to_s != self.tracker_id.to_s
322 323 self.tracker = (tracker_id.present? ? Tracker.find_by_id(tracker_id) : nil)
323 324 end
324 325 self.tracker_id
325 326 end
326 327
327 328 # Sets the tracker.
328 329 # This will set the status to the default status of the new tracker if:
329 330 # * the status was the default for the previous tracker
330 331 # * or if the status was not part of the new tracker statuses
331 332 # * or the status was nil
332 333 def tracker=(tracker)
333 334 tracker_was = self.tracker
334 335 association(:tracker).writer(tracker)
335 336 if tracker != tracker_was
336 337 if status == tracker_was.try(:default_status)
337 338 self.status = nil
338 339 elsif status && tracker && !tracker.issue_status_ids.include?(status.id)
339 340 self.status = nil
340 341 end
341 342 reassign_custom_field_values
342 343 @workflow_rule_by_attribute = nil
343 344 end
344 345 self.status ||= default_status
345 346 self.tracker
346 347 end
347 348
348 349 def project_id=(project_id)
349 350 if project_id.to_s != self.project_id.to_s
350 351 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
351 352 end
352 353 self.project_id
353 354 end
354 355
355 356 # Sets the project.
356 357 # Unless keep_tracker argument is set to true, this will change the tracker
357 358 # to the first tracker of the new project if the previous tracker is not part
358 359 # of the new project trackers.
359 360 # This will:
360 361 # * clear the fixed_version is it's no longer valid for the new project.
361 362 # * clear the parent issue if it's no longer valid for the new project.
362 363 # * set the category to the category with the same name in the new
363 364 # project if it exists, or clear it if it doesn't.
364 365 # * for new issue, set the fixed_version to the project default version
365 366 # if it's a valid fixed_version.
366 367 def project=(project, keep_tracker=false)
367 368 project_was = self.project
368 369 association(:project).writer(project)
369 370 if project_was && project && project_was != project
370 371 @assignable_versions = nil
371 372
372 373 unless keep_tracker || project.trackers.include?(tracker)
373 374 self.tracker = project.trackers.first
374 375 end
375 376 # Reassign to the category with same name if any
376 377 if category
377 378 self.category = project.issue_categories.find_by_name(category.name)
378 379 end
379 380 # Keep the fixed_version if it's still valid in the new_project
380 381 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
381 382 self.fixed_version = nil
382 383 end
383 384 # Clear the parent task if it's no longer valid
384 385 unless valid_parent_project?
385 386 self.parent_issue_id = nil
386 387 end
387 388 reassign_custom_field_values
388 389 @workflow_rule_by_attribute = nil
389 390 end
390 391 # Set fixed_version to the project default version if it's valid
391 392 if new_record? && fixed_version.nil? && project && project.default_version_id?
392 393 if project.shared_versions.open.exists?(project.default_version_id)
393 394 self.fixed_version_id = project.default_version_id
394 395 end
395 396 end
396 397 self.project
397 398 end
398 399
399 400 def description=(arg)
400 401 if arg.is_a?(String)
401 402 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
402 403 end
403 404 write_attribute(:description, arg)
404 405 end
405 406
407 def deleted_attachment_ids
408 Array(@deleted_attachment_ids).map(&:to_i)
409 end
410
406 411 # Overrides assign_attributes so that project and tracker get assigned first
407 412 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
408 413 return if new_attributes.nil?
409 414 attrs = new_attributes.dup
410 415 attrs.stringify_keys!
411 416
412 417 %w(project project_id tracker tracker_id).each do |attr|
413 418 if attrs.has_key?(attr)
414 419 send "#{attr}=", attrs.delete(attr)
415 420 end
416 421 end
417 422 send :assign_attributes_without_project_and_tracker_first, attrs, *args
418 423 end
419 424 # Do not redefine alias chain on reload (see #4838)
420 425 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
421 426
422 427 def attributes=(new_attributes)
423 428 assign_attributes new_attributes
424 429 end
425 430
426 431 def estimated_hours=(h)
427 432 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
428 433 end
429 434
430 435 safe_attributes 'project_id',
431 436 'tracker_id',
432 437 'status_id',
433 438 'category_id',
434 439 'assigned_to_id',
435 440 'priority_id',
436 441 'fixed_version_id',
437 442 'subject',
438 443 'description',
439 444 'start_date',
440 445 'due_date',
441 446 'done_ratio',
442 447 'estimated_hours',
443 448 'custom_field_values',
444 449 'custom_fields',
445 450 'lock_version',
446 451 'notes',
447 452 :if => lambda {|issue, user| issue.new_record? || issue.attributes_editable?(user) }
448 453
449 454 safe_attributes 'notes',
450 455 :if => lambda {|issue, user| issue.notes_addable?(user)}
451 456
452 457 safe_attributes 'private_notes',
453 458 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
454 459
455 460 safe_attributes 'watcher_user_ids',
456 461 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
457 462
458 463 safe_attributes 'is_private',
459 464 :if => lambda {|issue, user|
460 465 user.allowed_to?(:set_issues_private, issue.project) ||
461 466 (issue.author_id == user.id && user.allowed_to?(:set_own_issues_private, issue.project))
462 467 }
463 468
464 469 safe_attributes 'parent_issue_id',
465 470 :if => lambda {|issue, user| (issue.new_record? || issue.attributes_editable?(user)) &&
466 471 user.allowed_to?(:manage_subtasks, issue.project)}
467 472
473 safe_attributes 'deleted_attachment_ids',
474 :if => lambda {|issue, user| issue.attachments_deletable?(user)}
475
468 476 def safe_attribute_names(user=nil)
469 477 names = super
470 478 names -= disabled_core_fields
471 479 names -= read_only_attribute_names(user)
472 480 if new_record?
473 481 # Make sure that project_id can always be set for new issues
474 482 names |= %w(project_id)
475 483 end
476 484 if dates_derived?
477 485 names -= %w(start_date due_date)
478 486 end
479 487 if priority_derived?
480 488 names -= %w(priority_id)
481 489 end
482 490 if done_ratio_derived?
483 491 names -= %w(done_ratio)
484 492 end
485 493 names
486 494 end
487 495
488 496 # Safely sets attributes
489 497 # Should be called from controllers instead of #attributes=
490 498 # attr_accessible is too rough because we still want things like
491 499 # Issue.new(:project => foo) to work
492 500 def safe_attributes=(attrs, user=User.current)
493 501 return unless attrs.is_a?(Hash)
494 502
495 503 attrs = attrs.deep_dup
496 504
497 505 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
498 506 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
499 507 if allowed_target_projects(user).where(:id => p.to_i).exists?
500 508 self.project_id = p
501 509 end
502 510
503 511 if project_id_changed? && attrs['category_id'].to_s == category_id_was.to_s
504 512 # Discard submitted category on previous project
505 513 attrs.delete('category_id')
506 514 end
507 515 end
508 516
509 517 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
510 518 if allowed_target_trackers(user).where(:id => t.to_i).exists?
511 519 self.tracker_id = t
512 520 end
513 521 end
514 522 if project
515 523 # Set a default tracker to accept custom field values
516 524 # even if tracker is not specified
517 525 self.tracker ||= allowed_target_trackers(user).first
518 526 end
519 527
520 528 statuses_allowed = new_statuses_allowed_to(user)
521 529 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
522 530 if statuses_allowed.collect(&:id).include?(s.to_i)
523 531 self.status_id = s
524 532 end
525 533 end
526 534 if new_record? && !statuses_allowed.include?(status)
527 535 self.status = statuses_allowed.first || default_status
528 536 end
529 537 if (u = attrs.delete('assigned_to_id')) && safe_attribute?('assigned_to_id')
530 538 if u.blank?
531 539 self.assigned_to_id = nil
532 540 else
533 541 u = u.to_i
534 542 if assignable_users.any?{|assignable_user| assignable_user.id == u}
535 543 self.assigned_to_id = u
536 544 end
537 545 end
538 546 end
539 547
540 548
541 549 attrs = delete_unsafe_attributes(attrs, user)
542 550 return if attrs.empty?
543 551
544 552 if attrs['parent_issue_id'].present?
545 553 s = attrs['parent_issue_id'].to_s
546 554 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
547 555 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
548 556 end
549 557 end
550 558
551 559 if attrs['custom_field_values'].present?
552 560 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
553 561 attrs['custom_field_values'].select! {|k, v| editable_custom_field_ids.include?(k.to_s)}
554 562 end
555 563
556 564 if attrs['custom_fields'].present?
557 565 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
558 566 attrs['custom_fields'].select! {|c| editable_custom_field_ids.include?(c['id'].to_s)}
559 567 end
560 568
561 569 # mass-assignment security bypass
562 570 assign_attributes attrs, :without_protection => true
563 571 end
564 572
565 573 def disabled_core_fields
566 574 tracker ? tracker.disabled_core_fields : []
567 575 end
568 576
569 577 # Returns the custom_field_values that can be edited by the given user
570 578 def editable_custom_field_values(user=nil)
571 579 visible_custom_field_values(user).reject do |value|
572 580 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
573 581 end
574 582 end
575 583
576 584 # Returns the custom fields that can be edited by the given user
577 585 def editable_custom_fields(user=nil)
578 586 editable_custom_field_values(user).map(&:custom_field).uniq
579 587 end
580 588
581 589 # Returns the names of attributes that are read-only for user or the current user
582 590 # For users with multiple roles, the read-only fields are the intersection of
583 591 # read-only fields of each role
584 592 # The result is an array of strings where sustom fields are represented with their ids
585 593 #
586 594 # Examples:
587 595 # issue.read_only_attribute_names # => ['due_date', '2']
588 596 # issue.read_only_attribute_names(user) # => []
589 597 def read_only_attribute_names(user=nil)
590 598 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
591 599 end
592 600
593 601 # Returns the names of required attributes for user or the current user
594 602 # For users with multiple roles, the required fields are the intersection of
595 603 # required fields of each role
596 604 # The result is an array of strings where sustom fields are represented with their ids
597 605 #
598 606 # Examples:
599 607 # issue.required_attribute_names # => ['due_date', '2']
600 608 # issue.required_attribute_names(user) # => []
601 609 def required_attribute_names(user=nil)
602 610 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
603 611 end
604 612
605 613 # Returns true if the attribute is required for user
606 614 def required_attribute?(name, user=nil)
607 615 required_attribute_names(user).include?(name.to_s)
608 616 end
609 617
610 618 # Returns a hash of the workflow rule by attribute for the given user
611 619 #
612 620 # Examples:
613 621 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
614 622 def workflow_rule_by_attribute(user=nil)
615 623 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
616 624
617 625 user_real = user || User.current
618 626 roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project)
619 627 roles = roles.select(&:consider_workflow?)
620 628 return {} if roles.empty?
621 629
622 630 result = {}
623 631 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).to_a
624 632 if workflow_permissions.any?
625 633 workflow_rules = workflow_permissions.inject({}) do |h, wp|
626 634 h[wp.field_name] ||= {}
627 635 h[wp.field_name][wp.role_id] = wp.rule
628 636 h
629 637 end
630 638 fields_with_roles = {}
631 639 IssueCustomField.where(:visible => false).joins(:roles).pluck(:id, "role_id").each do |field_id, role_id|
632 640 fields_with_roles[field_id] ||= []
633 641 fields_with_roles[field_id] << role_id
634 642 end
635 643 roles.each do |role|
636 644 fields_with_roles.each do |field_id, role_ids|
637 645 unless role_ids.include?(role.id)
638 646 field_name = field_id.to_s
639 647 workflow_rules[field_name] ||= {}
640 648 workflow_rules[field_name][role.id] = 'readonly'
641 649 end
642 650 end
643 651 end
644 652 workflow_rules.each do |attr, rules|
645 653 next if rules.size < roles.size
646 654 uniq_rules = rules.values.uniq
647 655 if uniq_rules.size == 1
648 656 result[attr] = uniq_rules.first
649 657 else
650 658 result[attr] = 'required'
651 659 end
652 660 end
653 661 end
654 662 @workflow_rule_by_attribute = result if user.nil?
655 663 result
656 664 end
657 665 private :workflow_rule_by_attribute
658 666
659 667 def done_ratio
660 668 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
661 669 status.default_done_ratio
662 670 else
663 671 read_attribute(:done_ratio)
664 672 end
665 673 end
666 674
667 675 def self.use_status_for_done_ratio?
668 676 Setting.issue_done_ratio == 'issue_status'
669 677 end
670 678
671 679 def self.use_field_for_done_ratio?
672 680 Setting.issue_done_ratio == 'issue_field'
673 681 end
674 682
675 683 def validate_issue
676 684 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
677 685 errors.add :due_date, :greater_than_start_date
678 686 end
679 687
680 688 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
681 689 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
682 690 end
683 691
684 692 if fixed_version
685 693 if !assignable_versions.include?(fixed_version)
686 694 errors.add :fixed_version_id, :inclusion
687 695 elsif reopening? && fixed_version.closed?
688 696 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
689 697 end
690 698 end
691 699
692 700 # Checks that the issue can not be added/moved to a disabled tracker
693 701 if project && (tracker_id_changed? || project_id_changed?)
694 702 if tracker && !project.trackers.include?(tracker)
695 703 errors.add :tracker_id, :inclusion
696 704 end
697 705 end
698 706
699 707 # Checks parent issue assignment
700 708 if @invalid_parent_issue_id.present?
701 709 errors.add :parent_issue_id, :invalid
702 710 elsif @parent_issue
703 711 if !valid_parent_project?(@parent_issue)
704 712 errors.add :parent_issue_id, :invalid
705 713 elsif (@parent_issue != parent) && (
706 714 self.would_reschedule?(@parent_issue) ||
707 715 @parent_issue.self_and_ancestors.any? {|a| a.relations_from.any? {|r| r.relation_type == IssueRelation::TYPE_PRECEDES && r.issue_to.would_reschedule?(self)}}
708 716 )
709 717 errors.add :parent_issue_id, :invalid
710 718 elsif !new_record?
711 719 # moving an existing issue
712 720 if move_possible?(@parent_issue)
713 721 # move accepted
714 722 else
715 723 errors.add :parent_issue_id, :invalid
716 724 end
717 725 end
718 726 end
719 727 end
720 728
721 729 # Validates the issue against additional workflow requirements
722 730 def validate_required_fields
723 731 user = new_record? ? author : current_journal.try(:user)
724 732
725 733 required_attribute_names(user).each do |attribute|
726 734 if attribute =~ /^\d+$/
727 735 attribute = attribute.to_i
728 736 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
729 737 if v && Array(v.value).detect(&:present?).nil?
730 738 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
731 739 end
732 740 else
733 741 if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
734 742 next if attribute == 'category_id' && project.try(:issue_categories).blank?
735 743 next if attribute == 'fixed_version_id' && assignable_versions.blank?
736 744 errors.add attribute, :blank
737 745 end
738 746 end
739 747 end
740 748 end
741 749
742 750 # Overrides Redmine::Acts::Customizable::InstanceMethods#validate_custom_field_values
743 751 # so that custom values that are not editable are not validated (eg. a custom field that
744 752 # is marked as required should not trigger a validation error if the user is not allowed
745 753 # to edit this field).
746 754 def validate_custom_field_values
747 755 user = new_record? ? author : current_journal.try(:user)
748 756 if new_record? || custom_field_values_changed?
749 757 editable_custom_field_values(user).each(&:validate_value)
750 758 end
751 759 end
752 760
753 761 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
754 762 # even if the user turns off the setting later
755 763 def update_done_ratio_from_issue_status
756 764 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
757 765 self.done_ratio = status.default_done_ratio
758 766 end
759 767 end
760 768
761 769 def init_journal(user, notes = "")
762 770 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
763 771 end
764 772
765 773 # Returns the current journal or nil if it's not initialized
766 774 def current_journal
767 775 @current_journal
768 776 end
769 777
770 778 # Returns the names of attributes that are journalized when updating the issue
771 779 def journalized_attribute_names
772 780 names = Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)
773 781 if tracker
774 782 names -= tracker.disabled_core_fields
775 783 end
776 784 names
777 785 end
778 786
779 787 # Returns the id of the last journal or nil
780 788 def last_journal_id
781 789 if new_record?
782 790 nil
783 791 else
784 792 journals.maximum(:id)
785 793 end
786 794 end
787 795
788 796 # Returns a scope for journals that have an id greater than journal_id
789 797 def journals_after(journal_id)
790 798 scope = journals.reorder("#{Journal.table_name}.id ASC")
791 799 if journal_id.present?
792 800 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
793 801 end
794 802 scope
795 803 end
796 804
797 805 # Returns the initial status of the issue
798 806 # Returns nil for a new issue
799 807 def status_was
800 808 if status_id_changed?
801 809 if status_id_was.to_i > 0
802 810 @status_was ||= IssueStatus.find_by_id(status_id_was)
803 811 end
804 812 else
805 813 @status_was ||= status
806 814 end
807 815 end
808 816
809 817 # Return true if the issue is closed, otherwise false
810 818 def closed?
811 819 status.present? && status.is_closed?
812 820 end
813 821
814 822 # Returns true if the issue was closed when loaded
815 823 def was_closed?
816 824 status_was.present? && status_was.is_closed?
817 825 end
818 826
819 827 # Return true if the issue is being reopened
820 828 def reopening?
821 829 if new_record?
822 830 false
823 831 else
824 832 status_id_changed? && !closed? && was_closed?
825 833 end
826 834 end
827 835 alias :reopened? :reopening?
828 836
829 837 # Return true if the issue is being closed
830 838 def closing?
831 839 if new_record?
832 840 closed?
833 841 else
834 842 status_id_changed? && closed? && !was_closed?
835 843 end
836 844 end
837 845
838 846 # Returns true if the issue is overdue
839 847 def overdue?
840 848 due_date.present? && (due_date < User.current.today) && !closed?
841 849 end
842 850
843 851 # Is the amount of work done less than it should for the due date
844 852 def behind_schedule?
845 853 return false if start_date.nil? || due_date.nil?
846 854 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
847 855 return done_date <= User.current.today
848 856 end
849 857
850 858 # Does this issue have children?
851 859 def children?
852 860 !leaf?
853 861 end
854 862
855 863 # Users the issue can be assigned to
856 864 def assignable_users
857 865 users = project.assignable_users(tracker).to_a
858 866 users << author if author && author.active?
859 867 users << assigned_to if assigned_to
860 868 users.uniq.sort
861 869 end
862 870
863 871 # Versions that the issue can be assigned to
864 872 def assignable_versions
865 873 return @assignable_versions if @assignable_versions
866 874
867 875 versions = project.shared_versions.open.to_a
868 876 if fixed_version
869 877 if fixed_version_id_changed?
870 878 # nothing to do
871 879 elsif project_id_changed?
872 880 if project.shared_versions.include?(fixed_version)
873 881 versions << fixed_version
874 882 end
875 883 else
876 884 versions << fixed_version
877 885 end
878 886 end
879 887 @assignable_versions = versions.uniq.sort
880 888 end
881 889
882 890 # Returns true if this issue is blocked by another issue that is still open
883 891 def blocked?
884 892 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
885 893 end
886 894
887 895 # Returns the default status of the issue based on its tracker
888 896 # Returns nil if tracker is nil
889 897 def default_status
890 898 tracker.try(:default_status)
891 899 end
892 900
893 901 # Returns an array of statuses that user is able to apply
894 902 def new_statuses_allowed_to(user=User.current, include_default=false)
895 903 if new_record? && @copied_from
896 904 [default_status, @copied_from.status].compact.uniq.sort
897 905 else
898 906 initial_status = nil
899 907 if new_record?
900 908 # nop
901 909 elsif tracker_id_changed?
902 910 if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
903 911 initial_status = default_status
904 912 elsif tracker.issue_status_ids.include?(status_id_was)
905 913 initial_status = IssueStatus.find_by_id(status_id_was)
906 914 else
907 915 initial_status = default_status
908 916 end
909 917 else
910 918 initial_status = status_was
911 919 end
912 920
913 921 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
914 922 assignee_transitions_allowed = initial_assigned_to_id.present? &&
915 923 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
916 924
917 925 statuses = []
918 926 statuses += IssueStatus.new_statuses_allowed(
919 927 initial_status,
920 928 user.admin ? Role.all.to_a : user.roles_for_project(project),
921 929 tracker,
922 930 author == user,
923 931 assignee_transitions_allowed
924 932 )
925 933 statuses << initial_status unless statuses.empty?
926 934 statuses << default_status if include_default || (new_record? && statuses.empty?)
927 935 statuses = statuses.compact.uniq.sort
928 936 if blocked?
929 937 statuses.reject!(&:is_closed?)
930 938 end
931 939 statuses
932 940 end
933 941 end
934 942
935 943 # Returns the previous assignee (user or group) if changed
936 944 def assigned_to_was
937 945 # assigned_to_id_was is reset before after_save callbacks
938 946 user_id = @previous_assigned_to_id || assigned_to_id_was
939 947 if user_id && user_id != assigned_to_id
940 948 @assigned_to_was ||= Principal.find_by_id(user_id)
941 949 end
942 950 end
943 951
944 952 # Returns the original tracker
945 953 def tracker_was
946 954 Tracker.find_by_id(tracker_id_was)
947 955 end
948 956
949 957 # Returns the users that should be notified
950 958 def notified_users
951 959 notified = []
952 960 # Author and assignee are always notified unless they have been
953 961 # locked or don't want to be notified
954 962 notified << author if author
955 963 if assigned_to
956 964 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
957 965 end
958 966 if assigned_to_was
959 967 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
960 968 end
961 969 notified = notified.select {|u| u.active? && u.notify_about?(self)}
962 970
963 971 notified += project.notified_users
964 972 notified.uniq!
965 973 # Remove users that can not view the issue
966 974 notified.reject! {|user| !visible?(user)}
967 975 notified
968 976 end
969 977
970 978 # Returns the email addresses that should be notified
971 979 def recipients
972 980 notified_users.collect(&:mail)
973 981 end
974 982
975 983 def each_notification(users, &block)
976 984 if users.any?
977 985 if custom_field_values.detect {|value| !value.custom_field.visible?}
978 986 users_by_custom_field_visibility = users.group_by do |user|
979 987 visible_custom_field_values(user).map(&:custom_field_id).sort
980 988 end
981 989 users_by_custom_field_visibility.values.each do |users|
982 990 yield(users)
983 991 end
984 992 else
985 993 yield(users)
986 994 end
987 995 end
988 996 end
989 997
990 998 def notify?
991 999 @notify != false
992 1000 end
993 1001
994 1002 def notify=(arg)
995 1003 @notify = arg
996 1004 end
997 1005
998 1006 # Returns the number of hours spent on this issue
999 1007 def spent_hours
1000 1008 @spent_hours ||= time_entries.sum(:hours) || 0
1001 1009 end
1002 1010
1003 1011 # Returns the total number of hours spent on this issue and its descendants
1004 1012 def total_spent_hours
1005 1013 @total_spent_hours ||= if leaf?
1006 1014 spent_hours
1007 1015 else
1008 1016 self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f || 0.0
1009 1017 end
1010 1018 end
1011 1019
1012 1020 def total_estimated_hours
1013 1021 if leaf?
1014 1022 estimated_hours
1015 1023 else
1016 1024 @total_estimated_hours ||= self_and_descendants.sum(:estimated_hours)
1017 1025 end
1018 1026 end
1019 1027
1020 1028 def relations
1021 1029 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
1022 1030 end
1023 1031
1024 1032 # Preloads relations for a collection of issues
1025 1033 def self.load_relations(issues)
1026 1034 if issues.any?
1027 1035 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
1028 1036 issues.each do |issue|
1029 1037 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
1030 1038 end
1031 1039 end
1032 1040 end
1033 1041
1034 1042 # Preloads visible spent time for a collection of issues
1035 1043 def self.load_visible_spent_hours(issues, user=User.current)
1036 1044 if issues.any?
1037 1045 hours_by_issue_id = TimeEntry.visible(user).where(:issue_id => issues.map(&:id)).group(:issue_id).sum(:hours)
1038 1046 issues.each do |issue|
1039 1047 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
1040 1048 end
1041 1049 end
1042 1050 end
1043 1051
1044 1052 # Preloads visible total spent time for a collection of issues
1045 1053 def self.load_visible_total_spent_hours(issues, user=User.current)
1046 1054 if issues.any?
1047 1055 hours_by_issue_id = TimeEntry.visible(user).joins(:issue).
1048 1056 joins("JOIN #{Issue.table_name} parent ON parent.root_id = #{Issue.table_name}.root_id" +
1049 1057 " AND parent.lft <= #{Issue.table_name}.lft AND parent.rgt >= #{Issue.table_name}.rgt").
1050 1058 where("parent.id IN (?)", issues.map(&:id)).group("parent.id").sum(:hours)
1051 1059 issues.each do |issue|
1052 1060 issue.instance_variable_set "@total_spent_hours", (hours_by_issue_id[issue.id] || 0)
1053 1061 end
1054 1062 end
1055 1063 end
1056 1064
1057 1065 # Preloads visible relations for a collection of issues
1058 1066 def self.load_visible_relations(issues, user=User.current)
1059 1067 if issues.any?
1060 1068 issue_ids = issues.map(&:id)
1061 1069 # Relations with issue_from in given issues and visible issue_to
1062 1070 relations_from = IssueRelation.joins(:issue_to => :project).
1063 1071 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
1064 1072 # Relations with issue_to in given issues and visible issue_from
1065 1073 relations_to = IssueRelation.joins(:issue_from => :project).
1066 1074 where(visible_condition(user)).
1067 1075 where(:issue_to_id => issue_ids).to_a
1068 1076 issues.each do |issue|
1069 1077 relations =
1070 1078 relations_from.select {|relation| relation.issue_from_id == issue.id} +
1071 1079 relations_to.select {|relation| relation.issue_to_id == issue.id}
1072 1080
1073 1081 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
1074 1082 end
1075 1083 end
1076 1084 end
1077 1085
1078 1086 # Finds an issue relation given its id.
1079 1087 def find_relation(relation_id)
1080 1088 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
1081 1089 end
1082 1090
1083 1091 # Returns true if this issue blocks the other issue, otherwise returns false
1084 1092 def blocks?(other)
1085 1093 all = [self]
1086 1094 last = [self]
1087 1095 while last.any?
1088 1096 current = last.map {|i| i.relations_from.where(:relation_type => IssueRelation::TYPE_BLOCKS).map(&:issue_to)}.flatten.uniq
1089 1097 current -= last
1090 1098 current -= all
1091 1099 return true if current.include?(other)
1092 1100 last = current
1093 1101 all += last
1094 1102 end
1095 1103 false
1096 1104 end
1097 1105
1098 1106 # Returns true if the other issue might be rescheduled if the start/due dates of this issue change
1099 1107 def would_reschedule?(other)
1100 1108 all = [self]
1101 1109 last = [self]
1102 1110 while last.any?
1103 1111 current = last.map {|i|
1104 1112 i.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to) +
1105 1113 i.leaves.to_a +
1106 1114 i.ancestors.map {|a| a.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to)}
1107 1115 }.flatten.uniq
1108 1116 current -= last
1109 1117 current -= all
1110 1118 return true if current.include?(other)
1111 1119 last = current
1112 1120 all += last
1113 1121 end
1114 1122 false
1115 1123 end
1116 1124
1117 1125 # Returns an array of issues that duplicate this one
1118 1126 def duplicates
1119 1127 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1120 1128 end
1121 1129
1122 1130 # Returns the due date or the target due date if any
1123 1131 # Used on gantt chart
1124 1132 def due_before
1125 1133 due_date || (fixed_version ? fixed_version.effective_date : nil)
1126 1134 end
1127 1135
1128 1136 # Returns the time scheduled for this issue.
1129 1137 #
1130 1138 # Example:
1131 1139 # Start Date: 2/26/09, End Date: 3/04/09
1132 1140 # duration => 6
1133 1141 def duration
1134 1142 (start_date && due_date) ? due_date - start_date : 0
1135 1143 end
1136 1144
1137 1145 # Returns the duration in working days
1138 1146 def working_duration
1139 1147 (start_date && due_date) ? working_days(start_date, due_date) : 0
1140 1148 end
1141 1149
1142 1150 def soonest_start(reload=false)
1143 1151 if @soonest_start.nil? || reload
1144 1152 dates = relations_to(reload).collect{|relation| relation.successor_soonest_start}
1145 1153 p = @parent_issue || parent
1146 1154 if p && Setting.parent_issue_dates == 'derived'
1147 1155 dates << p.soonest_start
1148 1156 end
1149 1157 @soonest_start = dates.compact.max
1150 1158 end
1151 1159 @soonest_start
1152 1160 end
1153 1161
1154 1162 # Sets start_date on the given date or the next working day
1155 1163 # and changes due_date to keep the same working duration.
1156 1164 def reschedule_on(date)
1157 1165 wd = working_duration
1158 1166 date = next_working_date(date)
1159 1167 self.start_date = date
1160 1168 self.due_date = add_working_days(date, wd)
1161 1169 end
1162 1170
1163 1171 # Reschedules the issue on the given date or the next working day and saves the record.
1164 1172 # If the issue is a parent task, this is done by rescheduling its subtasks.
1165 1173 def reschedule_on!(date)
1166 1174 return if date.nil?
1167 1175 if leaf? || !dates_derived?
1168 1176 if start_date.nil? || start_date != date
1169 1177 if start_date && start_date > date
1170 1178 # Issue can not be moved earlier than its soonest start date
1171 1179 date = [soonest_start(true), date].compact.max
1172 1180 end
1173 1181 reschedule_on(date)
1174 1182 begin
1175 1183 save
1176 1184 rescue ActiveRecord::StaleObjectError
1177 1185 reload
1178 1186 reschedule_on(date)
1179 1187 save
1180 1188 end
1181 1189 end
1182 1190 else
1183 1191 leaves.each do |leaf|
1184 1192 if leaf.start_date
1185 1193 # Only move subtask if it starts at the same date as the parent
1186 1194 # or if it starts before the given date
1187 1195 if start_date == leaf.start_date || date > leaf.start_date
1188 1196 leaf.reschedule_on!(date)
1189 1197 end
1190 1198 else
1191 1199 leaf.reschedule_on!(date)
1192 1200 end
1193 1201 end
1194 1202 end
1195 1203 end
1196 1204
1197 1205 def dates_derived?
1198 1206 !leaf? && Setting.parent_issue_dates == 'derived'
1199 1207 end
1200 1208
1201 1209 def priority_derived?
1202 1210 !leaf? && Setting.parent_issue_priority == 'derived'
1203 1211 end
1204 1212
1205 1213 def done_ratio_derived?
1206 1214 !leaf? && Setting.parent_issue_done_ratio == 'derived'
1207 1215 end
1208 1216
1209 1217 def <=>(issue)
1210 1218 if issue.nil?
1211 1219 -1
1212 1220 elsif root_id != issue.root_id
1213 1221 (root_id || 0) <=> (issue.root_id || 0)
1214 1222 else
1215 1223 (lft || 0) <=> (issue.lft || 0)
1216 1224 end
1217 1225 end
1218 1226
1219 1227 def to_s
1220 1228 "#{tracker} ##{id}: #{subject}"
1221 1229 end
1222 1230
1223 1231 # Returns a string of css classes that apply to the issue
1224 1232 def css_classes(user=User.current)
1225 1233 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1226 1234 s << ' closed' if closed?
1227 1235 s << ' overdue' if overdue?
1228 1236 s << ' child' if child?
1229 1237 s << ' parent' unless leaf?
1230 1238 s << ' private' if is_private?
1231 1239 if user.logged?
1232 1240 s << ' created-by-me' if author_id == user.id
1233 1241 s << ' assigned-to-me' if assigned_to_id == user.id
1234 1242 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1235 1243 end
1236 1244 s
1237 1245 end
1238 1246
1239 1247 # Unassigns issues from +version+ if it's no longer shared with issue's project
1240 1248 def self.update_versions_from_sharing_change(version)
1241 1249 # Update issues assigned to the version
1242 1250 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1243 1251 end
1244 1252
1245 1253 # Unassigns issues from versions that are no longer shared
1246 1254 # after +project+ was moved
1247 1255 def self.update_versions_from_hierarchy_change(project)
1248 1256 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1249 1257 # Update issues of the moved projects and issues assigned to a version of a moved project
1250 1258 Issue.update_versions(
1251 1259 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1252 1260 moved_project_ids, moved_project_ids]
1253 1261 )
1254 1262 end
1255 1263
1256 1264 def parent_issue_id=(arg)
1257 1265 s = arg.to_s.strip.presence
1258 1266 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1259 1267 @invalid_parent_issue_id = nil
1260 1268 elsif s.blank?
1261 1269 @parent_issue = nil
1262 1270 @invalid_parent_issue_id = nil
1263 1271 else
1264 1272 @parent_issue = nil
1265 1273 @invalid_parent_issue_id = arg
1266 1274 end
1267 1275 end
1268 1276
1269 1277 def parent_issue_id
1270 1278 if @invalid_parent_issue_id
1271 1279 @invalid_parent_issue_id
1272 1280 elsif instance_variable_defined? :@parent_issue
1273 1281 @parent_issue.nil? ? nil : @parent_issue.id
1274 1282 else
1275 1283 parent_id
1276 1284 end
1277 1285 end
1278 1286
1279 1287 def set_parent_id
1280 1288 self.parent_id = parent_issue_id
1281 1289 end
1282 1290
1283 1291 # Returns true if issue's project is a valid
1284 1292 # parent issue project
1285 1293 def valid_parent_project?(issue=parent)
1286 1294 return true if issue.nil? || issue.project_id == project_id
1287 1295
1288 1296 case Setting.cross_project_subtasks
1289 1297 when 'system'
1290 1298 true
1291 1299 when 'tree'
1292 1300 issue.project.root == project.root
1293 1301 when 'hierarchy'
1294 1302 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1295 1303 when 'descendants'
1296 1304 issue.project.is_or_is_ancestor_of?(project)
1297 1305 else
1298 1306 false
1299 1307 end
1300 1308 end
1301 1309
1302 1310 # Returns an issue scope based on project and scope
1303 1311 def self.cross_project_scope(project, scope=nil)
1304 1312 if project.nil?
1305 1313 return Issue
1306 1314 end
1307 1315 case scope
1308 1316 when 'all', 'system'
1309 1317 Issue
1310 1318 when 'tree'
1311 1319 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1312 1320 :lft => project.root.lft, :rgt => project.root.rgt)
1313 1321 when 'hierarchy'
1314 1322 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt) OR (#{Project.table_name}.lft < :lft AND #{Project.table_name}.rgt > :rgt)",
1315 1323 :lft => project.lft, :rgt => project.rgt)
1316 1324 when 'descendants'
1317 1325 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1318 1326 :lft => project.lft, :rgt => project.rgt)
1319 1327 else
1320 1328 Issue.where(:project_id => project.id)
1321 1329 end
1322 1330 end
1323 1331
1324 1332 def self.by_tracker(project)
1325 1333 count_and_group_by(:project => project, :association => :tracker)
1326 1334 end
1327 1335
1328 1336 def self.by_version(project)
1329 1337 count_and_group_by(:project => project, :association => :fixed_version)
1330 1338 end
1331 1339
1332 1340 def self.by_priority(project)
1333 1341 count_and_group_by(:project => project, :association => :priority)
1334 1342 end
1335 1343
1336 1344 def self.by_category(project)
1337 1345 count_and_group_by(:project => project, :association => :category)
1338 1346 end
1339 1347
1340 1348 def self.by_assigned_to(project)
1341 1349 count_and_group_by(:project => project, :association => :assigned_to)
1342 1350 end
1343 1351
1344 1352 def self.by_author(project)
1345 1353 count_and_group_by(:project => project, :association => :author)
1346 1354 end
1347 1355
1348 1356 def self.by_subproject(project)
1349 1357 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1350 1358 r.reject {|r| r["project_id"] == project.id.to_s}
1351 1359 end
1352 1360
1353 1361 # Query generator for selecting groups of issue counts for a project
1354 1362 # based on specific criteria
1355 1363 #
1356 1364 # Options
1357 1365 # * project - Project to search in.
1358 1366 # * with_subprojects - Includes subprojects issues if set to true.
1359 1367 # * association - Symbol. Association for grouping.
1360 1368 def self.count_and_group_by(options)
1361 1369 assoc = reflect_on_association(options[:association])
1362 1370 select_field = assoc.foreign_key
1363 1371
1364 1372 Issue.
1365 1373 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1366 1374 joins(:status, assoc.name).
1367 1375 group(:status_id, :is_closed, select_field).
1368 1376 count.
1369 1377 map do |columns, total|
1370 1378 status_id, is_closed, field_value = columns
1371 1379 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1372 1380 {
1373 1381 "status_id" => status_id.to_s,
1374 1382 "closed" => is_closed,
1375 1383 select_field => field_value.to_s,
1376 1384 "total" => total.to_s
1377 1385 }
1378 1386 end
1379 1387 end
1380 1388
1381 1389 # Returns a scope of projects that user can assign the issue to
1382 1390 def allowed_target_projects(user=User.current)
1383 1391 current_project = new_record? ? nil : project
1384 1392 self.class.allowed_target_projects(user, current_project)
1385 1393 end
1386 1394
1387 1395 # Returns a scope of projects that user can assign issues to
1388 1396 # If current_project is given, it will be included in the scope
1389 1397 def self.allowed_target_projects(user=User.current, current_project=nil)
1390 1398 condition = Project.allowed_to_condition(user, :add_issues)
1391 1399 if current_project
1392 1400 condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id]
1393 1401 end
1394 1402 Project.where(condition).having_trackers
1395 1403 end
1396 1404
1397 1405 # Returns a scope of trackers that user can assign the issue to
1398 1406 def allowed_target_trackers(user=User.current)
1399 1407 self.class.allowed_target_trackers(project, user, tracker_id_was)
1400 1408 end
1401 1409
1402 1410 # Returns a scope of trackers that user can assign project issues to
1403 1411 def self.allowed_target_trackers(project, user=User.current, current_tracker=nil)
1404 1412 if project
1405 1413 scope = project.trackers.sorted
1406 1414 unless user.admin?
1407 1415 roles = user.roles_for_project(project).select {|r| r.has_permission?(:add_issues)}
1408 1416 unless roles.any? {|r| r.permissions_all_trackers?(:add_issues)}
1409 1417 tracker_ids = roles.map {|r| r.permissions_tracker_ids(:add_issues)}.flatten.uniq
1410 1418 if current_tracker
1411 1419 tracker_ids << current_tracker
1412 1420 end
1413 1421 scope = scope.where(:id => tracker_ids)
1414 1422 end
1415 1423 end
1416 1424 scope
1417 1425 else
1418 1426 Tracker.none
1419 1427 end
1420 1428 end
1421 1429
1422 1430 private
1423 1431
1424 1432 def user_tracker_permission?(user, permission)
1425 1433 if user.admin?
1426 1434 true
1427 1435 else
1428 1436 roles = user.roles_for_project(project).select {|r| r.has_permission?(permission)}
1429 1437 roles.any? {|r| r.permissions_all_trackers?(permission) || r.permissions_tracker_ids?(permission, tracker_id)}
1430 1438 end
1431 1439 end
1432 1440
1433 1441 def after_project_change
1434 1442 # Update project_id on related time entries
1435 1443 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1436 1444
1437 1445 # Delete issue relations
1438 1446 unless Setting.cross_project_issue_relations?
1439 1447 relations_from.clear
1440 1448 relations_to.clear
1441 1449 end
1442 1450
1443 1451 # Move subtasks that were in the same project
1444 1452 children.each do |child|
1445 1453 next unless child.project_id == project_id_was
1446 1454 # Change project and keep project
1447 1455 child.send :project=, project, true
1448 1456 unless child.save
1449 1457 raise ActiveRecord::Rollback
1450 1458 end
1451 1459 end
1452 1460 end
1453 1461
1454 1462 # Callback for after the creation of an issue by copy
1455 1463 # * adds a "copied to" relation with the copied issue
1456 1464 # * copies subtasks from the copied issue
1457 1465 def after_create_from_copy
1458 1466 return unless copy? && !@after_create_from_copy_handled
1459 1467
1460 1468 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1461 1469 if @current_journal
1462 1470 @copied_from.init_journal(@current_journal.user)
1463 1471 end
1464 1472 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1465 1473 unless relation.save
1466 1474 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1467 1475 end
1468 1476 end
1469 1477
1470 1478 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1471 1479 copy_options = (@copy_options || {}).merge(:subtasks => false)
1472 1480 copied_issue_ids = {@copied_from.id => self.id}
1473 1481 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1474 1482 # Do not copy self when copying an issue as a descendant of the copied issue
1475 1483 next if child == self
1476 1484 # Do not copy subtasks of issues that were not copied
1477 1485 next unless copied_issue_ids[child.parent_id]
1478 1486 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1479 1487 unless child.visible?
1480 1488 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1481 1489 next
1482 1490 end
1483 1491 copy = Issue.new.copy_from(child, copy_options)
1484 1492 if @current_journal
1485 1493 copy.init_journal(@current_journal.user)
1486 1494 end
1487 1495 copy.author = author
1488 1496 copy.project = project
1489 1497 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1490 1498 unless copy.save
1491 1499 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1492 1500 next
1493 1501 end
1494 1502 copied_issue_ids[child.id] = copy.id
1495 1503 end
1496 1504 end
1497 1505 @after_create_from_copy_handled = true
1498 1506 end
1499 1507
1500 1508 def update_nested_set_attributes
1501 1509 if parent_id_changed?
1502 1510 update_nested_set_attributes_on_parent_change
1503 1511 end
1504 1512 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1505 1513 end
1506 1514
1507 1515 # Updates the nested set for when an existing issue is moved
1508 1516 def update_nested_set_attributes_on_parent_change
1509 1517 former_parent_id = parent_id_was
1510 1518 # delete invalid relations of all descendants
1511 1519 self_and_descendants.each do |issue|
1512 1520 issue.relations.each do |relation|
1513 1521 relation.destroy unless relation.valid?
1514 1522 end
1515 1523 end
1516 1524 # update former parent
1517 1525 recalculate_attributes_for(former_parent_id) if former_parent_id
1518 1526 end
1519 1527
1520 1528 def update_parent_attributes
1521 1529 if parent_id
1522 1530 recalculate_attributes_for(parent_id)
1523 1531 association(:parent).reset
1524 1532 end
1525 1533 end
1526 1534
1527 1535 def recalculate_attributes_for(issue_id)
1528 1536 if issue_id && p = Issue.find_by_id(issue_id)
1529 1537 if p.priority_derived?
1530 1538 # priority = highest priority of open children
1531 1539 if priority_position = p.children.open.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1532 1540 p.priority = IssuePriority.find_by_position(priority_position)
1533 1541 else
1534 1542 p.priority = IssuePriority.default
1535 1543 end
1536 1544 end
1537 1545
1538 1546 if p.dates_derived?
1539 1547 # start/due dates = lowest/highest dates of children
1540 1548 p.start_date = p.children.minimum(:start_date)
1541 1549 p.due_date = p.children.maximum(:due_date)
1542 1550 if p.start_date && p.due_date && p.due_date < p.start_date
1543 1551 p.start_date, p.due_date = p.due_date, p.start_date
1544 1552 end
1545 1553 end
1546 1554
1547 1555 if p.done_ratio_derived?
1548 1556 # done ratio = weighted average ratio of leaves
1549 1557 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1550 1558 child_count = p.children.count
1551 1559 if child_count > 0
1552 1560 average = p.children.where("estimated_hours > 0").average(:estimated_hours).to_f
1553 1561 if average == 0
1554 1562 average = 1
1555 1563 end
1556 1564 done = p.children.joins(:status).
1557 1565 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1558 1566 "* (CASE WHEN is_closed = #{self.class.connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1559 1567 progress = done / (average * child_count)
1560 1568 p.done_ratio = progress.round
1561 1569 end
1562 1570 end
1563 1571 end
1564 1572
1565 1573 # ancestors will be recursively updated
1566 1574 p.save(:validate => false)
1567 1575 end
1568 1576 end
1569 1577
1570 1578 # Update issues so their versions are not pointing to a
1571 1579 # fixed_version that is not shared with the issue's project
1572 1580 def self.update_versions(conditions=nil)
1573 1581 # Only need to update issues with a fixed_version from
1574 1582 # a different project and that is not systemwide shared
1575 1583 Issue.joins(:project, :fixed_version).
1576 1584 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1577 1585 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1578 1586 " AND #{Version.table_name}.sharing <> 'system'").
1579 1587 where(conditions).each do |issue|
1580 1588 next if issue.project.nil? || issue.fixed_version.nil?
1581 1589 unless issue.project.shared_versions.include?(issue.fixed_version)
1582 1590 issue.init_journal(User.current)
1583 1591 issue.fixed_version = nil
1584 1592 issue.save
1585 1593 end
1586 1594 end
1587 1595 end
1588 1596
1597 def delete_selected_attachments
1598 if deleted_attachment_ids.present?
1599 objects = attachments.where(:id => deleted_attachment_ids.map(&:to_i))
1600 attachments.delete(objects)
1601 end
1602 end
1603
1589 1604 # Callback on file attachment
1590 1605 def attachment_added(attachment)
1591 1606 if current_journal && !attachment.new_record?
1592 1607 current_journal.journalize_attachment(attachment, :added)
1593 1608 end
1594 1609 end
1595 1610
1596 1611 # Callback on attachment deletion
1597 1612 def attachment_removed(attachment)
1598 1613 if current_journal && !attachment.new_record?
1599 1614 current_journal.journalize_attachment(attachment, :removed)
1600 1615 current_journal.save
1601 1616 end
1602 1617 end
1603 1618
1604 1619 # Called after a relation is added
1605 1620 def relation_added(relation)
1606 1621 if current_journal
1607 1622 current_journal.journalize_relation(relation, :added)
1608 1623 current_journal.save
1609 1624 end
1610 1625 end
1611 1626
1612 1627 # Called after a relation is removed
1613 1628 def relation_removed(relation)
1614 1629 if current_journal
1615 1630 current_journal.journalize_relation(relation, :removed)
1616 1631 current_journal.save
1617 1632 end
1618 1633 end
1619 1634
1620 1635 # Default assignment based on category
1621 1636 def default_assign
1622 1637 if assigned_to.nil? && category && category.assigned_to
1623 1638 self.assigned_to = category.assigned_to
1624 1639 end
1625 1640 end
1626 1641
1627 1642 # Updates start/due dates of following issues
1628 1643 def reschedule_following_issues
1629 1644 if start_date_changed? || due_date_changed?
1630 1645 relations_from.each do |relation|
1631 1646 relation.set_issue_to_dates
1632 1647 end
1633 1648 end
1634 1649 end
1635 1650
1636 1651 # Closes duplicates if the issue is being closed
1637 1652 def close_duplicates
1638 1653 if closing?
1639 1654 duplicates.each do |duplicate|
1640 1655 # Reload is needed in case the duplicate was updated by a previous duplicate
1641 1656 duplicate.reload
1642 1657 # Don't re-close it if it's already closed
1643 1658 next if duplicate.closed?
1644 1659 # Same user and notes
1645 1660 if @current_journal
1646 1661 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1647 1662 duplicate.private_notes = @current_journal.private_notes
1648 1663 end
1649 1664 duplicate.update_attribute :status, self.status
1650 1665 end
1651 1666 end
1652 1667 end
1653 1668
1654 1669 # Make sure updated_on is updated when adding a note and set updated_on now
1655 1670 # so we can set closed_on with the same value on closing
1656 1671 def force_updated_on_change
1657 1672 if @current_journal || changed?
1658 1673 self.updated_on = current_time_from_proper_timezone
1659 1674 if new_record?
1660 1675 self.created_on = updated_on
1661 1676 end
1662 1677 end
1663 1678 end
1664 1679
1665 1680 # Callback for setting closed_on when the issue is closed.
1666 1681 # The closed_on attribute stores the time of the last closing
1667 1682 # and is preserved when the issue is reopened.
1668 1683 def update_closed_on
1669 1684 if closing?
1670 1685 self.closed_on = updated_on
1671 1686 end
1672 1687 end
1673 1688
1674 1689 # Saves the changes in a Journal
1675 1690 # Called after_save
1676 1691 def create_journal
1677 1692 if current_journal
1678 1693 current_journal.save
1679 1694 end
1680 1695 end
1681 1696
1682 1697 def send_notification
1683 1698 if notify? && Setting.notified_events.include?('issue_added')
1684 1699 Mailer.deliver_issue_add(self)
1685 1700 end
1686 1701 end
1687 1702
1688 1703 # Stores the previous assignee so we can still have access
1689 1704 # to it during after_save callbacks (assigned_to_id_was is reset)
1690 1705 def set_assigned_to_was
1691 1706 @previous_assigned_to_id = assigned_to_id_was
1692 1707 end
1693 1708
1694 1709 # Clears the previous assignee at the end of after_save callbacks
1695 1710 def clear_assigned_to_was
1696 1711 @assigned_to_was = nil
1697 1712 @previous_assigned_to_id = nil
1698 1713 end
1699 1714
1700 1715 def clear_disabled_fields
1701 1716 if tracker
1702 1717 tracker.disabled_core_fields.each do |attribute|
1703 1718 send "#{attribute}=", nil
1704 1719 end
1705 1720 self.done_ratio ||= 0
1706 1721 end
1707 1722 end
1708 1723 end
@@ -1,60 +1,80
1 1 <%= labelled_form_for @issue, :html => {:id => 'issue-form', :multipart => true} do |f| %>
2 2 <%= error_messages_for 'issue', 'time_entry' %>
3 3 <%= render :partial => 'conflict' if @conflict %>
4 4 <div class="box">
5 5 <% if @issue.attributes_editable? %>
6 6 <fieldset class="tabular"><legend><%= l(:label_change_properties) %></legend>
7 7 <div id="all_attributes">
8 8 <%= render :partial => 'form', :locals => {:f => f} %>
9 9 </div>
10 10 </fieldset>
11 11 <% end %>
12 12 <% if User.current.allowed_to?(:log_time, @project) %>
13 13 <fieldset class="tabular"><legend><%= l(:button_log_time) %></legend>
14 14 <%= labelled_fields_for :time_entry, @time_entry do |time_entry| %>
15 15 <div class="splitcontent">
16 16 <div class="splitcontentleft">
17 17 <p><%= time_entry.text_field :hours, :size => 6, :label => :label_spent_time %> <%= l(:field_hours) %></p>
18 18 </div>
19 19 <div class="splitcontentright">
20 20 <p><%= time_entry.select :activity_id, activity_collection_for_select_options %></p>
21 21 </div>
22 22 </div>
23 23 <p><%= time_entry.text_field :comments, :size => 60 %></p>
24 24 <% @time_entry.custom_field_values.each do |value| %>
25 25 <p><%= custom_field_tag_with_label :time_entry, value %></p>
26 26 <% end %>
27 27 <% end %>
28 28 </fieldset>
29 29 <% end %>
30 30 <% if @issue.notes_addable? %>
31 31 <fieldset><legend><%= l(:field_notes) %></legend>
32 32 <%= f.text_area :notes, :cols => 60, :rows => 10, :class => 'wiki-edit', :no_label => true %>
33 33 <%= wikitoolbar_for 'issue_notes' %>
34 34
35 35 <% if @issue.safe_attribute? 'private_notes' %>
36 36 <%= f.check_box :private_notes, :no_label => true %> <label for="issue_private_notes"><%= l(:field_private_notes) %></label>
37 37 <% end %>
38 38
39 39 <%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %>
40 40 </fieldset>
41
41
42 42 <fieldset><legend><%= l(:label_attachment_plural) %></legend>
43 <p><%= render :partial => 'attachments/form', :locals => {:container => @issue} %></p>
43 <% if @issue.attachments.any? && @issue.safe_attribute?('deleted_attachment_ids') %>
44 <div class="contextual"><%= link_to l(:label_edit_attachments), '#', :onclick => "$('#existing-attachments').toggle(); return false;" %></div>
45 <div id="existing-attachments" style="<%= @issue.deleted_attachment_ids.blank? ? 'display:none;' : '' %>">
46 <% @issue.attachments.each do |attachment| %>
47 <span class="existing-attachment">
48 <%= text_field_tag '', attachment.filename, :class => "filename", :disabled => true %>
49 <label>
50 <%= check_box_tag 'issue[deleted_attachment_ids][]',
51 attachment.id,
52 @issue.deleted_attachment_ids.include?(attachment.id),
53 :id => nil, :class => "deleted_attachment" %> <%= l(:button_delete) %>
54 </label>
55 </span>
56 <% end %>
57 <hr />
58 </div>
59 <% end %>
60
61 <div id="new-attachments" style="display:inline-block;">
62 <%= render :partial => 'attachments/form', :locals => {:container => @issue} %>
63 </div>
44 64 </fieldset>
45 65 <% end %>
46 66 </div>
47 67
48 68 <%= f.hidden_field :lock_version %>
49 69 <%= hidden_field_tag 'last_journal_id', params[:last_journal_id] || @issue.last_journal_id %>
50 70 <%= submit_tag l(:button_submit) %>
51 71 <%= preview_link preview_edit_issue_path(:project_id => @project, :id => @issue), 'issue-form' %>
52 72 | <%= link_to l(:button_cancel), {}, :onclick => "$('#update').hide(); return false;" %>
53 73
54 74 <%= hidden_field_tag 'prev_issue_id', @prev_issue_id if @prev_issue_id %>
55 75 <%= hidden_field_tag 'next_issue_id', @next_issue_id if @next_issue_id %>
56 76 <%= hidden_field_tag 'issue_position', @issue_position if @issue_position %>
57 77 <%= hidden_field_tag 'issue_count', @issue_count if @issue_count %>
58 78 <% end %>
59 79
60 80 <div id="preview" class="wiki"></div>
@@ -1,191 +1,196
1 1 /* Redmine - project management software
2 2 Copyright (C) 2006-2016 Jean-Philippe Lang */
3 3
4 4 function addFile(inputEl, file, eagerUpload) {
5 5
6 6 if ($('#attachments_fields').children().length < 10) {
7 7
8 8 var attachmentId = addFile.nextAttachmentId++;
9 9
10 10 var fileSpan = $('<span>', { id: 'attachments_' + attachmentId });
11 11
12 12 fileSpan.append(
13 13 $('<input>', { type: 'text', 'class': 'filename readonly', name: 'attachments[' + attachmentId + '][filename]', readonly: 'readonly'} ).val(file.name),
14 14 $('<input>', { type: 'text', 'class': 'description', name: 'attachments[' + attachmentId + '][description]', maxlength: 255, placeholder: $(inputEl).data('description-placeholder') } ).toggle(!eagerUpload),
15 15 $('<a>&nbsp</a>').attr({ href: "#", 'class': 'remove-upload' }).click(removeFile).toggle(!eagerUpload)
16 16 ).appendTo('#attachments_fields');
17 17
18 18 if(eagerUpload) {
19 19 ajaxUpload(file, attachmentId, fileSpan, inputEl);
20 20 }
21 21
22 22 return attachmentId;
23 23 }
24 24 return null;
25 25 }
26 26
27 27 addFile.nextAttachmentId = 1;
28 28
29 29 function ajaxUpload(file, attachmentId, fileSpan, inputEl) {
30 30
31 31 function onLoadstart(e) {
32 32 fileSpan.removeClass('ajax-waiting');
33 33 fileSpan.addClass('ajax-loading');
34 34 $('input:submit', $(this).parents('form')).attr('disabled', 'disabled');
35 35 }
36 36
37 37 function onProgress(e) {
38 38 if(e.lengthComputable) {
39 39 this.progressbar( 'value', e.loaded * 100 / e.total );
40 40 }
41 41 }
42 42
43 43 function actualUpload(file, attachmentId, fileSpan, inputEl) {
44 44
45 45 ajaxUpload.uploading++;
46 46
47 47 uploadBlob(file, $(inputEl).data('upload-path'), attachmentId, {
48 48 loadstartEventHandler: onLoadstart.bind(progressSpan),
49 49 progressEventHandler: onProgress.bind(progressSpan)
50 50 })
51 51 .done(function(result) {
52 52 progressSpan.progressbar( 'value', 100 ).remove();
53 53 fileSpan.find('input.description, a').css('display', 'inline-block');
54 54 })
55 55 .fail(function(result) {
56 56 progressSpan.text(result.statusText);
57 57 }).always(function() {
58 58 ajaxUpload.uploading--;
59 59 fileSpan.removeClass('ajax-loading');
60 60 var form = fileSpan.parents('form');
61 61 if (form.queue('upload').length == 0 && ajaxUpload.uploading == 0) {
62 62 $('input:submit', form).removeAttr('disabled');
63 63 }
64 64 form.dequeue('upload');
65 65 });
66 66 }
67 67
68 68 var progressSpan = $('<div>').insertAfter(fileSpan.find('input.filename'));
69 69 progressSpan.progressbar();
70 70 fileSpan.addClass('ajax-waiting');
71 71
72 72 var maxSyncUpload = $(inputEl).data('max-concurrent-uploads');
73 73
74 74 if(maxSyncUpload == null || maxSyncUpload <= 0 || ajaxUpload.uploading < maxSyncUpload)
75 75 actualUpload(file, attachmentId, fileSpan, inputEl);
76 76 else
77 77 $(inputEl).parents('form').queue('upload', actualUpload.bind(this, file, attachmentId, fileSpan, inputEl));
78 78 }
79 79
80 80 ajaxUpload.uploading = 0;
81 81
82 82 function removeFile() {
83 83 $(this).parent('span').remove();
84 84 return false;
85 85 }
86 86
87 87 function uploadBlob(blob, uploadUrl, attachmentId, options) {
88 88
89 89 var actualOptions = $.extend({
90 90 loadstartEventHandler: $.noop,
91 91 progressEventHandler: $.noop
92 92 }, options);
93 93
94 94 uploadUrl = uploadUrl + '?attachment_id=' + attachmentId;
95 95 if (blob instanceof window.File) {
96 96 uploadUrl += '&filename=' + encodeURIComponent(blob.name);
97 97 uploadUrl += '&content_type=' + encodeURIComponent(blob.type);
98 98 }
99 99
100 100 return $.ajax(uploadUrl, {
101 101 type: 'POST',
102 102 contentType: 'application/octet-stream',
103 103 beforeSend: function(jqXhr, settings) {
104 104 jqXhr.setRequestHeader('Accept', 'application/js');
105 105 // attach proper File object
106 106 settings.data = blob;
107 107 },
108 108 xhr: function() {
109 109 var xhr = $.ajaxSettings.xhr();
110 110 xhr.upload.onloadstart = actualOptions.loadstartEventHandler;
111 111 xhr.upload.onprogress = actualOptions.progressEventHandler;
112 112 return xhr;
113 113 },
114 114 data: blob,
115 115 cache: false,
116 116 processData: false
117 117 });
118 118 }
119 119
120 120 function addInputFiles(inputEl) {
121 121 var clearedFileInput = $(inputEl).clone().val('');
122 122
123 123 if ($.ajaxSettings.xhr().upload && inputEl.files) {
124 124 // upload files using ajax
125 125 uploadAndAttachFiles(inputEl.files, inputEl);
126 126 $(inputEl).remove();
127 127 } else {
128 128 // browser not supporting the file API, upload on form submission
129 129 var attachmentId;
130 130 var aFilename = inputEl.value.split(/\/|\\/);
131 131 attachmentId = addFile(inputEl, { name: aFilename[ aFilename.length - 1 ] }, false);
132 132 if (attachmentId) {
133 133 $(inputEl).attr({ name: 'attachments[' + attachmentId + '][file]', style: 'display:none;' }).appendTo('#attachments_' + attachmentId);
134 134 }
135 135 }
136 136
137 137 clearedFileInput.insertAfter('#attachments_fields');
138 138 }
139 139
140 140 function uploadAndAttachFiles(files, inputEl) {
141 141
142 142 var maxFileSize = $(inputEl).data('max-file-size');
143 143 var maxFileSizeExceeded = $(inputEl).data('max-file-size-message');
144 144
145 145 var sizeExceeded = false;
146 146 $.each(files, function() {
147 147 if (this.size && maxFileSize != null && this.size > parseInt(maxFileSize)) {sizeExceeded=true;}
148 148 });
149 149 if (sizeExceeded) {
150 150 window.alert(maxFileSizeExceeded);
151 151 } else {
152 152 $.each(files, function() {addFile(inputEl, this, true);});
153 153 }
154 154 }
155 155
156 156 function handleFileDropEvent(e) {
157 157
158 158 $(this).removeClass('fileover');
159 159 blockEventPropagation(e);
160 160
161 161 if ($.inArray('Files', e.dataTransfer.types) > -1) {
162 162 uploadAndAttachFiles(e.dataTransfer.files, $('input:file.file_selector'));
163 163 }
164 164 }
165 165
166 166 function dragOverHandler(e) {
167 167 $(this).addClass('fileover');
168 168 blockEventPropagation(e);
169 169 }
170 170
171 171 function dragOutHandler(e) {
172 172 $(this).removeClass('fileover');
173 173 blockEventPropagation(e);
174 174 }
175 175
176 176 function setupFileDrop() {
177 177 if (window.File && window.FileList && window.ProgressEvent && window.FormData) {
178 178
179 179 $.event.fixHooks.drop = { props: [ 'dataTransfer' ] };
180 180
181 181 $('form div.box').has('input:file').each(function() {
182 182 $(this).on({
183 183 dragover: dragOverHandler,
184 184 dragleave: dragOutHandler,
185 185 drop: handleFileDropEvent
186 186 });
187 187 });
188 188 }
189 189 }
190 190
191 191 $(document).ready(setupFileDrop);
192 $(document).ready(function(){
193 $("input.deleted_attachment").change(function(){
194 $(this).parents('.existing-attachment').toggleClass('deleted', $(this).is(":checked"));
195 }).change();
196 });
@@ -1,1389 +1,1389
1 1 html {overflow-y:scroll;}
2 2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#333; margin: 0; padding: 0; min-width: 900px; }
3 3
4 4 h1, h2, h3, h4 {font-family: "Trebuchet MS", Verdana, sans-serif;padding: 2px 10px 1px 0px;margin: 0 0 10px 0;}
5 5 #content h1, h2, h3, h4 {color: #555;}
6 6 h2, .wiki h1 {font-size: 20px;}
7 7 h3, .wiki h2 {font-size: 16px;}
8 8 h4, .wiki h3 {font-size: 13px;}
9 9 h4 {border-bottom: 1px dotted #bbb;}
10 10 pre, code {font-family: Consolas, Menlo, "Liberation Mono", Courier, monospace;}
11 11
12 12 /***** Layout *****/
13 13 #wrapper {background: white;overflow: hidden;}
14 14
15 15 #top-menu {background: #3E5B76; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
16 16 #top-menu ul {margin: 0; padding: 0;}
17 17 #top-menu li {
18 18 float:left;
19 19 list-style-type:none;
20 20 margin: 0px 0px 0px 0px;
21 21 padding: 0px 0px 0px 0px;
22 22 white-space:nowrap;
23 23 }
24 24 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
25 25 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
26 26
27 27 #account {float:right;}
28 28
29 29 #header {min-height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 20px 6px; position:relative;}
30 30 #header a {color:#f8f8f8;}
31 31 #header h1 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
32 32 #header h1 .breadcrumbs { display:block; font-size: .6em; font-weight: normal; }
33 33 #quick-search {float:right;}
34 34
35 35 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px; width: 100%;}
36 36 #main-menu ul {margin: 0; padding: 0; width: 100%; white-space: nowrap;}
37 37 #main-menu li {
38 38 float:none;
39 39 list-style-type:none;
40 40 margin: 0px 2px 0px 0px;
41 41 padding: 0px 0px 0px 0px;
42 42 white-space:nowrap;
43 43 display:inline-block;
44 44 }
45 45 #main-menu li a {
46 46 display: block;
47 47 color: #fff;
48 48 text-decoration: none;
49 49 font-weight: bold;
50 50 margin: 0;
51 51 padding: 4px 10px 4px 10px;
52 52 }
53 53 #main-menu li a:hover {background:#759FCF; color:#fff;}
54 54 #main-menu li:hover ul.menu-children, #main-menu li ul.menu-children.visible {display: block;}
55 55 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
56 56 #main-menu li a.new-object { background-color:#759FCF; }
57 57
58 58 #main-menu .menu-children {
59 59 display: none;
60 60 position:absolute;
61 61 width: inherit;
62 62 z-index:1;
63 63 background-color:#fff;
64 64 border-right: 1px solid #759FCF;
65 65 border-bottom: 1px solid #759FCF;
66 66 border-left: 1px solid #759FCF;
67 67 }
68 68 #main-menu .menu-children li {float:left; clear:both; width:100%;}
69 69 #main-menu .menu-children li a {color: #555; background-color:#fff; font-weight:normal;}
70 70 #main-menu .menu-children li a:hover {color: #fff; background-color: #759FCF;}
71 71
72 72 #main-menu .tabs-buttons {
73 73 right: 6px;
74 74 background-color: transparent;
75 75 border-bottom-color: transparent;
76 76 }
77 77
78 78 #admin-menu ul {margin: 0; padding: 0;}
79 79 #admin-menu li {margin: 0; padding: 0 0 6px 0; list-style-type:none;}
80 80
81 81 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
82 82 #admin-menu a.projects { background-image: url(../images/projects.png); }
83 83 #admin-menu a.users { background-image: url(../images/user.png); }
84 84 #admin-menu a.groups { background-image: url(../images/group.png); }
85 85 #admin-menu a.roles { background-image: url(../images/database_key.png); }
86 86 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
87 87 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
88 88 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
89 89 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
90 90 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
91 91 #admin-menu a.settings { background-image: url(../images/changeset.png); }
92 92 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
93 93 #admin-menu a.info { background-image: url(../images/help.png); }
94 94 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
95 95
96 96 #main {background-color:#EEEEEE;}
97 97
98 98 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
99 99 * html #sidebar{ width: 22%; }
100 100 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
101 101 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
102 102 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
103 103 #sidebar .contextual { margin-right: 1em; }
104 104 #sidebar ul, ul.flat {margin: 0; padding: 0;}
105 105 #sidebar ul li, ul.flat li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
106 106 #sidebar div.wiki ul {margin:inherit; padding-left:40px;}
107 107 #sidebar div.wiki ul li {list-style-type:inherit;}
108 108
109 109 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
110 110 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
111 111 html>body #content { min-height: 600px; }
112 112 * html body #content { height: 600px; } /* IE */
113 113
114 114 #main.nosidebar #sidebar{ display: none; }
115 115 #main.nosidebar #content{ width: auto; border-right: 0; }
116 116
117 117 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
118 118
119 119 #login-form {margin:5em auto 2em auto; padding:20px; width:340px; border:1px solid #FDBF3B; background-color:#FFEBC1; border-radius:4px; box-sizing: border-box;}
120 120 #login-form label {display:block; margin-bottom:5px;}
121 121 #login-form input[type=text], #login-form input[type=password] {border:1px solid #ccc; border-radius:3px; margin-bottom:15px; padding:7px; display:block; width:100%; box-sizing: border-box;}
122 122 #login-form label {font-weight:bold;}
123 123 #login-form label[for=autologin] {font-weight:normal;}
124 124 #login-form a.lost_password {float:right; font-weight:normal;}
125 125 #login-form input#openid_url {background:#fff url(../images/openid-bg.gif) no-repeat 4px 50%; padding-left:24px !important;}
126 126 #login-form input#login-submit {margin-top:15px; padding:7px; display:block; width:100%; box-sizing: border-box;}
127 127
128 128 div.modal { border-radius:5px; background:#fff; z-index:50; padding:4px;}
129 129 div.modal h3.title {display:none;}
130 130 div.modal p.buttons {text-align:right; margin-bottom:0;}
131 131 div.modal .box p {margin: 0.3em 0;}
132 132
133 133 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
134 134
135 135 .mobile-show {display: none;}
136 136
137 137 /***** Links *****/
138 138 a, a:link, a:visited{ color: #169; text-decoration: none; }
139 139 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
140 140 a img{ border: 0; }
141 141
142 142 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
143 143 a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; }
144 144 a.user.locked, a.user.locked:link, a.user.locked:visited {color: #999;}
145 145
146 146 #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;}
147 147 #sidebar a.selected:hover {text-decoration:none;}
148 148 #admin-menu a {line-height:1.7em;}
149 149 #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;}
150 150
151 151 a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;}
152 152 a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;}
153 153
154 154 a#toggle-completed-versions {color:#999;}
155 155
156 156 a.toggle-checkboxes { margin-left: 5px; padding-left: 12px; background: url(../images/toggle_check.png) no-repeat 0% 50%; }
157 157
158 158 /***** Tables *****/
159 159 table.list, .table-list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
160 160 table.list th, .table-list-header { background-color:#EEEEEE; padding: 4px; white-space:nowrap; font-weight:bold; }
161 161 table.list td {text-align:center; vertical-align:top; padding-right:10px;}
162 162 table.list td.id { width: 2%; text-align: center;}
163 163 table.list td.name, table.list td.description, table.list td.subject, table.list td.comments, table.list td.roles {text-align: left;}
164 164 table.list td.tick {width:15%}
165 165 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
166 166 table.list td.checkbox input {padding:0px;}
167 167 table.list td.buttons, div.buttons { white-space:nowrap; text-align: right; }
168 168 table.list td.buttons a, div.buttons a { margin-right: 0.6em; }
169 169 table.list td.buttons img, div.buttons img {vertical-align:middle;}
170 170 table.list td.reorder {width:15%; white-space:nowrap; text-align:center; }
171 171 table.list table.progress td {padding-right:0px;}
172 172 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
173 173 #role-permissions-trackers table.list th {white-space:normal;}
174 174
175 175 .table-list-cell {display: table-cell; vertical-align: top; padding:2px; }
176 176
177 177 tr.project td.name a { white-space:nowrap; }
178 178 tr.project.closed, tr.project.archived { color: #aaa; }
179 179 tr.project.closed a, tr.project.archived a { color: #aaa; }
180 180
181 181 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
182 182 tr.project.idnt-1 td.name {padding-left: 0.5em;}
183 183 tr.project.idnt-2 td.name {padding-left: 2em;}
184 184 tr.project.idnt-3 td.name {padding-left: 3.5em;}
185 185 tr.project.idnt-4 td.name {padding-left: 5em;}
186 186 tr.project.idnt-5 td.name {padding-left: 6.5em;}
187 187 tr.project.idnt-6 td.name {padding-left: 8em;}
188 188 tr.project.idnt-7 td.name {padding-left: 9.5em;}
189 189 tr.project.idnt-8 td.name {padding-left: 11em;}
190 190 tr.project.idnt-9 td.name {padding-left: 12.5em;}
191 191
192 192 tr.issue { text-align: center; white-space: nowrap; }
193 193 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text, tr.issue td.list, tr.issue td.relations, tr.issue td.parent { white-space: normal; }
194 194 tr.issue td.relations { text-align: left; }
195 195 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
196 196 tr.issue td.relations span {white-space: nowrap;}
197 197 table.issues td.description {color:#777; font-size:90%; padding:4px 4px 4px 24px; text-align:left; white-space:normal;}
198 198 table.issues td.description pre {white-space:normal;}
199 199
200 200 tr.issue.idnt td.subject {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%;}
201 201 tr.issue.idnt-1 td.subject {padding-left: 24px; background-position: 8px 50%;}
202 202 tr.issue.idnt-2 td.subject {padding-left: 40px; background-position: 24px 50%;}
203 203 tr.issue.idnt-3 td.subject {padding-left: 56px; background-position: 40px 50%;}
204 204 tr.issue.idnt-4 td.subject {padding-left: 72px; background-position: 56px 50%;}
205 205 tr.issue.idnt-5 td.subject {padding-left: 88px; background-position: 72px 50%;}
206 206 tr.issue.idnt-6 td.subject {padding-left: 104px; background-position: 88px 50%;}
207 207 tr.issue.idnt-7 td.subject {padding-left: 120px; background-position: 104px 50%;}
208 208 tr.issue.idnt-8 td.subject {padding-left: 136px; background-position: 120px 50%;}
209 209 tr.issue.idnt-9 td.subject {padding-left: 152px; background-position: 136px 50%;}
210 210
211 211 table.issue-report {table-layout:fixed;}
212 212
213 213 tr.entry { border: 1px solid #f8f8f8; }
214 214 tr.entry td { white-space: nowrap; }
215 215 tr.entry td.filename {width:30%; text-align:left;}
216 216 tr.entry td.filename_no_report {width:70%; text-align:left;}
217 217 tr.entry td.size { text-align: right; font-size: 90%; }
218 218 tr.entry td.revision, tr.entry td.author { text-align: center; }
219 219 tr.entry td.age { text-align: right; }
220 220 tr.entry.file td.filename a { margin-left: 16px; }
221 221 tr.entry.file td.filename_no_report a { margin-left: 16px; }
222 222
223 223 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
224 224 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
225 225
226 226 tr.changeset { height: 20px }
227 227 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
228 228 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
229 229 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
230 230 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
231 231
232 232 table.files tbody th {text-align:left;}
233 233 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
234 234 table.files tr.file td.digest { font-size: 80%; }
235 235
236 236 table.members td.roles, table.memberships td.roles { width: 45%; }
237 237
238 238 tr.message { height: 2.6em; }
239 239 tr.message td.subject { padding-left: 20px; }
240 240 tr.message td.created_on { white-space: nowrap; }
241 241 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
242 242 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
243 243 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
244 244
245 245 tr.version.closed, tr.version.closed a { color: #999; }
246 246 tr.version td.name { padding-left: 20px; }
247 247 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
248 248 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
249 249
250 250 tr.user td {width:13%;white-space: nowrap;}
251 251 td.username, td.firstname, td.lastname, td.email {text-align:left !important;}
252 252 tr.user td.email { width:18%; }
253 253 tr.user.locked, tr.user.registered { color: #aaa; }
254 254 tr.user.locked a, tr.user.registered a { color: #aaa; }
255 255
256 256 table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;}
257 257
258 258 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
259 259
260 260 tr.time-entry { text-align: center; white-space: nowrap; }
261 261 tr.time-entry td.issue, tr.time-entry td.comments, tr.time-entry td.subject, tr.time-entry td.activity { text-align: left; white-space: normal; }
262 262 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
263 263 td.hours .hours-dec { font-size: 0.9em; }
264 264
265 265 table.plugins td { vertical-align: middle; }
266 266 table.plugins td.configure { text-align: right; padding-right: 1em; }
267 267 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
268 268 table.plugins span.description { display: block; font-size: 0.9em; }
269 269 table.plugins span.url { display: block; font-size: 0.9em; }
270 270
271 271 tr.group td { padding: 0.8em 0 0.5em 0.3em; border-bottom: 1px solid #ccc; text-align:left; }
272 272 tr.group span.name {font-weight:bold;}
273 273 tr.group span.count {font-weight:bold; position:relative; top:-1px; color:#fff; font-size:10px; background:#9DB9D5; padding:0px 6px 1px 6px; border-radius:3px; margin-left:4px;}
274 274 tr.group span.totals {color: #aaa; font-size: 80%;}
275 275 tr.group span.totals .value {font-weight:bold; color:#777;}
276 276 tr.group a.toggle-all { color: #aaa; font-size: 80%; display:none; float:right; margin-right:4px;}
277 277 tr.group:hover a.toggle-all { display:inline;}
278 278 a.toggle-all:hover {text-decoration:none;}
279 279
280 280 table.list tbody tr:hover { background-color:#ffffdd; }
281 281 table.list tbody tr.group:hover { background-color:inherit; }
282 282 table td {padding:2px;}
283 283 table p {margin:0;}
284 284 .odd {background-color:#f6f7f8;}
285 285 .even {background-color: #fff;}
286 286
287 287 tr.builtin td.name {font-style:italic;}
288 288
289 289 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
290 290 a.sort.asc { background-image: url(../images/sort_asc.png); }
291 291 a.sort.desc { background-image: url(../images/sort_desc.png); }
292 292
293 293 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
294 294 table.boards td.last-message {text-align:left;font-size:80%;}
295 295
296 296 div.table-list.boards .table-list-cell.name {width: 30%;}
297 297
298 298 table.messages td.last_message {text-align:left;}
299 299
300 300 #query_form_content {font-size:90%;}
301 301
302 302 .query_sort_criteria_count {
303 303 display: inline-block;
304 304 min-width: 1em;
305 305 }
306 306
307 307 table.query-columns {
308 308 border-collapse: collapse;
309 309 border: 0;
310 310 }
311 311
312 312 table.query-columns td.buttons {
313 313 vertical-align: middle;
314 314 text-align: center;
315 315 }
316 316 table.query-columns td.buttons input[type=button] {width:35px;}
317 317 .query-totals {text-align:right;}
318 318 .query-totals>span {margin-left:0.6em;}
319 319 .query-totals .value {font-weight:bold;}
320 320 body.controller-issues .query-totals {margin-top:-2.3em;}
321 321
322 322 td.center {text-align:center;}
323 323
324 324 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
325 325
326 326 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
327 327 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
328 328 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
329 329 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
330 330 div.spent_time h3 { background: url(../images/time.png) no-repeat 0% 50%; padding-left: 20px; }
331 331
332 332 #watchers select {width: 95%; display: block;}
333 333 #watchers a.delete {opacity: 0.4; margin-left: 5px;}
334 334 #watchers a.delete:hover {opacity: 1;}
335 335 #watchers img.gravatar {margin: 0 4px 2px 0;}
336 336
337 337 span#watchers_inputs {overflow:auto; display:block;}
338 338 span.search_for_watchers {display:block;}
339 339 span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;}
340 340 span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; }
341 341
342 342
343 343 .highlight { background-color: #FCFD8D;}
344 344 .highlight.token-1 { background-color: #faa;}
345 345 .highlight.token-2 { background-color: #afa;}
346 346 .highlight.token-3 { background-color: #aaf;}
347 347
348 348 .box{
349 349 padding:6px;
350 350 margin-bottom: 10px;
351 351 background-color:#f6f6f6;
352 352 color:#505050;
353 353 line-height:1.5em;
354 354 border: 1px solid #e4e4e4;
355 355 word-wrap: break-word;
356 356 border-radius: 3px;
357 357 }
358 358
359 359 div.square {
360 360 border: 1px solid #999;
361 361 float: left;
362 362 margin: .3em .4em 0 .4em;
363 363 overflow: hidden;
364 364 width: .6em; height: .6em;
365 365 }
366 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
366 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin:5px 0px; padding-left: 10px; font-size:0.9em;}
367 367 .contextual input, .contextual select {font-size:0.9em;}
368 368 .message .contextual { margin-top: 0; }
369 369
370 370 .splitcontent {overflow:auto;}
371 371 .splitcontentleft{float:left; width:49%;}
372 372 .splitcontentright{float:right; width:49%;}
373 373 form {display: inline;}
374 374 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
375 375 fieldset {border: 1px solid #e4e4e4; margin:0;}
376 376 legend {color: #333;}
377 377 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
378 378 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
379 379 blockquote blockquote { margin-left: 0;}
380 380 abbr, span.field-description[title] { border-bottom: 1px dotted #aaa; cursor: help; }
381 381 textarea.wiki-edit {width:99%; resize:vertical;}
382 382 li p {margin-top: 0;}
383 383 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px; border: 1px solid #d7d7d7; border-radius:3px;}
384 384 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
385 385 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
386 386 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
387 387 .ltr {direction:ltr !important; unicode-bidi:bidi-override;}
388 388 .rtl {direction:rtl !important; unicode-bidi:bidi-override;}
389 389
390 390 div.issue div.subject div div { padding-left: 16px; }
391 391 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
392 392 div.issue div.subject>div>p { margin-top: 0.5em; }
393 393 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
394 394 div.issue span.private, div.journal span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px;}
395 395 div.issue .next-prev-links {color:#999;}
396 396 div.issue .attributes {margin-top: 2em;}
397 397 div.issue .attribute {padding-left:180px; clear:left; min-height: 1.8em;}
398 398 div.issue .attribute .label {width: 170px; margin-left:-180px; font-weight:bold; float:left;}
399 399 div.issue.overdue .due-date .value { color: #c22; }
400 400
401 401 #issue_tree table.issues, #relations table.issues { border: 0; }
402 402 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
403 403 #relations td.buttons {padding:0;}
404 404
405 405 fieldset.collapsible {border-width: 1px 0 0 0;}
406 406 fieldset.collapsible>legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
407 407 fieldset.collapsible.collapsed>legend { background-image: url(../images/arrow_collapsed.png); }
408 408
409 409 fieldset#date-range p { margin: 2px 0 2px 0; }
410 410 fieldset#filters table { border-collapse: collapse; }
411 411 fieldset#filters table td { padding: 0; vertical-align: middle; }
412 412 fieldset#filters tr.filter { height: 2.1em; }
413 413 fieldset#filters td.field { width:230px; }
414 414 fieldset#filters td.operator { width:130px; }
415 415 fieldset#filters td.operator select {max-width:120px;}
416 416 fieldset#filters td.values { white-space:nowrap; }
417 417 fieldset#filters td.values select {min-width:130px; max-width:200px;}
418 418 fieldset#filters td.values input {height:1em;}
419 419
420 420 #filters-table {width:60%; float:left;}
421 421 .add-filter {width:35%; float:right; text-align: right; vertical-align: top;}
422 422
423 423 #issue_is_private_wrap {float:right; margin-right:1em;}
424 424 .toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:16px; margin-left:0; margin-right:5px; cursor:pointer;}
425 425 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
426 426
427 427 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
428 428 div#issue-changesets div.changeset { padding: 4px;}
429 429 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
430 430 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
431 431
432 432 div.journal {overflow:auto;}
433 433 div.journal.private-notes {border-left:2px solid #d22; padding-left:4px; margin-left:-6px;}
434 434 div.journal ul.details {color:#959595; margin-bottom: 1.5em;}
435 435 div.journal ul.details a {color:#70A7CD;}
436 436 div.journal ul.details a:hover {color:#D14848;}
437 437
438 438 div#activity dl, #search-results { margin-left: 2em; }
439 439 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
440 440 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
441 441 div#activity dt.me .time { border-bottom: 1px solid #999; }
442 442 div#activity dt .time { color: #777; font-size: 80%; }
443 443 div#activity dd .description, #search-results dd .description { font-style: italic; }
444 444 div#activity span.project:after, #search-results span.project:after { content: " -"; }
445 445 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
446 446 div#activity dt.grouped {margin-left:5em;}
447 447 div#activity dd.grouped {margin-left:9em;}
448 448
449 449 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
450 450
451 451 div#search-results-counts {float:right;}
452 452 div#search-results-counts ul { margin-top: 0.5em; }
453 453 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
454 454
455 455 dt.issue { background-image: url(../images/ticket.png); }
456 456 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
457 457 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
458 458 dt.issue-note { background-image: url(../images/ticket_note.png); }
459 459 dt.changeset { background-image: url(../images/changeset.png); }
460 460 dt.news { background-image: url(../images/news.png); }
461 461 dt.message { background-image: url(../images/message.png); }
462 462 dt.reply { background-image: url(../images/comments.png); }
463 463 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
464 464 dt.attachment { background-image: url(../images/attachment.png); }
465 465 dt.document { background-image: url(../images/document.png); }
466 466 dt.project { background-image: url(../images/projects.png); }
467 467 dt.time-entry { background-image: url(../images/time.png); }
468 468
469 469 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
470 470
471 471 div#roadmap .related-issues { margin-bottom: 1em; }
472 472 div#roadmap .related-issues td.checkbox { display: none; }
473 473 div#roadmap .wiki h1:first-child { display: none; }
474 474 div#roadmap .wiki h1 { font-size: 120%; }
475 475 div#roadmap .wiki h2 { font-size: 110%; }
476 476 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
477 477
478 478 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
479 479 div#version-summary fieldset { margin-bottom: 1em; }
480 480 div#version-summary fieldset.time-tracking table { width:100%; }
481 481 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
482 482
483 483 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
484 484 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
485 485 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
486 486 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
487 487 table#time-report .hours-dec { font-size: 0.9em; }
488 488
489 489 div.wiki-page .contextual a {opacity: 0.4}
490 490 div.wiki-page .contextual a:hover {opacity: 1}
491 491
492 492 form .attributes select { width: 60%; }
493 493 form .attributes select + a.icon-only { vertical-align: middle; margin-left: 4px; }
494 494 input#issue_subject, input#document_title { width: 99%; }
495 495 select#issue_done_ratio { width: 95px; }
496 496
497 497 ul.projects {margin:0; padding-left:1em;}
498 498 ul.projects ul {padding-left:1.6em;}
499 499 ul.projects.root {margin:0; padding:0;}
500 500 ul.projects li {list-style-type:none;}
501 501
502 502 #projects-index ul.projects ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;}
503 503 #projects-index ul.projects li.root {margin-bottom: 1em;}
504 504 #projects-index ul.projects li.child {margin-top: 1em;}
505 505 #projects-index ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
506 506 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
507 507
508 508 #notified-projects>ul, #tracker_project_ids>ul, #custom_field_project_ids>ul {max-height:250px; overflow-y:auto;}
509 509
510 510 #related-issues li img {vertical-align:middle;}
511 511
512 512 ul.properties {padding:0; font-size: 0.9em; color: #777;}
513 513 ul.properties li {list-style-type:none;}
514 514 ul.properties li span {font-style:italic;}
515 515
516 516 .total-hours { font-size: 110%; font-weight: bold; }
517 517 .total-hours span.hours-int { font-size: 120%; }
518 518
519 519 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em; position: relative;}
520 520 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
521 521
522 522 #workflow_copy_form select { width: 200px; }
523 523 table.transitions td.enabled {background: #bfb;}
524 524 #workflow_form table select {font-size:90%; max-width:100px;}
525 525 table.fields_permissions td.readonly {background:#ddd;}
526 526 table.fields_permissions td.required {background:#d88;}
527 527
528 528 select.expandable {vertical-align:top;}
529 529
530 530 textarea#custom_field_possible_values {width: 95%; resize:vertical}
531 531 textarea#custom_field_default_value {width: 95%; resize:vertical}
532 532 .sort-handle {display:inline-block; vertical-align:middle;}
533 533
534 534 input#content_comments {width: 99%}
535 535
536 536 span.pagination {margin-left:3px; color:#888; display:block;}
537 537 .pagination ul.pages {
538 538 margin: 0 5px 0 0;
539 539 padding: 0;
540 540 display: inline;
541 541 }
542 542 .pagination ul.pages li {
543 543 display: inline-block;
544 544 padding: 0;
545 545 border: 1px solid #ddd;
546 546 margin-left: -1px;
547 547 line-height: 2em;
548 548 margin-bottom: 1em;
549 549 white-space: nowrap;
550 550 text-align: center;
551 551 }
552 552 .pagination ul.pages li a,
553 553 .pagination ul.pages li span {
554 554 padding: 3px 8px;
555 555 }
556 556 .pagination ul.pages li:first-child {
557 557 border-top-left-radius: 4px;
558 558 border-bottom-left-radius: 4px;
559 559 }
560 560 .pagination ul.pages li:last-child {
561 561 border-top-right-radius: 4px;
562 562 border-bottom-right-radius: 4px;
563 563 }
564 564 .pagination ul.pages li.current {
565 565 color: white;
566 566 background-color: #628DB6;
567 567 border-color: #628DB6;
568 568 }
569 569 .pagination ul.pages li.page:hover {
570 570 background-color: #ddd;
571 571 }
572 572 .pagination ul.pages li.page a:hover,
573 573 .pagination ul.pages li.page a:active {
574 574 color: #169;
575 575 text-decoration: inherit;
576 576 }
577 577 .pagination .per-page span.selected {
578 578 font-weight: bold;
579 579 }
580 580 span.pagination>span {white-space:nowrap;}
581 581
582 582 #search-form fieldset p {margin:0.2em 0;}
583 583
584 584 /***** Tabular forms ******/
585 585 .tabular p{
586 586 margin: 0;
587 587 padding: 3px 0 3px 0;
588 588 padding-left: 180px; /* width of left column containing the label elements */
589 589 min-height: 1.8em;
590 590 clear:left;
591 591 }
592 592
593 593 html>body .tabular p {overflow:hidden;}
594 594
595 595 .tabular input, .tabular select {max-width:95%}
596 596 .tabular textarea {width:95%; resize:vertical;}
597 597
598 598 .tabular label{
599 599 font-weight: bold;
600 600 float: left;
601 601 text-align: right;
602 602 /* width of left column */
603 603 margin-left: -180px;
604 604 /* width of labels. Should be smaller than left column to create some right margin */
605 605 width: 175px;
606 606 }
607 607
608 608 .tabular label.floating{
609 609 font-weight: normal;
610 610 margin-left: 0px;
611 611 text-align: left;
612 612 width: 270px;
613 613 }
614 614
615 615 .tabular label.block{
616 616 font-weight: normal;
617 617 margin-left: 0px !important;
618 618 text-align: left;
619 619 float: none;
620 620 display: block;
621 621 width: auto !important;
622 622 }
623 623
624 624 .tabular label.inline{
625 625 font-weight: normal;
626 626 float:none;
627 627 margin-left: 5px !important;
628 628 width: auto;
629 629 }
630 630
631 631 label.no-css {
632 632 font-weight: inherit;
633 633 float:none;
634 634 text-align:left;
635 635 margin-left:0px;
636 636 width:auto;
637 637 }
638 638 input#time_entry_comments { width: 90%;}
639 639
640 640 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
641 641
642 642 .tabular.settings p{ padding-left: 300px; }
643 643 .tabular.settings label{ margin-left: -300px; width: 295px; }
644 644 .tabular.settings textarea { width: 99%; }
645 645
646 646 .settings.enabled_scm table {width:100%}
647 647 .settings.enabled_scm td.scm_name{ font-weight: bold; }
648 648
649 649 fieldset.settings label { display: block; }
650 650 fieldset#notified_events .parent { padding-left: 20px; }
651 651
652 652 span.required {color: #bb0000;}
653 653 .summary {font-style: italic;}
654 654
655 655 .check_box_group {
656 656 display:block;
657 657 width:95%;
658 658 max-height:300px;
659 659 overflow-y:auto;
660 660 padding:2px 4px 4px 2px;
661 661 background:#fff;
662 662 border:1px solid #9EB1C2;
663 663 border-radius:2px
664 664 }
665 665 .check_box_group label {
666 666 font-weight: normal;
667 667 margin-left: 0px !important;
668 668 text-align: left;
669 669 float: none;
670 670 display: block;
671 671 width: auto;
672 672 }
673 673 .check_box_group.bool_cf {border:0; background:inherit;}
674 674 .check_box_group.bool_cf label {display: inline;}
675 675
676 #attachments_fields input.description {margin-left:4px; width:340px;}
677 #attachments_fields span {display:block; white-space:nowrap;}
678 #attachments_fields input.filename {border:0; height:1.8em; width:250px; color:#555; background-color:inherit; background:url(../images/attachment.png) no-repeat 1px 50%; padding-left:18px;}
676 #attachments_fields input.description, #existing-attachments input.description {margin-left:4px; width:340px;}
677 #attachments_fields>span, #existing-attachments>span {display:block; white-space:nowrap;}
678 #attachments_fields input.filename, #existing-attachments .filename {border:0; width:250px; color:#555; background-color:inherit; background:url(../images/attachment.png) no-repeat 1px 50%; padding-left:18px;}
679 #attachments_fields input.filename {height:1.8em;}
679 680 #attachments_fields .ajax-waiting input.filename {background:url(../images/hourglass.png) no-repeat 0px 50%;}
680 681 #attachments_fields .ajax-loading input.filename {background:url(../images/loading.gif) no-repeat 0px 50%;}
681 682 #attachments_fields div.ui-progressbar { width: 100px; height:14px; margin: 2px 0 -5px 8px; display: inline-block; }
682 683 a.remove-upload {background: url(../images/delete.png) no-repeat 1px 50%; width:1px; display:inline-block; padding-left:16px;}
683 684 a.remove-upload:hover {text-decoration:none !important;}
685 .existing-attachment.deleted .filename {text-decoration:line-through; color:#999 !important;}
684 686
685 687 div.fileover { background-color: lavender; }
686 688
687 689 div.attachments { margin: 12px 0; }
688 690 div.attachments p { margin:4px 0 2px 0; }
689 691 div.attachments img { vertical-align: middle; }
690 692 div.attachments span.author { font-size: 0.9em; color: #888; }
691 693
692 694 div.thumbnails {margin:0.6em;}
693 695 div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;}
694 696 div.thumbnails img {margin: 3px; vertical-align: middle;}
695 697 #history div.thumbnails {margin-left: 2em;}
696 698
697 699 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
698 700 .other-formats span + span:before { content: "| "; }
699 701
700 702 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
701 703
702 704 em.info {font-style:normal;font-size:90%;color:#888;display:block;}
703 705 em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
704 706
705 707 textarea.text_cf {width:95%; resize:vertical;}
706 708 input.string_cf, input.link_cf {width:95%;}
707 709 select.bool_cf {width:auto !important;}
708 710
709 711 #tab-content-modules fieldset p {margin:3px 0 4px 0;}
710 712
711 713 #tab-content-users .splitcontentleft {width: 64%;}
712 714 #tab-content-users .splitcontentright {width: 34%;}
713 715 #tab-content-users fieldset {padding:1em; margin-bottom: 1em;}
714 716 #tab-content-users fieldset legend {font-weight: bold;}
715 717 #tab-content-users fieldset label {display: block;}
716 718 #tab-content-users #principals {max-height: 400px; overflow: auto;}
717 719
718 720 #users_for_watcher {height: 200px; overflow:auto;}
719 721 #users_for_watcher label {display: block;}
720 722
721 723 table.members td.name {padding-left: 20px;}
722 724 table.members td.group, table.members td.groupnonmember, table.members td.groupanonymous {background: url(../images/group.png) no-repeat 0% 1px;}
723 725
724 726 input#principal_search, input#user_search {width:90%}
725 727 .roles-selection label {display:inline-block; width:210px;}
726 728
727 729 input.autocomplete {
728 730 background: #fff url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px !important;
729 731 border:1px solid #9EB1C2; border-radius:2px; height:1.5em;
730 732 }
731 733 input.autocomplete.ajax-loading {
732 734 background-image: url(../images/loading.gif);
733 735 }
734 736
735 737 .role-visibility {padding-left:2em;}
736 738
737 739 .objects-selection {
738 740 height: 300px;
739 741 overflow: auto;
740 742 margin-bottom: 1em;
741 743 }
742 744
743 745 .objects-selection label {
744 746 display: block;
745 747 }
746 748
747 749 .objects-selection>div {
748 750 column-count: auto;
749 751 column-width: 200px;
750 752 -webkit-column-count: auto;
751 753 -webkit-column-width: 200px;
752 754 -webkit-column-gap : 0.5rem;
753 755 -webkit-column-rule: 1px solid #ccc;
754 756 -moz-column-count: auto;
755 757 -moz-column-width: 200px;
756 758 -moz-column-gap : 0.5rem;
757 759 -moz-column-rule: 1px solid #ccc;
758 760 }
759 761
760 762 /***** Flash & error messages ****/
761 763 #errorExplanation, div.flash, .nodata, .warning, .conflict {
762 764 padding: 6px 4px 6px 30px;
763 765 margin-bottom: 12px;
764 766 font-size: 1.1em;
765 767 border: 1px solid;
766 768 border-radius: 3px;
767 769 }
768 770
769 771 div.flash {margin-top: 8px;}
770 772
771 773 div.flash.error, #errorExplanation {
772 774 background: url(../images/exclamation.png) 8px 50% no-repeat;
773 775 background-color: #ffe3e3;
774 776 border-color: #d88;
775 777 color: #880000;
776 778 }
777 779
778 780 div.flash.notice {
779 781 background: url(../images/true.png) 8px 5px no-repeat;
780 782 background-color: #dfffdf;
781 783 border-color: #9fcf9f;
782 784 color: #005f00;
783 785 }
784 786
785 787 div.flash.warning, .conflict {
786 788 background: url(../images/warning.png) 8px 5px no-repeat;
787 789 background-color: #F3EDD1;
788 790 border-color: #eadbbc;
789 791 color: #A6750C;
790 792 text-align: left;
791 793 }
792 794
793 795 .nodata, .warning {
794 796 text-align: center;
795 797 background-color: #F3EDD1;
796 798 border-color: #eadbbc;
797 799 color: #A6750C;
798 800 }
799 801
800 802 #errorExplanation ul { font-size: 0.9em;}
801 803 #errorExplanation h2, #errorExplanation p { display: none; }
802 804
803 805 .conflict-details {font-size:80%;}
804 806
805 807 /***** Ajax indicator ******/
806 808 #ajax-indicator {
807 809 position: absolute; /* fixed not supported by IE */
808 810 background-color:#eee;
809 811 border: 1px solid #bbb;
810 812 top:35%;
811 813 left:40%;
812 814 width:20%;
813 815 font-weight:bold;
814 816 text-align:center;
815 817 padding:0.6em;
816 818 z-index:100;
817 819 opacity: 0.5;
818 820 }
819 821
820 822 html>body #ajax-indicator { position: fixed; }
821 823
822 824 #ajax-indicator span {
823 825 background-position: 0% 40%;
824 826 background-repeat: no-repeat;
825 827 background-image: url(../images/loading.gif);
826 828 padding-left: 26px;
827 829 vertical-align: bottom;
828 830 }
829 831
830 832 /***** Calendar *****/
831 833 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
832 834 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
833 835 table.cal thead th.week-number {width: auto;}
834 836 table.cal tbody tr {height: 100px;}
835 837 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
836 838 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
837 839 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
838 840 table.cal td.odd p.day-num {color: #bbb;}
839 841 table.cal td.today {background:#ffffdd;}
840 842 table.cal td.today p.day-num {font-weight: bold;}
841 843 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
842 844 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
843 845 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
844 846 p.cal.legend span {display:block;}
845 847
846 848 /***** Tooltips ******/
847 849 .tooltip{position:relative;z-index:24;}
848 850 .tooltip:hover{z-index:25;color:#000;}
849 851 .tooltip span.tip{display: none; text-align:left;}
850 852
851 853 div.tooltip:hover span.tip{
852 854 display:block;
853 855 position:absolute;
854 856 top:12px; width:270px;
855 857 border:1px solid #555;
856 858 background-color:#fff;
857 859 padding: 4px;
858 860 font-size: 0.8em;
859 861 color:#505050;
860 862 }
861 863
862 864 img.ui-datepicker-trigger {
863 865 cursor: pointer;
864 866 vertical-align: middle;
865 867 margin-left: 4px;
866 868 }
867 869
868 870 /***** Progress bar *****/
869 871 table.progress {
870 872 border-collapse: collapse;
871 873 border-spacing: 0pt;
872 874 empty-cells: show;
873 875 text-align: center;
874 876 float:left;
875 877 margin: 1px 6px 1px 0px;
876 878 }
877 879
878 880 table.progress {width:80px;}
879 881 table.progress td { height: 1em; }
880 882 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
881 883 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
882 884 table.progress td.todo { background: #eee none repeat scroll 0%; }
883 885 p.percent {font-size: 80%; margin:0;}
884 886 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
885 887
886 888 .version-overview table.progress {width:40em;}
887 889 .version-overview table.progress td { height: 1.2em; }
888 890
889 891 /***** Tabs *****/
890 892 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
891 893 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
892 894 #content .tabs ul li {
893 895 float:left;
894 896 list-style-type:none;
895 897 white-space:nowrap;
896 898 margin-right:4px;
897 899 background:#fff;
898 900 position:relative;
899 901 margin-bottom:-1px;
900 902 }
901 903 #content .tabs ul li a{
902 904 display:block;
903 905 font-size: 0.9em;
904 906 text-decoration:none;
905 907 line-height:1.3em;
906 908 padding:4px 6px 4px 6px;
907 909 border: 1px solid #ccc;
908 910 border-bottom: 1px solid #bbbbbb;
909 911 background-color: #f6f6f6;
910 912 color:#999;
911 913 font-weight:bold;
912 914 border-top-left-radius:3px;
913 915 border-top-right-radius:3px;
914 916 }
915 917
916 918 #content .tabs ul li a:hover {
917 919 background-color: #ffffdd;
918 920 text-decoration:none;
919 921 }
920 922
921 923 #content .tabs ul li a.selected {
922 924 background-color: #fff;
923 925 border: 1px solid #bbbbbb;
924 926 border-bottom: 1px solid #fff;
925 927 color:#444;
926 928 }
927 929
928 930 #content .tabs ul li a.selected:hover {background-color: #fff;}
929 931
930 932 div.tabs-buttons { position:absolute; right: 0; width: 54px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
931 933
932 934 button.tab-left, button.tab-right {
933 935 font-size: 0.9em;
934 936 cursor: pointer;
935 937 height:24px;
936 938 border: 1px solid #ccc;
937 939 border-bottom: 1px solid #bbbbbb;
938 940 position:absolute;
939 941 padding:4px;
940 942 width: 20px;
941 943 bottom: -1px;
942 944 }
943 945 button.tab-left:hover, button.tab-right:hover {
944 946 background-color: #f5f5f5;
945 947 }
946 948 button.tab-left:focus, button.tab-right:focus {
947 949 outline: 0;
948 950 }
949 951
950 952 button.tab-left {
951 953 right: 20px;
952 954 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
953 955 border-top-left-radius:3px;
954 956 }
955 957
956 958 button.tab-right {
957 959 right: 0;
958 960 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
959 961 border-top-right-radius:3px;
960 962 }
961 963
962 964 button.tab-left.disabled, button.tab-right.disabled {
963 965 background-color: #ccc;
964 966 cursor: unset;
965 967 }
966 968
967 969 /***** Diff *****/
968 970 .diff_out { background: #fcc; }
969 971 .diff_out span { background: #faa; }
970 972 .diff_in { background: #cfc; }
971 973 .diff_in span { background: #afa; }
972 974
973 975 .text-diff {
974 976 padding: 1em;
975 977 background-color:#f6f6f6;
976 978 color:#505050;
977 979 border: 1px solid #e4e4e4;
978 980 }
979 981
980 982 /***** Wiki *****/
981 983 div.wiki table {
982 984 border-collapse: collapse;
983 985 margin-bottom: 1em;
984 986 }
985 987
986 988 div.wiki table, div.wiki td, div.wiki th {
987 989 border: 1px solid #bbb;
988 990 padding: 4px;
989 991 }
990 992
991 993 div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;}
992 994
993 995 div.wiki .external {
994 996 background-position: 0% 60%;
995 997 background-repeat: no-repeat;
996 998 padding-left: 12px;
997 999 background-image: url(../images/external.png);
998 1000 }
999 1001
1000 1002 div.wiki a {word-wrap: break-word;}
1001 1003 div.wiki a.new {color: #b73535;}
1002 1004
1003 1005 div.wiki ul, div.wiki ol {margin-bottom:1em;}
1004 1006 div.wiki li>ul, div.wiki li>ol {margin-bottom: 0;}
1005 1007
1006 1008 div.wiki pre {
1007 1009 margin: 1em 1em 1em 1.6em;
1008 1010 padding: 8px;
1009 1011 background-color: #fafafa;
1010 1012 border: 1px solid #e2e2e2;
1011 1013 border-radius: 3px;
1012 1014 width:auto;
1013 1015 overflow-x: auto;
1014 1016 overflow-y: hidden;
1015 1017 }
1016 1018
1017 1019 div.wiki ul.toc {
1018 1020 background-color: #ffffdd;
1019 1021 border: 1px solid #e4e4e4;
1020 1022 padding: 4px;
1021 1023 line-height: 1.2em;
1022 1024 margin-bottom: 12px;
1023 1025 margin-right: 12px;
1024 1026 margin-left: 0;
1025 1027 display: table
1026 1028 }
1027 1029 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
1028 1030
1029 1031 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
1030 1032 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
1031 1033 div.wiki ul.toc ul { margin: 0; padding: 0; }
1032 1034 div.wiki ul.toc li {list-style-type:none; margin: 0; font-size:12px;}
1033 1035 div.wiki ul.toc li li {margin-left: 1.5em; font-size:10px;}
1034 1036 div.wiki ul.toc a {
1035 1037 font-size: 0.9em;
1036 1038 font-weight: normal;
1037 1039 text-decoration: none;
1038 1040 color: #606060;
1039 1041 }
1040 1042 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
1041 1043
1042 1044 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
1043 1045 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
1044 1046 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
1045 1047
1046 1048 div.wiki img {vertical-align:middle; max-width:100%;}
1047 1049
1048 1050 /***** My page layout *****/
1049 1051 .block-receiver {
1050 1052 border:1px dashed #c0c0c0;
1051 1053 margin-bottom: 20px;
1052 1054 padding: 15px 0 15px 0;
1053 1055 }
1054 1056
1055 1057 .mypage-box {
1056 1058 margin:0 0 20px 0;
1057 1059 color:#505050;
1058 1060 line-height:1.5em;
1059 1061 }
1060 1062
1061 1063 .handle {cursor: move;}
1062 1064
1063 1065 a.close-icon {
1064 1066 display:block;
1065 1067 margin-top:3px;
1066 1068 overflow:hidden;
1067 1069 width:12px;
1068 1070 height:12px;
1069 1071 background-repeat: no-repeat;
1070 1072 cursor:pointer;
1071 1073 background-image:url('../images/close.png');
1072 1074 }
1073 1075 a.close-icon:hover {background-image:url('../images/close_hl.png');}
1074 1076
1075 1077 /***** Gantt chart *****/
1076 1078 .gantt_hdr {
1077 1079 position:absolute;
1078 1080 top:0;
1079 1081 height:16px;
1080 1082 border-top: 1px solid #c0c0c0;
1081 1083 border-bottom: 1px solid #c0c0c0;
1082 1084 border-right: 1px solid #c0c0c0;
1083 1085 text-align: center;
1084 1086 overflow: hidden;
1085 1087 }
1086 1088
1087 1089 .gantt_hdr.nwday {background-color:#f1f1f1; color:#999;}
1088 1090
1089 1091 .gantt_subjects { font-size: 0.8em; }
1090 1092 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
1091 1093
1092 1094 .task {
1093 1095 position: absolute;
1094 1096 height:8px;
1095 1097 font-size:0.8em;
1096 1098 color:#888;
1097 1099 padding:0;
1098 1100 margin:0;
1099 1101 line-height:16px;
1100 1102 white-space:nowrap;
1101 1103 }
1102 1104
1103 1105 .task.label {width:100%;}
1104 1106 .task.label.project, .task.label.version { font-weight: bold; }
1105 1107
1106 1108 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
1107 1109 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
1108 1110 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
1109 1111
1110 1112 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
1111 1113 .task_late.parent, .task_done.parent { height: 3px;}
1112 1114 .task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
1113 1115 .task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
1114 1116
1115 1117 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
1116 1118 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
1117 1119 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
1118 1120 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
1119 1121
1120 1122 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
1121 1123 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
1122 1124 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
1123 1125 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
1124 1126
1125 1127 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
1126 1128 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
1127 1129
1128 1130 /***** Icons *****/
1129 1131 .icon {
1130 1132 background-position: 0% 50%;
1131 1133 background-repeat: no-repeat;
1132 1134 padding-left: 20px;
1133 1135 padding-top: 2px;
1134 1136 padding-bottom: 3px;
1135 1137 }
1136 1138 .icon-only {
1137 1139 background-position: 0% 50%;
1138 1140 background-repeat: no-repeat;
1139 1141 padding-left: 16px;
1140 }
1141 a.icon-only {
1142 1142 display: inline-block;
1143 1143 width: 0;
1144 1144 height: 16px;
1145 1145 overflow: hidden;
1146 1146 padding-top: 0;
1147 1147 padding-bottom: 0;
1148 1148 font-size: 8px;
1149 1149 vertical-align: text-bottom;
1150 1150 }
1151 a.icon-only::after {
1151 .icon-only::after {
1152 1152 content: "&nbsp;";
1153 1153 }
1154 1154
1155 1155 .icon-add { background-image: url(../images/add.png); }
1156 1156 .icon-edit { background-image: url(../images/edit.png); }
1157 1157 .icon-copy { background-image: url(../images/copy.png); }
1158 1158 .icon-duplicate { background-image: url(../images/duplicate.png); }
1159 1159 .icon-del { background-image: url(../images/delete.png); }
1160 1160 .icon-move { background-image: url(../images/move.png); }
1161 1161 .icon-save { background-image: url(../images/save.png); }
1162 1162 .icon-cancel { background-image: url(../images/cancel.png); }
1163 1163 .icon-multiple { background-image: url(../images/table_multiple.png); }
1164 1164 .icon-folder { background-image: url(../images/folder.png); }
1165 1165 .open .icon-folder { background-image: url(../images/folder_open.png); }
1166 1166 .icon-package { background-image: url(../images/package.png); }
1167 1167 .icon-user { background-image: url(../images/user.png); }
1168 1168 .icon-projects { background-image: url(../images/projects.png); }
1169 1169 .icon-help { background-image: url(../images/help.png); }
1170 1170 .icon-attachment { background-image: url(../images/attachment.png); }
1171 1171 .icon-history { background-image: url(../images/history.png); }
1172 1172 .icon-time { background-image: url(../images/time.png); }
1173 1173 .icon-time-add { background-image: url(../images/time_add.png); }
1174 1174 .icon-stats { background-image: url(../images/stats.png); }
1175 1175 .icon-warning { background-image: url(../images/warning.png); }
1176 1176 .icon-error { background-image: url(../images/exclamation.png); }
1177 1177 .icon-fav { background-image: url(../images/fav.png); }
1178 1178 .icon-fav-off { background-image: url(../images/fav_off.png); }
1179 1179 .icon-reload { background-image: url(../images/reload.png); }
1180 1180 .icon-lock { background-image: url(../images/locked.png); }
1181 1181 .icon-unlock { background-image: url(../images/unlock.png); }
1182 1182 .icon-checked { background-image: url(../images/toggle_check.png); }
1183 1183 .icon-details { background-image: url(../images/zoom_in.png); }
1184 1184 .icon-report { background-image: url(../images/report.png); }
1185 1185 .icon-comment { background-image: url(../images/comment.png); }
1186 1186 .icon-summary { background-image: url(../images/lightning.png); }
1187 1187 .icon-server-authentication { background-image: url(../images/server_key.png); }
1188 1188 .icon-issue { background-image: url(../images/ticket.png); }
1189 1189 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
1190 1190 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
1191 1191 .icon-magnifier { background-image: url(../images/magnifier.png); }
1192 1192 .icon-passwd { background-image: url(../images/textfield_key.png); }
1193 1193 .icon-test { background-image: url(../images/bullet_go.png); }
1194 1194 .icon-email { background-image: url(../images/email.png); }
1195 1195 .icon-email-disabled { background-image: url(../images/email_disabled.png); }
1196 1196 .icon-email-add { background-image: url(../images/email_add.png); }
1197 1197 .icon-move-up { background-image: url(../images/1uparrow.png); }
1198 1198 .icon-move-top { background-image: url(../images/2uparrow.png); }
1199 1199 .icon-move-down { background-image: url(../images/1downarrow.png); }
1200 1200 .icon-move-bottom { background-image: url(../images/2downarrow.png); }
1201 1201 .icon-ok { background-image: url(../images/true.png); }
1202 1202 .icon-not-ok { background-image: url(../images/false.png); }
1203 1203 .icon-link-break { background-image: url(../images/link_break.png); }
1204 1204 .icon-list { background-image: url(../images/text_list_bullets.png); }
1205 1205
1206 1206 .icon-file { background-image: url(../images/files/default.png); }
1207 1207 .icon-file.text-plain { background-image: url(../images/files/text.png); }
1208 1208 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
1209 1209 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
1210 1210 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
1211 1211 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
1212 1212 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
1213 1213 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
1214 1214 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
1215 1215 .icon-file.text-css { background-image: url(../images/files/css.png); }
1216 1216 .icon-file.text-html { background-image: url(../images/files/html.png); }
1217 1217 .icon-file.image-gif { background-image: url(../images/files/image.png); }
1218 1218 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
1219 1219 .icon-file.image-png { background-image: url(../images/files/image.png); }
1220 1220 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
1221 1221 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
1222 1222 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
1223 1223 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
1224 1224
1225 1225 .sort-handle { width:16px; height:16px; background:url(../images/reorder.png) no-repeat 0 50%; cursor:move; }
1226 1226 .sort-handle.ajax-loading { background-image: url(../images/loading.gif); }
1227 1227 tr.ui-sortable-helper { border:1px solid #e4e4e4; }
1228 1228
1229 1229 .contextual>.icon:not(:first-child), .buttons>.icon:not(:first-child) { margin-left: 5px; }
1230 1230
1231 1231 img.gravatar {
1232 1232 vertical-align: middle;
1233 1233 border-radius: 20%;
1234 1234 }
1235 1235
1236 1236 div.issue img.gravatar {
1237 1237 float: left;
1238 1238 margin: 0 6px 0 0;
1239 1239 }
1240 1240
1241 1241 h2 img.gravatar {margin: -2px 4px -4px 0;}
1242 1242 h3 img.gravatar {margin: -4px 4px -4px 0;}
1243 1243 h4 img.gravatar {margin: -6px 4px -4px 0;}
1244 1244 td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;}
1245 1245 #activity dt img.gravatar {float: left; margin: 0 1em 1em 0;}
1246 1246 /* Used on 12px Gravatar img tags without the icon background */
1247 1247 .icon-gravatar {float: left; margin-right: 4px;}
1248 1248
1249 1249 #activity dt, .journal {clear: left;}
1250 1250
1251 1251 .journal-link {float: right;}
1252 1252
1253 1253 h2 img { vertical-align:middle; }
1254 1254
1255 1255 .hascontextmenu { cursor: context-menu; }
1256 1256
1257 1257 .sample-data {border:1px solid #ccc; border-collapse:collapse; background-color:#fff; margin:0.5em;}
1258 1258 .sample-data td {border:1px solid #ccc; padding: 2px 4px; font-family: Consolas, Menlo, "Liberation Mono", Courier, monospace;}
1259 1259 .sample-data tr:first-child td {font-weight:bold; text-align:center;}
1260 1260
1261 1261 .ui-progressbar {position: relative;}
1262 1262 #progress-label {
1263 1263 position: absolute; left: 50%; top: 4px;
1264 1264 font-weight: bold;
1265 1265 color: #555; text-shadow: 1px 1px 0 #fff;
1266 1266 }
1267 1267
1268 1268 /* Custom JQuery styles */
1269 1269 .ui-datepicker-title select {width:70px !important; margin-top:-2px !important; margin-right:4px !important;}
1270 1270
1271 1271
1272 1272 /************* CodeRay styles *************/
1273 1273 .syntaxhl div {display: inline;}
1274 1274 .syntaxhl .code pre { overflow: auto }
1275 1275
1276 1276 .syntaxhl .annotation { color:#007 }
1277 1277 .syntaxhl .attribute-name { color:#b48 }
1278 1278 .syntaxhl .attribute-value { color:#700 }
1279 1279 .syntaxhl .binary { color:#549 }
1280 1280 .syntaxhl .binary .char { color:#325 }
1281 1281 .syntaxhl .binary .delimiter { color:#325 }
1282 1282 .syntaxhl .char { color:#D20 }
1283 1283 .syntaxhl .char .content { color:#D20 }
1284 1284 .syntaxhl .char .delimiter { color:#710 }
1285 1285 .syntaxhl .class { color:#B06; font-weight:bold }
1286 1286 .syntaxhl .class-variable { color:#369 }
1287 1287 .syntaxhl .color { color:#0A0 }
1288 1288 .syntaxhl .comment { color:#777 }
1289 1289 .syntaxhl .comment .char { color:#444 }
1290 1290 .syntaxhl .comment .delimiter { color:#444 }
1291 1291 .syntaxhl .constant { color:#036; font-weight:bold }
1292 1292 .syntaxhl .decorator { color:#B0B }
1293 1293 .syntaxhl .definition { color:#099; font-weight:bold }
1294 1294 .syntaxhl .delimiter { color:black }
1295 1295 .syntaxhl .directive { color:#088; font-weight:bold }
1296 1296 .syntaxhl .docstring { color:#D42; }
1297 1297 .syntaxhl .doctype { color:#34b }
1298 1298 .syntaxhl .done { text-decoration: line-through; color: gray }
1299 1299 .syntaxhl .entity { color:#800; font-weight:bold }
1300 1300 .syntaxhl .error { color:#F00; background-color:#FAA }
1301 1301 .syntaxhl .escape { color:#666 }
1302 1302 .syntaxhl .exception { color:#C00; font-weight:bold }
1303 1303 .syntaxhl .float { color:#60E }
1304 1304 .syntaxhl .function { color:#06B; font-weight:bold }
1305 1305 .syntaxhl .function .delimiter { color:#059 }
1306 1306 .syntaxhl .function .content { color:#037 }
1307 1307 .syntaxhl .global-variable { color:#d70 }
1308 1308 .syntaxhl .hex { color:#02b }
1309 1309 .syntaxhl .id { color:#33D; font-weight:bold }
1310 1310 .syntaxhl .include { color:#B44; font-weight:bold }
1311 1311 .syntaxhl .inline { background-color: hsla(0,0%,0%,0.07); color: black }
1312 1312 .syntaxhl .inline-delimiter { font-weight: bold; color: #666 }
1313 1313 .syntaxhl .instance-variable { color:#33B }
1314 1314 .syntaxhl .integer { color:#00D }
1315 1315 .syntaxhl .imaginary { color:#f00 }
1316 1316 .syntaxhl .important { color:#D00 }
1317 1317 .syntaxhl .key { color: #606 }
1318 1318 .syntaxhl .key .char { color: #60f }
1319 1319 .syntaxhl .key .delimiter { color: #404 }
1320 1320 .syntaxhl .keyword { color:#080; font-weight:bold }
1321 1321 .syntaxhl .label { color:#970; font-weight:bold }
1322 1322 .syntaxhl .local-variable { color:#950 }
1323 1323 .syntaxhl .map .content { color:#808 }
1324 1324 .syntaxhl .map .delimiter { color:#40A}
1325 1325 .syntaxhl .map { background-color:hsla(200,100%,50%,0.06); }
1326 1326 .syntaxhl .namespace { color:#707; font-weight:bold }
1327 1327 .syntaxhl .octal { color:#40E }
1328 1328 .syntaxhl .operator { }
1329 1329 .syntaxhl .predefined { color:#369; font-weight:bold }
1330 1330 .syntaxhl .predefined-constant { color:#069 }
1331 1331 .syntaxhl .predefined-type { color:#0a8; font-weight:bold }
1332 1332 .syntaxhl .preprocessor { color:#579 }
1333 1333 .syntaxhl .pseudo-class { color:#00C; font-weight:bold }
1334 1334 .syntaxhl .regexp { background-color:hsla(300,100%,50%,0.06); }
1335 1335 .syntaxhl .regexp .content { color:#808 }
1336 1336 .syntaxhl .regexp .delimiter { color:#404 }
1337 1337 .syntaxhl .regexp .modifier { color:#C2C }
1338 1338 .syntaxhl .reserved { color:#080; font-weight:bold }
1339 1339 .syntaxhl .shell { background-color:hsla(120,100%,50%,0.06); }
1340 1340 .syntaxhl .shell .content { color:#2B2 }
1341 1341 .syntaxhl .shell .delimiter { color:#161 }
1342 1342 .syntaxhl .string { background-color:hsla(0,100%,50%,0.05); }
1343 1343 .syntaxhl .string .char { color: #b0b }
1344 1344 .syntaxhl .string .content { color: #D20 }
1345 1345 .syntaxhl .string .delimiter { color: #710 }
1346 1346 .syntaxhl .string .modifier { color: #E40 }
1347 1347 .syntaxhl .symbol { color:#A60 }
1348 1348 .syntaxhl .symbol .content { color:#A60 }
1349 1349 .syntaxhl .symbol .delimiter { color:#740 }
1350 1350 .syntaxhl .tag { color:#070; font-weight:bold }
1351 1351 .syntaxhl .type { color:#339; font-weight:bold }
1352 1352 .syntaxhl .value { color: #088 }
1353 1353 .syntaxhl .variable { color:#037 }
1354 1354
1355 1355 .syntaxhl .insert { background: hsla(120,100%,50%,0.12) }
1356 1356 .syntaxhl .delete { background: hsla(0,100%,50%,0.12) }
1357 1357 .syntaxhl .change { color: #bbf; background: #007 }
1358 1358 .syntaxhl .head { color: #f8f; background: #505 }
1359 1359 .syntaxhl .head .filename { color: white; }
1360 1360
1361 1361 .syntaxhl .delete .eyecatcher { background-color: hsla(0,100%,50%,0.2); border: 1px solid hsla(0,100%,45%,0.5); margin: -1px; border-bottom: none; border-top-left-radius: 5px; border-top-right-radius: 5px; }
1362 1362 .syntaxhl .insert .eyecatcher { background-color: hsla(120,100%,50%,0.2); border: 1px solid hsla(120,100%,25%,0.5); margin: -1px; border-top: none; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; }
1363 1363
1364 1364 .syntaxhl .insert .insert { color: #0c0; background:transparent; font-weight:bold }
1365 1365 .syntaxhl .delete .delete { color: #c00; background:transparent; font-weight:bold }
1366 1366 .syntaxhl .change .change { color: #88f }
1367 1367 .syntaxhl .head .head { color: #f4f }
1368 1368
1369 1369 /***** Media print specific styles *****/
1370 1370 @media print {
1371 1371 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1372 1372 #main { background: #fff; }
1373 1373 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1374 1374 #wiki_add_attachment { display:none; }
1375 1375 .hide-when-print, .pagination ul.pages, .pagination .per-page { display: none !important; }
1376 1376 .autoscroll {overflow-x: visible;}
1377 1377 table.list {margin-top:0.5em;}
1378 1378 table.list th, table.list td {border: 1px solid #aaa;}
1379 1379 }
1380 1380
1381 1381 /* Accessibility specific styles */
1382 1382 .hidden-for-sighted {
1383 1383 position:absolute;
1384 1384 left:-10000px;
1385 1385 top:auto;
1386 1386 width:1px;
1387 1387 height:1px;
1388 1388 overflow:hidden;
1389 1389 }
@@ -1,4788 +1,4826
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class IssuesControllerTest < ActionController::TestCase
21 21 fixtures :projects,
22 22 :users, :email_addresses, :user_preferences,
23 23 :roles,
24 24 :members,
25 25 :member_roles,
26 26 :issues,
27 27 :issue_statuses,
28 28 :issue_relations,
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 :repositories,
46 46 :changesets
47 47
48 48 include Redmine::I18n
49 49
50 50 def setup
51 51 User.current = nil
52 52 end
53 53
54 54 def test_index
55 55 with_settings :default_language => "en" do
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
62 62 # links to visible issues
63 63 assert_select 'a[href="/issues/1"]', :text => /Cannot print recipes/
64 64 assert_select 'a[href="/issues/5"]', :text => /Subproject issue/
65 65 # private projects hidden
66 66 assert_select 'a[href="/issues/6"]', 0
67 67 assert_select 'a[href="/issues/4"]', 0
68 68 # project column
69 69 assert_select 'th', :text => /Project/
70 70 end
71 71 end
72 72
73 73 def test_index_should_not_list_issues_when_module_disabled
74 74 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
75 75 get :index
76 76 assert_response :success
77 77 assert_template 'index'
78 78 assert_not_nil assigns(:issues)
79 79 assert_nil assigns(:project)
80 80
81 81 assert_select 'a[href="/issues/1"]', 0
82 82 assert_select 'a[href="/issues/5"]', :text => /Subproject issue/
83 83 end
84 84
85 85 def test_index_should_list_visible_issues_only
86 86 get :index, :per_page => 100
87 87 assert_response :success
88 88 assert_not_nil assigns(:issues)
89 89 assert_nil assigns(:issues).detect {|issue| !issue.visible?}
90 90 end
91 91
92 92 def test_index_with_project
93 93 Setting.display_subprojects_issues = 0
94 94 get :index, :project_id => 1
95 95 assert_response :success
96 96 assert_template 'index'
97 97 assert_not_nil assigns(:issues)
98 98
99 99 assert_select 'a[href="/issues/1"]', :text => /Cannot print recipes/
100 100 assert_select 'a[href="/issues/5"]', 0
101 101 end
102 102
103 103 def test_index_with_project_and_subprojects
104 104 Setting.display_subprojects_issues = 1
105 105 get :index, :project_id => 1
106 106 assert_response :success
107 107 assert_template 'index'
108 108 assert_not_nil assigns(:issues)
109 109
110 110 assert_select 'a[href="/issues/1"]', :text => /Cannot print recipes/
111 111 assert_select 'a[href="/issues/5"]', :text => /Subproject issue/
112 112 assert_select 'a[href="/issues/6"]', 0
113 113 end
114 114
115 115 def test_index_with_project_and_subprojects_should_show_private_subprojects_with_permission
116 116 @request.session[:user_id] = 2
117 117 Setting.display_subprojects_issues = 1
118 118 get :index, :project_id => 1
119 119 assert_response :success
120 120 assert_template 'index'
121 121 assert_not_nil assigns(:issues)
122 122
123 123 assert_select 'a[href="/issues/1"]', :text => /Cannot print recipes/
124 124 assert_select 'a[href="/issues/5"]', :text => /Subproject issue/
125 125 assert_select 'a[href="/issues/6"]', :text => /Issue of a private subproject/
126 126 end
127 127
128 128 def test_index_with_project_and_default_filter
129 129 get :index, :project_id => 1, :set_filter => 1
130 130 assert_response :success
131 131 assert_template 'index'
132 132 assert_not_nil assigns(:issues)
133 133
134 134 query = assigns(:query)
135 135 assert_not_nil query
136 136 # default filter
137 137 assert_equal({'status_id' => {:operator => 'o', :values => ['']}}, query.filters)
138 138 end
139 139
140 140 def test_index_with_project_and_filter
141 141 get :index, :project_id => 1, :set_filter => 1,
142 142 :f => ['tracker_id'],
143 143 :op => {'tracker_id' => '='},
144 144 :v => {'tracker_id' => ['1']}
145 145 assert_response :success
146 146 assert_template 'index'
147 147 assert_not_nil assigns(:issues)
148 148
149 149 query = assigns(:query)
150 150 assert_not_nil query
151 151 assert_equal({'tracker_id' => {:operator => '=', :values => ['1']}}, query.filters)
152 152 end
153 153
154 154 def test_index_with_short_filters
155 155 to_test = {
156 156 'status_id' => {
157 157 'o' => { :op => 'o', :values => [''] },
158 158 'c' => { :op => 'c', :values => [''] },
159 159 '7' => { :op => '=', :values => ['7'] },
160 160 '7|3|4' => { :op => '=', :values => ['7', '3', '4'] },
161 161 '=7' => { :op => '=', :values => ['7'] },
162 162 '!3' => { :op => '!', :values => ['3'] },
163 163 '!7|3|4' => { :op => '!', :values => ['7', '3', '4'] }},
164 164 'subject' => {
165 165 'This is a subject' => { :op => '=', :values => ['This is a subject'] },
166 166 'o' => { :op => '=', :values => ['o'] },
167 167 '~This is part of a subject' => { :op => '~', :values => ['This is part of a subject'] },
168 168 '!~This is part of a subject' => { :op => '!~', :values => ['This is part of a subject'] }},
169 169 'tracker_id' => {
170 170 '3' => { :op => '=', :values => ['3'] },
171 171 '=3' => { :op => '=', :values => ['3'] }},
172 172 'start_date' => {
173 173 '2011-10-12' => { :op => '=', :values => ['2011-10-12'] },
174 174 '=2011-10-12' => { :op => '=', :values => ['2011-10-12'] },
175 175 '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] },
176 176 '<=2011-10-12' => { :op => '<=', :values => ['2011-10-12'] },
177 177 '><2011-10-01|2011-10-30' => { :op => '><', :values => ['2011-10-01', '2011-10-30'] },
178 178 '<t+2' => { :op => '<t+', :values => ['2'] },
179 179 '>t+2' => { :op => '>t+', :values => ['2'] },
180 180 't+2' => { :op => 't+', :values => ['2'] },
181 181 't' => { :op => 't', :values => [''] },
182 182 'w' => { :op => 'w', :values => [''] },
183 183 '>t-2' => { :op => '>t-', :values => ['2'] },
184 184 '<t-2' => { :op => '<t-', :values => ['2'] },
185 185 't-2' => { :op => 't-', :values => ['2'] }},
186 186 'created_on' => {
187 187 '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] },
188 188 '<t-2' => { :op => '<t-', :values => ['2'] },
189 189 '>t-2' => { :op => '>t-', :values => ['2'] },
190 190 't-2' => { :op => 't-', :values => ['2'] }},
191 191 'cf_1' => {
192 192 'c' => { :op => '=', :values => ['c'] },
193 193 '!c' => { :op => '!', :values => ['c'] },
194 194 '!*' => { :op => '!*', :values => [''] },
195 195 '*' => { :op => '*', :values => [''] }},
196 196 'estimated_hours' => {
197 197 '=13.4' => { :op => '=', :values => ['13.4'] },
198 198 '>=45' => { :op => '>=', :values => ['45'] },
199 199 '<=125' => { :op => '<=', :values => ['125'] },
200 200 '><10.5|20.5' => { :op => '><', :values => ['10.5', '20.5'] },
201 201 '!*' => { :op => '!*', :values => [''] },
202 202 '*' => { :op => '*', :values => [''] }}
203 203 }
204 204
205 205 default_filter = { 'status_id' => {:operator => 'o', :values => [''] }}
206 206
207 207 to_test.each do |field, expression_and_expected|
208 208 expression_and_expected.each do |filter_expression, expected|
209 209
210 210 get :index, :set_filter => 1, field => filter_expression
211 211
212 212 assert_response :success
213 213 assert_template 'index'
214 214 assert_not_nil assigns(:issues)
215 215
216 216 query = assigns(:query)
217 217 assert_not_nil query
218 218 assert query.has_filter?(field)
219 219 assert_equal(default_filter.merge({field => {:operator => expected[:op], :values => expected[:values]}}), query.filters)
220 220 end
221 221 end
222 222 end
223 223
224 224 def test_index_with_project_and_empty_filters
225 225 get :index, :project_id => 1, :set_filter => 1, :fields => ['']
226 226 assert_response :success
227 227 assert_template 'index'
228 228 assert_not_nil assigns(:issues)
229 229
230 230 query = assigns(:query)
231 231 assert_not_nil query
232 232 # no filter
233 233 assert_equal({}, query.filters)
234 234 end
235 235
236 236 def test_index_with_project_custom_field_filter
237 237 field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
238 238 CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
239 239 CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo')
240 240 filter_name = "project.cf_#{field.id}"
241 241 @request.session[:user_id] = 1
242 242
243 243 get :index, :set_filter => 1,
244 244 :f => [filter_name],
245 245 :op => {filter_name => '='},
246 246 :v => {filter_name => ['Foo']}
247 247 assert_response :success
248 248 assert_template 'index'
249 249 assert_equal [3, 5], assigns(:issues).map(&:project_id).uniq.sort
250 250 end
251 251
252 252 def test_index_with_query
253 253 get :index, :project_id => 1, :query_id => 5
254 254 assert_response :success
255 255 assert_template 'index'
256 256 assert_not_nil assigns(:issues)
257 257 assert_nil assigns(:issue_count_by_group)
258 258 end
259 259
260 260 def test_index_with_query_grouped_by_tracker
261 261 get :index, :project_id => 1, :query_id => 6
262 262 assert_response :success
263 263 assert_template 'index'
264 264 assert_not_nil assigns(:issues)
265 265 assert_not_nil assigns(:issue_count_by_group)
266 266 end
267 267
268 268 def test_index_with_query_grouped_and_sorted_by_category
269 269 get :index, :project_id => 1, :set_filter => 1, :group_by => "category", :sort => "category"
270 270 assert_response :success
271 271 assert_template 'index'
272 272 assert_not_nil assigns(:issues)
273 273 assert_not_nil assigns(:issue_count_by_group)
274 274 end
275 275
276 276 def test_index_with_query_grouped_and_sorted_by_fixed_version
277 277 get :index, :project_id => 1, :set_filter => 1, :group_by => "fixed_version", :sort => "fixed_version"
278 278 assert_response :success
279 279 assert_template 'index'
280 280 assert_not_nil assigns(:issues)
281 281 assert_not_nil assigns(:issue_count_by_group)
282 282 end
283 283
284 284 def test_index_with_query_grouped_and_sorted_by_fixed_version_in_reverse_order
285 285 get :index, :project_id => 1, :set_filter => 1, :group_by => "fixed_version", :sort => "fixed_version:desc"
286 286 assert_response :success
287 287 assert_template 'index'
288 288 assert_not_nil assigns(:issues)
289 289 assert_not_nil assigns(:issue_count_by_group)
290 290 end
291 291
292 292 def test_index_with_query_grouped_by_list_custom_field
293 293 get :index, :project_id => 1, :query_id => 9
294 294 assert_response :success
295 295 assert_template 'index'
296 296 assert_not_nil assigns(:issues)
297 297 assert_not_nil assigns(:issue_count_by_group)
298 298 end
299 299
300 300 def test_index_with_query_grouped_by_key_value_custom_field
301 301 cf = IssueCustomField.create!(:name => 'Key', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'enumeration')
302 302 cf.enumerations << valueb = CustomFieldEnumeration.new(:name => 'Value B', :position => 1)
303 303 cf.enumerations << valuea = CustomFieldEnumeration.new(:name => 'Value A', :position => 2)
304 304 CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => valueb.id)
305 305 CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => valueb.id)
306 306 CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => valuea.id)
307 307 CustomValue.create!(:custom_field => cf, :customized => Issue.find(5), :value => '')
308 308
309 309 get :index, :project_id => 1, :set_filter => 1, :group_by => "cf_#{cf.id}"
310 310 assert_response :success
311 311 assert_template 'index'
312 312 assert_not_nil assigns(:issues)
313 313 assert_not_nil assigns(:issue_count_by_group)
314 314
315 315 assert_select 'tr.group', 3
316 316 assert_select 'tr.group' do
317 317 assert_select 'span.name', :text => 'Value B'
318 318 assert_select 'span.count', :text => '2'
319 319 end
320 320 assert_select 'tr.group' do
321 321 assert_select 'span.name', :text => 'Value A'
322 322 assert_select 'span.count', :text => '1'
323 323 end
324 324 end
325 325
326 326 def test_index_with_query_grouped_by_user_custom_field
327 327 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'user')
328 328 CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '2')
329 329 CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => '3')
330 330 CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => '3')
331 331 CustomValue.create!(:custom_field => cf, :customized => Issue.find(5), :value => '')
332 332
333 333 get :index, :project_id => 1, :set_filter => 1, :group_by => "cf_#{cf.id}"
334 334 assert_response :success
335 335
336 336 assert_select 'tr.group', 3
337 337 assert_select 'tr.group' do
338 338 assert_select 'a', :text => 'John Smith'
339 339 assert_select 'span.count', :text => '1'
340 340 end
341 341 assert_select 'tr.group' do
342 342 assert_select 'a', :text => 'Dave Lopper'
343 343 assert_select 'span.count', :text => '2'
344 344 end
345 345 end
346 346
347 347 def test_index_grouped_by_boolean_custom_field_should_distinguish_blank_and_false_values
348 348 cf = IssueCustomField.create!(:name => 'Bool', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'bool')
349 349 CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '1')
350 350 CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => '0')
351 351 CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => '')
352 352
353 353 with_settings :default_language => 'en' do
354 354 get :index, :project_id => 1, :set_filter => 1, :group_by => "cf_#{cf.id}"
355 355 assert_response :success
356 356 end
357 357
358 358 assert_select 'tr.group', 3
359 359 assert_select 'tr.group', :text => /Yes/
360 360 assert_select 'tr.group', :text => /No/
361 361 assert_select 'tr.group', :text => /blank/
362 362 end
363 363
364 364 def test_index_grouped_by_boolean_custom_field_with_false_group_in_first_position_should_show_the_group
365 365 cf = IssueCustomField.create!(:name => 'Bool', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'bool', :is_filter => true)
366 366 CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '0')
367 367 CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => '0')
368 368
369 369 with_settings :default_language => 'en' do
370 370 get :index, :project_id => 1, :set_filter => 1, "cf_#{cf.id}" => "*", :group_by => "cf_#{cf.id}"
371 371 assert_response :success
372 372 assert_equal [1, 2], assigns(:issues).map(&:id).sort
373 373 end
374 374
375 375 assert_select 'tr.group', 1
376 376 assert_select 'tr.group', :text => /No/
377 377 end
378 378
379 379 def test_index_with_query_grouped_by_tracker_in_normal_order
380 380 3.times {|i| Issue.generate!(:tracker_id => (i + 1))}
381 381
382 382 get :index, :set_filter => 1, :group_by => 'tracker', :sort => 'id:desc'
383 383 assert_response :success
384 384
385 385 trackers = assigns(:issues).map(&:tracker).uniq
386 386 assert_equal [1, 2, 3], trackers.map(&:id)
387 387 end
388 388
389 389 def test_index_with_query_grouped_by_tracker_in_reverse_order
390 390 3.times {|i| Issue.generate!(:tracker_id => (i + 1))}
391 391
392 392 get :index, :set_filter => 1, :group_by => 'tracker', :sort => 'id:desc,tracker:desc'
393 393 assert_response :success
394 394
395 395 trackers = assigns(:issues).map(&:tracker).uniq
396 396 assert_equal [3, 2, 1], trackers.map(&:id)
397 397 end
398 398
399 399 def test_index_with_query_id_and_project_id_should_set_session_query
400 400 get :index, :project_id => 1, :query_id => 4
401 401 assert_response :success
402 402 assert_kind_of Hash, session[:issue_query]
403 403 assert_equal 4, session[:issue_query][:id]
404 404 assert_equal 1, session[:issue_query][:project_id]
405 405 end
406 406
407 407 def test_index_with_invalid_query_id_should_respond_404
408 408 get :index, :project_id => 1, :query_id => 999
409 409 assert_response 404
410 410 end
411 411
412 412 def test_index_with_cross_project_query_in_session_should_show_project_issues
413 413 q = IssueQuery.create!(:name => "test", :user_id => 2, :visibility => IssueQuery::VISIBILITY_PRIVATE, :project => nil)
414 414 @request.session[:issue_query] = {:id => q.id, :project_id => 1}
415 415
416 416 with_settings :display_subprojects_issues => '0' do
417 417 get :index, :project_id => 1
418 418 end
419 419 assert_response :success
420 420 assert_not_nil assigns(:query)
421 421 assert_equal q.id, assigns(:query).id
422 422 assert_equal 1, assigns(:query).project_id
423 423 assert_equal [1], assigns(:issues).map(&:project_id).uniq
424 424 end
425 425
426 426 def test_private_query_should_not_be_available_to_other_users
427 427 q = IssueQuery.create!(:name => "private", :user => User.find(2), :visibility => IssueQuery::VISIBILITY_PRIVATE, :project => nil)
428 428 @request.session[:user_id] = 3
429 429
430 430 get :index, :query_id => q.id
431 431 assert_response 403
432 432 end
433 433
434 434 def test_private_query_should_be_available_to_its_user
435 435 q = IssueQuery.create!(:name => "private", :user => User.find(2), :visibility => IssueQuery::VISIBILITY_PRIVATE, :project => nil)
436 436 @request.session[:user_id] = 2
437 437
438 438 get :index, :query_id => q.id
439 439 assert_response :success
440 440 end
441 441
442 442 def test_public_query_should_be_available_to_other_users
443 443 q = IssueQuery.create!(:name => "public", :user => User.find(2), :visibility => IssueQuery::VISIBILITY_PUBLIC, :project => nil)
444 444 @request.session[:user_id] = 3
445 445
446 446 get :index, :query_id => q.id
447 447 assert_response :success
448 448 end
449 449
450 450 def test_index_should_omit_page_param_in_export_links
451 451 get :index, :page => 2
452 452 assert_response :success
453 453 assert_select 'a.atom[href="/issues.atom"]'
454 454 assert_select 'a.csv[href="/issues.csv"]'
455 455 assert_select 'a.pdf[href="/issues.pdf"]'
456 456 assert_select 'form#csv-export-form[action="/issues.csv"]'
457 457 end
458 458
459 459 def test_index_should_not_warn_when_not_exceeding_export_limit
460 460 with_settings :issues_export_limit => 200 do
461 461 get :index
462 462 assert_select '#csv-export-options p.icon-warning', 0
463 463 end
464 464 end
465 465
466 466 def test_index_should_warn_when_exceeding_export_limit
467 467 with_settings :issues_export_limit => 2 do
468 468 get :index
469 469 assert_select '#csv-export-options p.icon-warning', :text => %r{limit: 2}
470 470 end
471 471 end
472 472
473 473 def test_index_should_include_query_params_as_hidden_fields_in_csv_export_form
474 474 get :index, :project_id => 1, :set_filter => "1", :tracker_id => "2", :sort => 'status', :c => ["status", "priority"]
475 475
476 476 assert_select '#csv-export-form[action=?]', '/projects/ecookbook/issues.csv'
477 477 assert_select '#csv-export-form[method=?]', 'get'
478 478
479 479 assert_select '#csv-export-form' do
480 480 assert_select 'input[name=?][value=?]', 'set_filter', '1'
481 481
482 482 assert_select 'input[name=?][value=?]', 'f[]', 'tracker_id'
483 483 assert_select 'input[name=?][value=?]', 'op[tracker_id]', '='
484 484 assert_select 'input[name=?][value=?]', 'v[tracker_id][]', '2'
485 485
486 486 assert_select 'input[name=?][value=?]', 'c[]', 'status'
487 487 assert_select 'input[name=?][value=?]', 'c[]', 'priority'
488 488
489 489 assert_select 'input[name=?][value=?]', 'sort', 'status'
490 490 end
491 491
492 492 get :index, :project_id => 1, :set_filter => "1", :f => []
493 493 assert_select '#csv-export-form input[name=?][value=?]', 'f[]', ''
494 494 end
495 495
496 496 def test_index_csv
497 497 get :index, :format => 'csv'
498 498 assert_response :success
499 499 assert_not_nil assigns(:issues)
500 500 assert_equal 'text/csv; header=present', @response.content_type
501 501 assert @response.body.starts_with?("#,")
502 502 lines = @response.body.chomp.split("\n")
503 503 assert_equal assigns(:query).columns.size, lines[0].split(',').size
504 504 end
505 505
506 506 def test_index_csv_with_project
507 507 get :index, :project_id => 1, :format => 'csv'
508 508 assert_response :success
509 509 assert_not_nil assigns(:issues)
510 510 assert_equal 'text/csv; header=present', @response.content_type
511 511 end
512 512
513 513 def test_index_csv_without_any_filters
514 514 @request.session[:user_id] = 1
515 515 Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 5, :subject => 'Closed issue', :author_id => 1)
516 516 get :index, :set_filter => 1, :f => [], :format => 'csv'
517 517 assert_response :success
518 518 assert_equal Issue.count, assigns(:issues).count
519 519 end
520 520
521 521 def test_index_csv_with_description
522 522 Issue.generate!(:description => 'test_index_csv_with_description')
523 523
524 524 with_settings :default_language => 'en' do
525 525 get :index, :format => 'csv', :csv => {:description => '1'}
526 526 assert_response :success
527 527 assert_not_nil assigns(:issues)
528 528 end
529 529
530 530 assert_equal 'text/csv; header=present', response.content_type
531 531 headers = response.body.chomp.split("\n").first.split(',')
532 532 assert_include 'Description', headers
533 533 assert_include 'test_index_csv_with_description', response.body
534 534 end
535 535
536 536 def test_index_csv_with_spent_time_column
537 537 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :subject => 'test_index_csv_with_spent_time_column', :author_id => 2)
538 538 TimeEntry.create!(:project => issue.project, :issue => issue, :hours => 7.33, :user => User.find(2), :spent_on => Date.today)
539 539
540 540 get :index, :format => 'csv', :set_filter => '1', :c => %w(subject spent_hours)
541 541 assert_response :success
542 542 assert_equal 'text/csv; header=present', @response.content_type
543 543 lines = @response.body.chomp.split("\n")
544 544 assert_include "#{issue.id},#{issue.subject},7.33", lines
545 545 end
546 546
547 547 def test_index_csv_with_all_columns
548 548 get :index, :format => 'csv', :csv => {:columns => 'all'}
549 549 assert_response :success
550 550 assert_not_nil assigns(:issues)
551 551 assert_equal 'text/csv; header=present', @response.content_type
552 552 assert_match /\A#,/, response.body
553 553 lines = response.body.chomp.split("\n")
554 554 assert_equal assigns(:query).available_inline_columns.size, lines[0].split(',').size
555 555 end
556 556
557 557 def test_index_csv_with_multi_column_field
558 558 CustomField.find(1).update_attribute :multiple, true
559 559 issue = Issue.find(1)
560 560 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
561 561 issue.save!
562 562
563 563 get :index, :format => 'csv', :csv => {:columns => 'all'}
564 564 assert_response :success
565 565 lines = @response.body.chomp.split("\n")
566 566 assert lines.detect {|line| line.include?('"MySQL, Oracle"')}
567 567 end
568 568
569 569 def test_index_csv_should_format_float_custom_fields_with_csv_decimal_separator
570 570 field = IssueCustomField.create!(:name => 'Float', :is_for_all => true, :tracker_ids => [1], :field_format => 'float')
571 571 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id => '185.6'})
572 572
573 573 with_settings :default_language => 'fr' do
574 574 get :index, :format => 'csv', :csv => {:columns => 'all'}
575 575 assert_response :success
576 576 issue_line = response.body.chomp.split("\n").map {|line| line.split(';')}.detect {|line| line[0]==issue.id.to_s}
577 577 assert_include '185,60', issue_line
578 578 end
579 579
580 580 with_settings :default_language => 'en' do
581 581 get :index, :format => 'csv', :csv => {:columns => 'all'}
582 582 assert_response :success
583 583 issue_line = response.body.chomp.split("\n").map {|line| line.split(',')}.detect {|line| line[0]==issue.id.to_s}
584 584 assert_include '185.60', issue_line
585 585 end
586 586 end
587 587
588 588 def test_index_csv_should_fill_parent_column_with_parent_id
589 589 Issue.delete_all
590 590 parent = Issue.generate!
591 591 child = Issue.generate!(:parent_issue_id => parent.id)
592 592
593 593 with_settings :default_language => 'en' do
594 594 get :index, :format => 'csv', :c => %w(parent)
595 595 end
596 596 lines = response.body.split("\n")
597 597 assert_include "#{child.id},#{parent.id}", lines
598 598 end
599 599
600 600 def test_index_csv_big_5
601 601 with_settings :default_language => "zh-TW" do
602 602 str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88".force_encoding('UTF-8')
603 603 str_big5 = "\xa4@\xa4\xeb".force_encoding('Big5')
604 604 issue = Issue.generate!(:subject => str_utf8)
605 605
606 606 get :index, :project_id => 1,
607 607 :f => ['subject'],
608 608 :op => '=', :values => [str_utf8],
609 609 :format => 'csv'
610 610 assert_equal 'text/csv; header=present', @response.content_type
611 611 lines = @response.body.chomp.split("\n")
612 612 header = lines[0]
613 613 status = "\xaa\xac\xbaA".force_encoding('Big5')
614 614 assert_include status, header
615 615 issue_line = lines.find {|l| l =~ /^#{issue.id},/}
616 616 assert_include str_big5, issue_line
617 617 end
618 618 end
619 619
620 620 def test_index_csv_cannot_convert_should_be_replaced_big_5
621 621 with_settings :default_language => "zh-TW" do
622 622 str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85".force_encoding('UTF-8')
623 623 issue = Issue.generate!(:subject => str_utf8)
624 624
625 625 get :index, :project_id => 1,
626 626 :f => ['subject'],
627 627 :op => '=', :values => [str_utf8],
628 628 :c => ['status', 'subject'],
629 629 :format => 'csv',
630 630 :set_filter => 1
631 631 assert_equal 'text/csv; header=present', @response.content_type
632 632 lines = @response.body.chomp.split("\n")
633 633 header = lines[0]
634 634 issue_line = lines.find {|l| l =~ /^#{issue.id},/}
635 635 s1 = "\xaa\xac\xbaA".force_encoding('Big5') # status
636 636 assert header.include?(s1)
637 637 s2 = issue_line.split(",")[2]
638 638 s3 = "\xa5H?".force_encoding('Big5') # subject
639 639 assert_equal s3, s2
640 640 end
641 641 end
642 642
643 643 def test_index_csv_tw
644 644 with_settings :default_language => "zh-TW" do
645 645 str1 = "test_index_csv_tw"
646 646 issue = Issue.generate!(:subject => str1, :estimated_hours => '1234.5')
647 647
648 648 get :index, :project_id => 1,
649 649 :f => ['subject'],
650 650 :op => '=', :values => [str1],
651 651 :c => ['estimated_hours', 'subject'],
652 652 :format => 'csv',
653 653 :set_filter => 1
654 654 assert_equal 'text/csv; header=present', @response.content_type
655 655 lines = @response.body.chomp.split("\n")
656 656 assert_include "#{issue.id},1234.50,#{str1}", lines
657 657 end
658 658 end
659 659
660 660 def test_index_csv_fr
661 661 with_settings :default_language => "fr" do
662 662 str1 = "test_index_csv_fr"
663 663 issue = Issue.generate!(:subject => str1, :estimated_hours => '1234.5')
664 664
665 665 get :index, :project_id => 1,
666 666 :f => ['subject'],
667 667 :op => '=', :values => [str1],
668 668 :c => ['estimated_hours', 'subject'],
669 669 :format => 'csv',
670 670 :set_filter => 1
671 671 assert_equal 'text/csv; header=present', @response.content_type
672 672 lines = @response.body.chomp.split("\n")
673 673 assert_include "#{issue.id};1234,50;#{str1}", lines
674 674 end
675 675 end
676 676
677 677 def test_index_pdf
678 678 ["en", "zh", "zh-TW", "ja", "ko"].each do |lang|
679 679 with_settings :default_language => lang do
680 680
681 681 get :index
682 682 assert_response :success
683 683 assert_template 'index'
684 684
685 685 get :index, :format => 'pdf'
686 686 assert_response :success
687 687 assert_not_nil assigns(:issues)
688 688 assert_equal 'application/pdf', @response.content_type
689 689
690 690 get :index, :project_id => 1, :format => 'pdf'
691 691 assert_response :success
692 692 assert_not_nil assigns(:issues)
693 693 assert_equal 'application/pdf', @response.content_type
694 694
695 695 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
696 696 assert_response :success
697 697 assert_not_nil assigns(:issues)
698 698 assert_equal 'application/pdf', @response.content_type
699 699 end
700 700 end
701 701 end
702 702
703 703 def test_index_pdf_with_query_grouped_by_list_custom_field
704 704 get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
705 705 assert_response :success
706 706 assert_not_nil assigns(:issues)
707 707 assert_not_nil assigns(:issue_count_by_group)
708 708 assert_equal 'application/pdf', @response.content_type
709 709 end
710 710
711 711 def test_index_atom
712 712 get :index, :project_id => 'ecookbook', :format => 'atom'
713 713 assert_response :success
714 714 assert_template 'common/feed'
715 715 assert_equal 'application/atom+xml', response.content_type
716 716
717 717 assert_select 'feed' do
718 718 assert_select 'link[rel=self][href=?]', 'http://test.host/projects/ecookbook/issues.atom'
719 719 assert_select 'link[rel=alternate][href=?]', 'http://test.host/projects/ecookbook/issues'
720 720 assert_select 'entry link[href=?]', 'http://test.host/issues/1'
721 721 end
722 722 end
723 723
724 724 def test_index_should_include_back_url_input
725 725 get :index, :project_id => 'ecookbook', :foo => 'bar'
726 726 assert_response :success
727 727 assert_select 'input[name=back_url][value=?]', '/projects/ecookbook/issues?foo=bar'
728 728 end
729 729
730 730 def test_index_sort
731 731 get :index, :sort => 'tracker,id:desc'
732 732 assert_response :success
733 733
734 734 sort_params = @request.session['issues_index_sort']
735 735 assert sort_params.is_a?(String)
736 736 assert_equal 'tracker,id:desc', sort_params
737 737
738 738 issues = assigns(:issues)
739 739 assert_not_nil issues
740 740 assert !issues.empty?
741 741 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
742 742 assert_select 'table.issues.sort-by-tracker.sort-asc'
743 743 end
744 744
745 745 def test_index_sort_by_field_not_included_in_columns
746 746 Setting.issue_list_default_columns = %w(subject author)
747 747 get :index, :sort => 'tracker'
748 748 end
749 749
750 750 def test_index_sort_by_assigned_to
751 751 get :index, :sort => 'assigned_to'
752 752 assert_response :success
753 753 assignees = assigns(:issues).collect(&:assigned_to).compact
754 754 assert_equal assignees.sort, assignees
755 755 assert_select 'table.issues.sort-by-assigned-to.sort-asc'
756 756 end
757 757
758 758 def test_index_sort_by_assigned_to_desc
759 759 get :index, :sort => 'assigned_to:desc'
760 760 assert_response :success
761 761 assignees = assigns(:issues).collect(&:assigned_to).compact
762 762 assert_equal assignees.sort.reverse, assignees
763 763 assert_select 'table.issues.sort-by-assigned-to.sort-desc'
764 764 end
765 765
766 766 def test_index_group_by_assigned_to
767 767 get :index, :group_by => 'assigned_to', :sort => 'priority'
768 768 assert_response :success
769 769 end
770 770
771 771 def test_index_sort_by_author
772 772 get :index, :sort => 'author'
773 773 assert_response :success
774 774 authors = assigns(:issues).collect(&:author)
775 775 assert_equal authors.sort, authors
776 776 end
777 777
778 778 def test_index_sort_by_author_desc
779 779 get :index, :sort => 'author:desc'
780 780 assert_response :success
781 781 authors = assigns(:issues).collect(&:author)
782 782 assert_equal authors.sort.reverse, authors
783 783 end
784 784
785 785 def test_index_group_by_author
786 786 get :index, :group_by => 'author', :sort => 'priority'
787 787 assert_response :success
788 788 end
789 789
790 790 def test_index_sort_by_spent_hours
791 791 get :index, :sort => 'spent_hours:desc'
792 792 assert_response :success
793 793 hours = assigns(:issues).collect(&:spent_hours)
794 794 assert_equal hours.sort.reverse, hours
795 795 end
796 796
797 797 def test_index_sort_by_total_spent_hours
798 798 get :index, :sort => 'total_spent_hours:desc'
799 799 assert_response :success
800 800 hours = assigns(:issues).collect(&:total_spent_hours)
801 801 assert_equal hours.sort.reverse, hours
802 802 end
803 803
804 804 def test_index_sort_by_total_estimated_hours
805 805 get :index, :sort => 'total_estimated_hours:desc'
806 806 assert_response :success
807 807 hours = assigns(:issues).collect(&:total_estimated_hours)
808 808 assert_equal hours.sort.reverse, hours
809 809 end
810 810
811 811 def test_index_sort_by_user_custom_field
812 812 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'user')
813 813 CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '2')
814 814 CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => '3')
815 815 CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => '3')
816 816 CustomValue.create!(:custom_field => cf, :customized => Issue.find(5), :value => '')
817 817
818 818 get :index, :project_id => 1, :set_filter => 1, :sort => "cf_#{cf.id},id"
819 819 assert_response :success
820 820
821 821 assert_equal [2, 3, 1], assigns(:issues).select {|issue| issue.custom_field_value(cf).present?}.map(&:id)
822 822 end
823 823
824 824 def test_index_with_columns
825 825 columns = ['tracker', 'subject', 'assigned_to']
826 826 get :index, :set_filter => 1, :c => columns
827 827 assert_response :success
828 828
829 829 # query should use specified columns
830 830 query = assigns(:query)
831 831 assert_kind_of IssueQuery, query
832 832 assert_equal columns, query.column_names.map(&:to_s)
833 833
834 834 # columns should be stored in session
835 835 assert_kind_of Hash, session[:issue_query]
836 836 assert_kind_of Array, session[:issue_query][:column_names]
837 837 assert_equal columns, session[:issue_query][:column_names].map(&:to_s)
838 838
839 839 # ensure only these columns are kept in the selected columns list
840 840 assert_select 'select#selected_columns option' do
841 841 assert_select 'option', 3
842 842 assert_select 'option[value=tracker]'
843 843 assert_select 'option[value=project]', 0
844 844 end
845 845 end
846 846
847 847 def test_index_without_project_should_implicitly_add_project_column_to_default_columns
848 848 Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to']
849 849 get :index, :set_filter => 1
850 850
851 851 # query should use specified columns
852 852 query = assigns(:query)
853 853 assert_kind_of IssueQuery, query
854 854 assert_equal [:id, :project, :tracker, :subject, :assigned_to], query.columns.map(&:name)
855 855 end
856 856
857 857 def test_index_without_project_and_explicit_default_columns_should_not_add_project_column
858 858 Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to']
859 859 columns = ['id', 'tracker', 'subject', 'assigned_to']
860 860 get :index, :set_filter => 1, :c => columns
861 861
862 862 # query should use specified columns
863 863 query = assigns(:query)
864 864 assert_kind_of IssueQuery, query
865 865 assert_equal columns.map(&:to_sym), query.columns.map(&:name)
866 866 end
867 867
868 868 def test_index_with_default_columns_should_respect_default_columns_order
869 869 columns = ['assigned_to', 'subject', 'status', 'tracker']
870 870 with_settings :issue_list_default_columns => columns do
871 871 get :index, :project_id => 1, :set_filter => 1
872 872
873 873 query = assigns(:query)
874 874 assert_equal (['id'] + columns).map(&:to_sym), query.columns.map(&:name)
875 875 end
876 876 end
877 877
878 878 def test_index_with_custom_field_column
879 879 columns = %w(tracker subject cf_2)
880 880 get :index, :set_filter => 1, :c => columns
881 881 assert_response :success
882 882
883 883 # query should use specified columns
884 884 query = assigns(:query)
885 885 assert_kind_of IssueQuery, query
886 886 assert_equal columns, query.column_names.map(&:to_s)
887 887
888 888 assert_select 'table.issues td.cf_2.string'
889 889 end
890 890
891 891 def test_index_with_multi_custom_field_column
892 892 field = CustomField.find(1)
893 893 field.update_attribute :multiple, true
894 894 issue = Issue.find(1)
895 895 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
896 896 issue.save!
897 897
898 898 get :index, :set_filter => 1, :c => %w(tracker subject cf_1)
899 899 assert_response :success
900 900
901 901 assert_select 'table.issues td.cf_1', :text => 'MySQL, Oracle'
902 902 end
903 903
904 904 def test_index_with_multi_user_custom_field_column
905 905 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
906 906 :tracker_ids => [1], :is_for_all => true)
907 907 issue = Issue.find(1)
908 908 issue.custom_field_values = {field.id => ['2', '3']}
909 909 issue.save!
910 910
911 911 get :index, :set_filter => 1, :c => ['tracker', 'subject', "cf_#{field.id}"]
912 912 assert_response :success
913 913
914 914 assert_select "table.issues td.cf_#{field.id}" do
915 915 assert_select 'a', 2
916 916 assert_select 'a[href=?]', '/users/2', :text => 'John Smith'
917 917 assert_select 'a[href=?]', '/users/3', :text => 'Dave Lopper'
918 918 end
919 919 end
920 920
921 921 def test_index_with_date_column
922 922 with_settings :date_format => '%d/%m/%Y' do
923 923 Issue.find(1).update_attribute :start_date, '1987-08-24'
924 924 get :index, :set_filter => 1, :c => %w(start_date)
925 925 assert_select "table.issues td.start_date", :text => '24/08/1987'
926 926 end
927 927 end
928 928
929 929 def test_index_with_done_ratio_column
930 930 Issue.find(1).update_attribute :done_ratio, 40
931 931 get :index, :set_filter => 1, :c => %w(done_ratio)
932 932 assert_select 'table.issues td.done_ratio' do
933 933 assert_select 'table.progress' do
934 934 assert_select 'td.closed[style=?]', 'width: 40%;'
935 935 end
936 936 end
937 937 end
938 938
939 939 def test_index_with_spent_hours_column
940 940 Issue.expects(:load_visible_spent_hours).once
941 941 get :index, :set_filter => 1, :c => %w(subject spent_hours)
942 942 assert_select 'table.issues tr#issue-3 td.spent_hours', :text => '1.00'
943 943 end
944 944
945 945 def test_index_with_total_spent_hours_column
946 946 Issue.expects(:load_visible_total_spent_hours).once
947 947 get :index, :set_filter => 1, :c => %w(subject total_spent_hours)
948 948 assert_select 'table.issues tr#issue-3 td.total_spent_hours', :text => '1.00'
949 949 end
950 950
951 951 def test_index_with_total_estimated_hours_column
952 952 get :index, :set_filter => 1, :c => %w(subject total_estimated_hours)
953 953 assert_select 'table.issues td.total_estimated_hours'
954 954 end
955 955
956 956 def test_index_should_not_show_spent_hours_column_without_permission
957 957 Role.anonymous.remove_permission! :view_time_entries
958 958 get :index, :set_filter => 1, :c => %w(subject spent_hours)
959 959 assert_select 'td.spent_hours', 0
960 960 end
961 961
962 962 def test_index_with_fixed_version_column
963 963 get :index, :set_filter => 1, :c => %w(fixed_version)
964 964 assert_select 'table.issues td.fixed_version' do
965 965 assert_select 'a[href=?]', '/versions/2', :text => 'eCookbook - 1.0'
966 966 end
967 967 end
968 968
969 969 def test_index_with_relations_column
970 970 IssueRelation.delete_all
971 971 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(7))
972 972 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(8), :issue_to => Issue.find(1))
973 973 IssueRelation.create!(:relation_type => "blocks", :issue_from => Issue.find(1), :issue_to => Issue.find(11))
974 974 IssueRelation.create!(:relation_type => "blocks", :issue_from => Issue.find(12), :issue_to => Issue.find(2))
975 975
976 976 get :index, :set_filter => 1, :c => %w(subject relations)
977 977 assert_response :success
978 978 assert_select "tr#issue-1 td.relations" do
979 979 assert_select "span", 3
980 980 assert_select "span", :text => "Related to #7"
981 981 assert_select "span", :text => "Related to #8"
982 982 assert_select "span", :text => "Blocks #11"
983 983 end
984 984 assert_select "tr#issue-2 td.relations" do
985 985 assert_select "span", 1
986 986 assert_select "span", :text => "Blocked by #12"
987 987 end
988 988 assert_select "tr#issue-3 td.relations" do
989 989 assert_select "span", 0
990 990 end
991 991
992 992 get :index, :set_filter => 1, :c => %w(relations), :format => 'csv'
993 993 assert_response :success
994 994 assert_equal 'text/csv; header=present', response.content_type
995 995 lines = response.body.chomp.split("\n")
996 996 assert_include '1,"Related to #7, Related to #8, Blocks #11"', lines
997 997 assert_include '2,Blocked by #12', lines
998 998 assert_include '3,""', lines
999 999
1000 1000 get :index, :set_filter => 1, :c => %w(subject relations), :format => 'pdf'
1001 1001 assert_response :success
1002 1002 assert_equal 'application/pdf', response.content_type
1003 1003 end
1004 1004
1005 1005 def test_index_with_description_column
1006 1006 get :index, :set_filter => 1, :c => %w(subject description)
1007 1007
1008 1008 assert_select 'table.issues thead th', 3 # columns: chekbox + id + subject
1009 1009 assert_select 'td.description[colspan="3"]', :text => 'Unable to print recipes'
1010 1010
1011 1011 get :index, :set_filter => 1, :c => %w(subject description), :format => 'pdf'
1012 1012 assert_response :success
1013 1013 assert_equal 'application/pdf', response.content_type
1014 1014 end
1015 1015
1016 1016 def test_index_with_parent_column
1017 1017 Issue.delete_all
1018 1018 parent = Issue.generate!
1019 1019 child = Issue.generate!(:parent_issue_id => parent.id)
1020 1020
1021 1021 get :index, :c => %w(parent)
1022 1022
1023 1023 assert_select 'td.parent', :text => "#{parent.tracker} ##{parent.id}"
1024 1024 assert_select 'td.parent a[title=?]', parent.subject
1025 1025 end
1026 1026
1027 1027 def test_index_with_estimated_hours_total
1028 1028 Issue.delete_all
1029 1029 Issue.generate!(:estimated_hours => 5.5)
1030 1030 Issue.generate!(:estimated_hours => 1.1)
1031 1031
1032 1032 get :index, :t => %w(estimated_hours)
1033 1033 assert_response :success
1034 1034 assert_select '.query-totals'
1035 1035 assert_select '.total-for-estimated-hours span.value', :text => '6.60'
1036 1036 assert_select 'input[type=checkbox][name=?][value=estimated_hours][checked=checked]', 't[]'
1037 1037 end
1038 1038
1039 1039 def test_index_with_grouped_query_and_estimated_hours_total
1040 1040 Issue.delete_all
1041 1041 Issue.generate!(:estimated_hours => 5.5, :category_id => 1)
1042 1042 Issue.generate!(:estimated_hours => 2.3, :category_id => 1)
1043 1043 Issue.generate!(:estimated_hours => 1.1, :category_id => 2)
1044 1044 Issue.generate!(:estimated_hours => 4.6)
1045 1045
1046 1046 get :index, :t => %w(estimated_hours), :group_by => 'category'
1047 1047 assert_response :success
1048 1048 assert_select '.query-totals'
1049 1049 assert_select '.query-totals .total-for-estimated-hours span.value', :text => '13.50'
1050 1050 assert_select 'tr.group', :text => /Printing/ do
1051 1051 assert_select '.total-for-estimated-hours span.value', :text => '7.80'
1052 1052 end
1053 1053 assert_select 'tr.group', :text => /Recipes/ do
1054 1054 assert_select '.total-for-estimated-hours span.value', :text => '1.10'
1055 1055 end
1056 1056 assert_select 'tr.group', :text => /blank/ do
1057 1057 assert_select '.total-for-estimated-hours span.value', :text => '4.60'
1058 1058 end
1059 1059 end
1060 1060
1061 1061 def test_index_with_int_custom_field_total
1062 1062 field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
1063 1063 CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2')
1064 1064 CustomValue.create!(:customized => Issue.find(2), :custom_field => field, :value => '7')
1065 1065
1066 1066 get :index, :t => ["cf_#{field.id}"]
1067 1067 assert_response :success
1068 1068 assert_select '.query-totals'
1069 1069 assert_select ".total-for-cf-#{field.id} span.value", :text => '9'
1070 1070 end
1071 1071
1072 1072 def test_index_totals_should_default_to_settings
1073 1073 with_settings :issue_list_default_totals => ['estimated_hours'] do
1074 1074 get :index
1075 1075 assert_response :success
1076 1076 assert_select '.total-for-estimated-hours span.value'
1077 1077 assert_select '.query-totals>span', 1
1078 1078 end
1079 1079 end
1080 1080
1081 1081 def test_index_send_html_if_query_is_invalid
1082 1082 get :index, :f => ['start_date'], :op => {:start_date => '='}
1083 1083 assert_equal 'text/html', @response.content_type
1084 1084 assert_template 'index'
1085 1085 end
1086 1086
1087 1087 def test_index_send_nothing_if_query_is_invalid
1088 1088 get :index, :f => ['start_date'], :op => {:start_date => '='}, :format => 'csv'
1089 1089 assert_equal 'text/csv', @response.content_type
1090 1090 assert @response.body.blank?
1091 1091 end
1092 1092
1093 1093 def test_index_should_include_new_issue_link
1094 1094 @request.session[:user_id] = 2
1095 1095 get :index, :project_id => 1
1096 1096 assert_select '#content a.new-issue[href="/projects/ecookbook/issues/new"]', :text => 'New issue'
1097 1097 end
1098 1098
1099 1099 def test_index_should_not_include_new_issue_link_for_project_without_trackers
1100 1100 Project.find(1).trackers.clear
1101 1101
1102 1102 @request.session[:user_id] = 2
1103 1103 get :index, :project_id => 1
1104 1104 assert_select '#content a.new-issue', 0
1105 1105 end
1106 1106
1107 1107 def test_index_should_not_include_new_issue_link_for_users_with_copy_issues_permission_only
1108 1108 role = Role.find(1)
1109 1109 role.remove_permission! :add_issues
1110 1110 role.add_permission! :copy_issues
1111 1111
1112 1112 @request.session[:user_id] = 2
1113 1113 get :index, :project_id => 1
1114 1114 assert_select '#content a.new-issue', 0
1115 1115 end
1116 1116
1117 1117 def test_index_without_project_should_include_new_issue_link
1118 1118 @request.session[:user_id] = 2
1119 1119 get :index
1120 1120 assert_select '#content a.new-issue[href="/issues/new"]', :text => 'New issue'
1121 1121 end
1122 1122
1123 1123 def test_index_should_not_include_new_issue_tab_when_disabled
1124 1124 with_settings :new_item_menu_tab => '0' do
1125 1125 @request.session[:user_id] = 2
1126 1126 get :index, :project_id => 1
1127 1127 assert_select '#main-menu a.new-issue', 0
1128 1128 end
1129 1129 end
1130 1130
1131 1131 def test_index_should_include_new_issue_tab_when_enabled
1132 1132 with_settings :new_item_menu_tab => '1' do
1133 1133 @request.session[:user_id] = 2
1134 1134 get :index, :project_id => 1
1135 1135 assert_select '#main-menu a.new-issue[href="/projects/ecookbook/issues/new"]', :text => 'New issue'
1136 1136 end
1137 1137 end
1138 1138
1139 1139 def test_new_should_have_new_issue_tab_as_current_menu_item
1140 1140 with_settings :new_item_menu_tab => '1' do
1141 1141 @request.session[:user_id] = 2
1142 1142 get :new, :project_id => 1
1143 1143 assert_select '#main-menu a.new-issue.selected'
1144 1144 end
1145 1145 end
1146 1146
1147 1147 def test_index_should_not_include_new_issue_tab_for_project_without_trackers
1148 1148 with_settings :new_item_menu_tab => '1' do
1149 1149 Project.find(1).trackers.clear
1150 1150
1151 1151 @request.session[:user_id] = 2
1152 1152 get :index, :project_id => 1
1153 1153 assert_select '#main-menu a.new-issue', 0
1154 1154 end
1155 1155 end
1156 1156
1157 1157 def test_index_should_not_include_new_issue_tab_for_users_with_copy_issues_permission_only
1158 1158 with_settings :new_item_menu_tab => '1' do
1159 1159 role = Role.find(1)
1160 1160 role.remove_permission! :add_issues
1161 1161 role.add_permission! :copy_issues
1162 1162
1163 1163 @request.session[:user_id] = 2
1164 1164 get :index, :project_id => 1
1165 1165 assert_select '#main-menu a.new-issue', 0
1166 1166 end
1167 1167 end
1168 1168
1169 1169 def test_show_by_anonymous
1170 1170 get :show, :id => 1
1171 1171 assert_response :success
1172 1172 assert_template 'show'
1173 1173 assert_equal Issue.find(1), assigns(:issue)
1174 1174 assert_select 'div.issue div.description', :text => /Unable to print recipes/
1175 1175 # anonymous role is allowed to add a note
1176 1176 assert_select 'form#issue-form' do
1177 1177 assert_select 'fieldset' do
1178 1178 assert_select 'legend', :text => 'Notes'
1179 1179 assert_select 'textarea[name=?]', 'issue[notes]'
1180 1180 end
1181 1181 end
1182 1182 assert_select 'title', :text => "Bug #1: Cannot print recipes - eCookbook - Redmine"
1183 1183 end
1184 1184
1185 1185 def test_show_by_manager
1186 1186 @request.session[:user_id] = 2
1187 1187 get :show, :id => 1
1188 1188 assert_response :success
1189 1189 assert_select 'a', :text => /Quote/
1190 1190 assert_select 'form#issue-form' do
1191 1191 assert_select 'fieldset' do
1192 1192 assert_select 'legend', :text => 'Change properties'
1193 1193 assert_select 'input[name=?]', 'issue[subject]'
1194 1194 end
1195 1195 assert_select 'fieldset' do
1196 1196 assert_select 'legend', :text => 'Log time'
1197 1197 assert_select 'input[name=?]', 'time_entry[hours]'
1198 1198 end
1199 1199 assert_select 'fieldset' do
1200 1200 assert_select 'legend', :text => 'Notes'
1201 1201 assert_select 'textarea[name=?]', 'issue[notes]'
1202 1202 end
1203 1203 end
1204 1204 end
1205 1205
1206 1206 def test_show_should_display_update_form
1207 1207 @request.session[:user_id] = 2
1208 1208 get :show, :id => 1
1209 1209 assert_response :success
1210 1210
1211 1211 assert_select 'form#issue-form' do
1212 1212 assert_select 'input[name=?]', 'issue[is_private]'
1213 1213 assert_select 'select[name=?]', 'issue[project_id]'
1214 1214 assert_select 'select[name=?]', 'issue[tracker_id]'
1215 1215 assert_select 'input[name=?]', 'issue[subject]'
1216 1216 assert_select 'textarea[name=?]', 'issue[description]'
1217 1217 assert_select 'select[name=?]', 'issue[status_id]'
1218 1218 assert_select 'select[name=?]', 'issue[priority_id]'
1219 1219 assert_select 'select[name=?]', 'issue[assigned_to_id]'
1220 1220 assert_select 'select[name=?]', 'issue[category_id]'
1221 1221 assert_select 'select[name=?]', 'issue[fixed_version_id]'
1222 1222 assert_select 'input[name=?]', 'issue[parent_issue_id]'
1223 1223 assert_select 'input[name=?]', 'issue[start_date]'
1224 1224 assert_select 'input[name=?]', 'issue[due_date]'
1225 1225 assert_select 'select[name=?]', 'issue[done_ratio]'
1226 1226 assert_select 'input[name=?]', 'issue[custom_field_values][2]'
1227 1227 assert_select 'input[name=?]', 'issue[watcher_user_ids][]', 0
1228 1228 assert_select 'textarea[name=?]', 'issue[notes]'
1229 1229 end
1230 1230 end
1231 1231
1232 1232 def test_show_should_display_update_form_with_minimal_permissions
1233 1233 Role.find(1).update_attribute :permissions, [:view_issues, :add_issue_notes]
1234 1234 WorkflowTransition.delete_all :role_id => 1
1235 1235
1236 1236 @request.session[:user_id] = 2
1237 1237 get :show, :id => 1
1238 1238 assert_response :success
1239 1239
1240 1240 assert_select 'form#issue-form' do
1241 1241 assert_select 'input[name=?]', 'issue[is_private]', 0
1242 1242 assert_select 'select[name=?]', 'issue[project_id]', 0
1243 1243 assert_select 'select[name=?]', 'issue[tracker_id]', 0
1244 1244 assert_select 'input[name=?]', 'issue[subject]', 0
1245 1245 assert_select 'textarea[name=?]', 'issue[description]', 0
1246 1246 assert_select 'select[name=?]', 'issue[status_id]', 0
1247 1247 assert_select 'select[name=?]', 'issue[priority_id]', 0
1248 1248 assert_select 'select[name=?]', 'issue[assigned_to_id]', 0
1249 1249 assert_select 'select[name=?]', 'issue[category_id]', 0
1250 1250 assert_select 'select[name=?]', 'issue[fixed_version_id]', 0
1251 1251 assert_select 'input[name=?]', 'issue[parent_issue_id]', 0
1252 1252 assert_select 'input[name=?]', 'issue[start_date]', 0
1253 1253 assert_select 'input[name=?]', 'issue[due_date]', 0
1254 1254 assert_select 'select[name=?]', 'issue[done_ratio]', 0
1255 1255 assert_select 'input[name=?]', 'issue[custom_field_values][2]', 0
1256 1256 assert_select 'input[name=?]', 'issue[watcher_user_ids][]', 0
1257 1257 assert_select 'textarea[name=?]', 'issue[notes]'
1258 1258 end
1259 1259 end
1260 1260
1261 1261 def test_show_should_not_display_update_form_without_permissions
1262 1262 Role.find(1).update_attribute :permissions, [:view_issues]
1263 1263
1264 1264 @request.session[:user_id] = 2
1265 1265 get :show, :id => 1
1266 1266 assert_response :success
1267 1267
1268 1268 assert_select 'form#issue-form', 0
1269 1269 end
1270 1270
1271 1271 def test_update_form_should_not_display_inactive_enumerations
1272 1272 assert !IssuePriority.find(15).active?
1273 1273
1274 1274 @request.session[:user_id] = 2
1275 1275 get :show, :id => 1
1276 1276 assert_response :success
1277 1277
1278 1278 assert_select 'form#issue-form' do
1279 1279 assert_select 'select[name=?]', 'issue[priority_id]' do
1280 1280 assert_select 'option[value="4"]'
1281 1281 assert_select 'option[value="15"]', 0
1282 1282 end
1283 1283 end
1284 1284 end
1285 1285
1286 1286 def test_update_form_should_allow_attachment_upload
1287 1287 @request.session[:user_id] = 2
1288 1288 get :show, :id => 1
1289 1289
1290 1290 assert_select 'form#issue-form[method=post][enctype="multipart/form-data"]' do
1291 1291 assert_select 'input[type=file][name=?]', 'attachments[dummy][file]'
1292 1292 end
1293 1293 end
1294 1294
1295 1295 def test_show_should_deny_anonymous_access_without_permission
1296 1296 Role.anonymous.remove_permission!(:view_issues)
1297 1297 get :show, :id => 1
1298 1298 assert_response :redirect
1299 1299 end
1300 1300
1301 1301 def test_show_should_deny_anonymous_access_to_private_issue
1302 1302 Issue.where(:id => 1).update_all(["is_private = ?", true])
1303 1303 get :show, :id => 1
1304 1304 assert_response :redirect
1305 1305 end
1306 1306
1307 1307 def test_show_should_deny_non_member_access_without_permission
1308 1308 Role.non_member.remove_permission!(:view_issues)
1309 1309 @request.session[:user_id] = 9
1310 1310 get :show, :id => 1
1311 1311 assert_response 403
1312 1312 end
1313 1313
1314 1314 def test_show_should_deny_non_member_access_to_private_issue
1315 1315 Issue.where(:id => 1).update_all(["is_private = ?", true])
1316 1316 @request.session[:user_id] = 9
1317 1317 get :show, :id => 1
1318 1318 assert_response 403
1319 1319 end
1320 1320
1321 1321 def test_show_should_deny_member_access_without_permission
1322 1322 Role.find(1).remove_permission!(:view_issues)
1323 1323 @request.session[:user_id] = 2
1324 1324 get :show, :id => 1
1325 1325 assert_response 403
1326 1326 end
1327 1327
1328 1328 def test_show_should_deny_member_access_to_private_issue_without_permission
1329 1329 Issue.where(:id => 1).update_all(["is_private = ?", true])
1330 1330 @request.session[:user_id] = 3
1331 1331 get :show, :id => 1
1332 1332 assert_response 403
1333 1333 end
1334 1334
1335 1335 def test_show_should_allow_author_access_to_private_issue
1336 1336 Issue.where(:id => 1).update_all(["is_private = ?, author_id = 3", true])
1337 1337 @request.session[:user_id] = 3
1338 1338 get :show, :id => 1
1339 1339 assert_response :success
1340 1340 end
1341 1341
1342 1342 def test_show_should_allow_assignee_access_to_private_issue
1343 1343 Issue.where(:id => 1).update_all(["is_private = ?, assigned_to_id = 3", true])
1344 1344 @request.session[:user_id] = 3
1345 1345 get :show, :id => 1
1346 1346 assert_response :success
1347 1347 end
1348 1348
1349 1349 def test_show_should_allow_member_access_to_private_issue_with_permission
1350 1350 Issue.where(:id => 1).update_all(["is_private = ?", true])
1351 1351 User.find(3).roles_for_project(Project.find(1)).first.update_attribute :issues_visibility, 'all'
1352 1352 @request.session[:user_id] = 3
1353 1353 get :show, :id => 1
1354 1354 assert_response :success
1355 1355 end
1356 1356
1357 1357 def test_show_should_not_disclose_relations_to_invisible_issues
1358 1358 Setting.cross_project_issue_relations = '1'
1359 1359 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
1360 1360 # Relation to a private project issue
1361 1361 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
1362 1362
1363 1363 get :show, :id => 1
1364 1364 assert_response :success
1365 1365
1366 1366 assert_select 'div#relations' do
1367 1367 assert_select 'a', :text => /#2$/
1368 1368 assert_select 'a', :text => /#4$/, :count => 0
1369 1369 end
1370 1370 end
1371 1371
1372 1372 def test_show_should_list_subtasks
1373 1373 Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :parent_issue_id => 1, :subject => 'Child Issue')
1374 1374
1375 1375 get :show, :id => 1
1376 1376 assert_response :success
1377 1377
1378 1378 assert_select 'div#issue_tree' do
1379 1379 assert_select 'td.subject', :text => /Child Issue/
1380 1380 end
1381 1381 end
1382 1382
1383 1383 def test_show_should_list_parents
1384 1384 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :parent_issue_id => 1, :subject => 'Child Issue')
1385 1385
1386 1386 get :show, :id => issue.id
1387 1387 assert_response :success
1388 1388
1389 1389 assert_select 'div.subject' do
1390 1390 assert_select 'h3', 'Child Issue'
1391 1391 assert_select 'a[href="/issues/1"]'
1392 1392 end
1393 1393 end
1394 1394
1395 1395 def test_show_should_not_display_prev_next_links_without_query_in_session
1396 1396 get :show, :id => 1
1397 1397 assert_response :success
1398 1398 assert_nil assigns(:prev_issue_id)
1399 1399 assert_nil assigns(:next_issue_id)
1400 1400
1401 1401 assert_select 'div.next-prev-links', 0
1402 1402 end
1403 1403
1404 1404 def test_show_should_display_prev_next_links_with_query_in_session
1405 1405 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => nil}
1406 1406 @request.session['issues_index_sort'] = 'id'
1407 1407
1408 1408 with_settings :display_subprojects_issues => '0' do
1409 1409 get :show, :id => 3
1410 1410 end
1411 1411
1412 1412 assert_response :success
1413 1413 # Previous and next issues for all projects
1414 1414 assert_equal 2, assigns(:prev_issue_id)
1415 1415 assert_equal 5, assigns(:next_issue_id)
1416 1416
1417 1417 count = Issue.open.visible.count
1418 1418
1419 1419 assert_select 'div.next-prev-links' do
1420 1420 assert_select 'a[href="/issues/2"]', :text => /Previous/
1421 1421 assert_select 'a[href="/issues/5"]', :text => /Next/
1422 1422 assert_select 'span.position', :text => "3 of #{count}"
1423 1423 end
1424 1424 end
1425 1425
1426 1426 def test_show_should_display_prev_next_links_with_saved_query_in_session
1427 1427 query = IssueQuery.create!(:name => 'test', :visibility => IssueQuery::VISIBILITY_PUBLIC, :user_id => 1,
1428 1428 :filters => {'status_id' => {:values => ['5'], :operator => '='}},
1429 1429 :sort_criteria => [['id', 'asc']])
1430 1430 @request.session[:query] = {:id => query.id, :project_id => nil}
1431 1431
1432 1432 get :show, :id => 11
1433 1433
1434 1434 assert_response :success
1435 1435 assert_equal query, assigns(:query)
1436 1436 # Previous and next issues for all projects
1437 1437 assert_equal 8, assigns(:prev_issue_id)
1438 1438 assert_equal 12, assigns(:next_issue_id)
1439 1439
1440 1440 assert_select 'div.next-prev-links' do
1441 1441 assert_select 'a[href="/issues/8"]', :text => /Previous/
1442 1442 assert_select 'a[href="/issues/12"]', :text => /Next/
1443 1443 end
1444 1444 end
1445 1445
1446 1446 def test_show_should_display_prev_next_links_with_query_and_sort_on_association
1447 1447 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => nil}
1448 1448
1449 1449 %w(project tracker status priority author assigned_to category fixed_version).each do |assoc_sort|
1450 1450 @request.session['issues_index_sort'] = assoc_sort
1451 1451
1452 1452 get :show, :id => 3
1453 1453 assert_response :success, "Wrong response status for #{assoc_sort} sort"
1454 1454
1455 1455 assert_select 'div.next-prev-links' do
1456 1456 assert_select 'a', :text => /(Previous|Next)/
1457 1457 end
1458 1458 end
1459 1459 end
1460 1460
1461 1461 def test_show_should_display_prev_next_links_with_project_query_in_session
1462 1462 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => 1}
1463 1463 @request.session['issues_index_sort'] = 'id'
1464 1464
1465 1465 with_settings :display_subprojects_issues => '0' do
1466 1466 get :show, :id => 3
1467 1467 end
1468 1468
1469 1469 assert_response :success
1470 1470 # Previous and next issues inside project
1471 1471 assert_equal 2, assigns(:prev_issue_id)
1472 1472 assert_equal 7, assigns(:next_issue_id)
1473 1473
1474 1474 assert_select 'div.next-prev-links' do
1475 1475 assert_select 'a[href="/issues/2"]', :text => /Previous/
1476 1476 assert_select 'a[href="/issues/7"]', :text => /Next/
1477 1477 end
1478 1478 end
1479 1479
1480 1480 def test_show_should_not_display_prev_link_for_first_issue
1481 1481 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => 1}
1482 1482 @request.session['issues_index_sort'] = 'id'
1483 1483
1484 1484 with_settings :display_subprojects_issues => '0' do
1485 1485 get :show, :id => 1
1486 1486 end
1487 1487
1488 1488 assert_response :success
1489 1489 assert_nil assigns(:prev_issue_id)
1490 1490 assert_equal 2, assigns(:next_issue_id)
1491 1491
1492 1492 assert_select 'div.next-prev-links' do
1493 1493 assert_select 'a', :text => /Previous/, :count => 0
1494 1494 assert_select 'a[href="/issues/2"]', :text => /Next/
1495 1495 end
1496 1496 end
1497 1497
1498 1498 def test_show_should_not_display_prev_next_links_for_issue_not_in_query_results
1499 1499 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'c'}}, :project_id => 1}
1500 1500 @request.session['issues_index_sort'] = 'id'
1501 1501
1502 1502 get :show, :id => 1
1503 1503
1504 1504 assert_response :success
1505 1505 assert_nil assigns(:prev_issue_id)
1506 1506 assert_nil assigns(:next_issue_id)
1507 1507
1508 1508 assert_select 'a', :text => /Previous/, :count => 0
1509 1509 assert_select 'a', :text => /Next/, :count => 0
1510 1510 end
1511 1511
1512 1512 def test_show_show_should_display_prev_next_links_with_query_sort_by_user_custom_field
1513 1513 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'user')
1514 1514 CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '2')
1515 1515 CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => '3')
1516 1516 CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => '3')
1517 1517 CustomValue.create!(:custom_field => cf, :customized => Issue.find(5), :value => '')
1518 1518
1519 1519 query = IssueQuery.create!(:name => 'test', :visibility => IssueQuery::VISIBILITY_PUBLIC, :user_id => 1, :filters => {},
1520 1520 :sort_criteria => [["cf_#{cf.id}", 'asc'], ['id', 'asc']])
1521 1521 @request.session[:query] = {:id => query.id, :project_id => nil}
1522 1522
1523 1523 get :show, :id => 3
1524 1524 assert_response :success
1525 1525
1526 1526 assert_equal 2, assigns(:prev_issue_id)
1527 1527 assert_equal 1, assigns(:next_issue_id)
1528 1528
1529 1529 assert_select 'div.next-prev-links' do
1530 1530 assert_select 'a[href="/issues/2"]', :text => /Previous/
1531 1531 assert_select 'a[href="/issues/1"]', :text => /Next/
1532 1532 end
1533 1533 end
1534 1534
1535 1535 def test_show_should_display_prev_next_links_when_request_has_previous_and_next_issue_ids_params
1536 1536 get :show, :id => 1, :prev_issue_id => 1, :next_issue_id => 3, :issue_position => 2, :issue_count => 4
1537 1537 assert_response :success
1538 1538
1539 1539 assert_select 'div.next-prev-links' do
1540 1540 assert_select 'a[href="/issues/1"]', :text => /Previous/
1541 1541 assert_select 'a[href="/issues/3"]', :text => /Next/
1542 1542 assert_select 'span.position', :text => "2 of 4"
1543 1543 end
1544 1544 end
1545 1545
1546 1546 def test_show_should_display_category_field_if_categories_are_defined
1547 1547 Issue.update_all :category_id => nil
1548 1548
1549 1549 get :show, :id => 1
1550 1550 assert_response :success
1551 1551 assert_select '.attributes .category'
1552 1552 end
1553 1553
1554 1554 def test_show_should_not_display_category_field_if_no_categories_are_defined
1555 1555 Project.find(1).issue_categories.delete_all
1556 1556
1557 1557 get :show, :id => 1
1558 1558 assert_response :success
1559 1559 assert_select 'table.attributes .category', 0
1560 1560 end
1561 1561
1562 1562 def test_show_should_display_link_to_the_assignee
1563 1563 get :show, :id => 2
1564 1564 assert_response :success
1565 1565 assert_select '.assigned-to' do
1566 1566 assert_select 'a[href="/users/3"]'
1567 1567 end
1568 1568 end
1569 1569
1570 1570 def test_show_should_display_visible_changesets_from_other_projects
1571 1571 project = Project.find(2)
1572 1572 issue = project.issues.first
1573 1573 issue.changeset_ids = [102]
1574 1574 issue.save!
1575 1575 # changesets from other projects should be displayed even if repository
1576 1576 # is disabled on issue's project
1577 1577 project.disable_module! :repository
1578 1578
1579 1579 @request.session[:user_id] = 2
1580 1580 get :show, :id => issue.id
1581 1581
1582 1582 assert_select 'a[href=?]', '/projects/ecookbook/repository/revisions/3'
1583 1583 end
1584 1584
1585 1585 def test_show_should_display_watchers
1586 1586 @request.session[:user_id] = 2
1587 1587 Issue.find(1).add_watcher User.find(2)
1588 1588
1589 1589 get :show, :id => 1
1590 1590 assert_select 'div#watchers ul' do
1591 1591 assert_select 'li' do
1592 1592 assert_select 'a[href="/users/2"]'
1593 1593 assert_select 'a[class*=delete]'
1594 1594 end
1595 1595 end
1596 1596 end
1597 1597
1598 1598 def test_show_should_display_watchers_with_gravatars
1599 1599 @request.session[:user_id] = 2
1600 1600 Issue.find(1).add_watcher User.find(2)
1601 1601
1602 1602 with_settings :gravatar_enabled => '1' do
1603 1603 get :show, :id => 1
1604 1604 end
1605 1605
1606 1606 assert_select 'div#watchers ul' do
1607 1607 assert_select 'li' do
1608 1608 assert_select 'img.gravatar'
1609 1609 assert_select 'a[href="/users/2"]'
1610 1610 assert_select 'a[class*=delete]'
1611 1611 end
1612 1612 end
1613 1613 end
1614 1614
1615 1615 def test_show_with_thumbnails_enabled_should_display_thumbnails
1616 1616 @request.session[:user_id] = 2
1617 1617
1618 1618 with_settings :thumbnails_enabled => '1' do
1619 1619 get :show, :id => 14
1620 1620 assert_response :success
1621 1621 end
1622 1622
1623 1623 assert_select 'div.thumbnails' do
1624 1624 assert_select 'a[href="/attachments/16/testfile.png"]' do
1625 1625 assert_select 'img[src="/attachments/thumbnail/16"]'
1626 1626 end
1627 1627 end
1628 1628 end
1629 1629
1630 1630 def test_show_with_thumbnails_disabled_should_not_display_thumbnails
1631 1631 @request.session[:user_id] = 2
1632 1632
1633 1633 with_settings :thumbnails_enabled => '0' do
1634 1634 get :show, :id => 14
1635 1635 assert_response :success
1636 1636 end
1637 1637
1638 1638 assert_select 'div.thumbnails', 0
1639 1639 end
1640 1640
1641 1641 def test_show_with_multi_custom_field
1642 1642 field = CustomField.find(1)
1643 1643 field.update_attribute :multiple, true
1644 1644 issue = Issue.find(1)
1645 1645 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
1646 1646 issue.save!
1647 1647
1648 1648 get :show, :id => 1
1649 1649 assert_response :success
1650 1650
1651 1651 assert_select ".cf_1 .value", :text => 'MySQL, Oracle'
1652 1652 end
1653 1653
1654 1654 def test_show_with_multi_user_custom_field
1655 1655 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
1656 1656 :tracker_ids => [1], :is_for_all => true)
1657 1657 issue = Issue.find(1)
1658 1658 issue.custom_field_values = {field.id => ['2', '3']}
1659 1659 issue.save!
1660 1660
1661 1661 get :show, :id => 1
1662 1662 assert_response :success
1663 1663
1664 1664 assert_select ".cf_#{field.id} .value", :text => 'Dave Lopper, John Smith' do
1665 1665 assert_select 'a', :text => 'Dave Lopper'
1666 1666 assert_select 'a', :text => 'John Smith'
1667 1667 end
1668 1668 end
1669 1669
1670 1670 def test_show_should_display_private_notes_with_permission_only
1671 1671 journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Privates notes', :private_notes => true, :user_id => 1)
1672 1672 @request.session[:user_id] = 2
1673 1673
1674 1674 get :show, :id => 2
1675 1675 assert_response :success
1676 1676 assert_include journal, assigns(:journals)
1677 1677
1678 1678 Role.find(1).remove_permission! :view_private_notes
1679 1679 get :show, :id => 2
1680 1680 assert_response :success
1681 1681 assert_not_include journal, assigns(:journals)
1682 1682 end
1683 1683
1684 1684 def test_show_atom
1685 1685 get :show, :id => 2, :format => 'atom'
1686 1686 assert_response :success
1687 1687 assert_template 'journals/index'
1688 1688 # Inline image
1689 1689 assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10'))
1690 1690 end
1691 1691
1692 1692 def test_show_export_to_pdf
1693 1693 issue = Issue.find(3)
1694 1694 assert issue.relations.select{|r| r.other_issue(issue).visible?}.present?
1695 1695 get :show, :id => 3, :format => 'pdf'
1696 1696 assert_response :success
1697 1697 assert_equal 'application/pdf', @response.content_type
1698 1698 assert @response.body.starts_with?('%PDF')
1699 1699 assert_not_nil assigns(:issue)
1700 1700 end
1701 1701
1702 1702 def test_export_to_pdf_with_utf8_u_fffd
1703 1703 # U+FFFD
1704 1704 s = "\xef\xbf\xbd"
1705 1705 s.force_encoding('UTF-8') if s.respond_to?(:force_encoding)
1706 1706 issue = Issue.generate!(:subject => s)
1707 1707 ["en", "zh", "zh-TW", "ja", "ko"].each do |lang|
1708 1708 with_settings :default_language => lang do
1709 1709 get :show, :id => issue.id, :format => 'pdf'
1710 1710 assert_response :success
1711 1711 assert_equal 'application/pdf', @response.content_type
1712 1712 assert @response.body.starts_with?('%PDF')
1713 1713 assert_not_nil assigns(:issue)
1714 1714 end
1715 1715 end
1716 1716 end
1717 1717
1718 1718 def test_show_export_to_pdf_with_ancestors
1719 1719 issue = Issue.generate!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => 1)
1720 1720
1721 1721 get :show, :id => issue.id, :format => 'pdf'
1722 1722 assert_response :success
1723 1723 assert_equal 'application/pdf', @response.content_type
1724 1724 assert @response.body.starts_with?('%PDF')
1725 1725 end
1726 1726
1727 1727 def test_show_export_to_pdf_with_descendants
1728 1728 c1 = Issue.generate!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => 1)
1729 1729 c2 = Issue.generate!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => 1)
1730 1730 c3 = Issue.generate!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => c1.id)
1731 1731
1732 1732 get :show, :id => 1, :format => 'pdf'
1733 1733 assert_response :success
1734 1734 assert_equal 'application/pdf', @response.content_type
1735 1735 assert @response.body.starts_with?('%PDF')
1736 1736 end
1737 1737
1738 1738 def test_show_export_to_pdf_with_journals
1739 1739 get :show, :id => 1, :format => 'pdf'
1740 1740 assert_response :success
1741 1741 assert_equal 'application/pdf', @response.content_type
1742 1742 assert @response.body.starts_with?('%PDF')
1743 1743 end
1744 1744
1745 1745 def test_show_export_to_pdf_with_changesets
1746 1746 [[100], [100, 101], [100, 101, 102]].each do |cs|
1747 1747 issue1 = Issue.find(3)
1748 1748 issue1.changesets = Changeset.find(cs)
1749 1749 issue1.save!
1750 1750 issue = Issue.find(3)
1751 1751 assert_equal issue.changesets.count, cs.size
1752 1752 get :show, :id => 3, :format => 'pdf'
1753 1753 assert_response :success
1754 1754 assert_equal 'application/pdf', @response.content_type
1755 1755 assert @response.body.starts_with?('%PDF')
1756 1756 end
1757 1757 end
1758 1758
1759 1759 def test_show_invalid_should_respond_with_404
1760 1760 get :show, :id => 999
1761 1761 assert_response 404
1762 1762 end
1763 1763
1764 1764 def test_get_new
1765 1765 @request.session[:user_id] = 2
1766 1766 get :new, :project_id => 1, :tracker_id => 1
1767 1767 assert_response :success
1768 1768 assert_template 'new'
1769 1769
1770 1770 assert_select 'form#issue-form[action=?]', '/projects/ecookbook/issues'
1771 1771 assert_select 'form#issue-form' do
1772 1772 assert_select 'input[name=?]', 'issue[is_private]'
1773 1773 assert_select 'select[name=?]', 'issue[project_id]', 0
1774 1774 assert_select 'select[name=?]', 'issue[tracker_id]'
1775 1775 assert_select 'input[name=?]', 'issue[subject]'
1776 1776 assert_select 'textarea[name=?]', 'issue[description]'
1777 1777 assert_select 'select[name=?]', 'issue[status_id]'
1778 1778 assert_select 'select[name=?]', 'issue[priority_id]'
1779 1779 assert_select 'select[name=?]', 'issue[assigned_to_id]'
1780 1780 assert_select 'select[name=?]', 'issue[category_id]'
1781 1781 assert_select 'select[name=?]', 'issue[fixed_version_id]'
1782 1782 assert_select 'input[name=?]', 'issue[parent_issue_id]'
1783 1783 assert_select 'input[name=?]', 'issue[start_date]'
1784 1784 assert_select 'input[name=?]', 'issue[due_date]'
1785 1785 assert_select 'select[name=?]', 'issue[done_ratio]'
1786 1786 assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', 'Default string'
1787 1787 assert_select 'input[name=?]', 'issue[watcher_user_ids][]'
1788 1788 end
1789 1789
1790 1790 # Be sure we don't display inactive IssuePriorities
1791 1791 assert ! IssuePriority.find(15).active?
1792 1792 assert_select 'select[name=?]', 'issue[priority_id]' do
1793 1793 assert_select 'option[value="15"]', 0
1794 1794 end
1795 1795 end
1796 1796
1797 1797 def test_get_new_with_minimal_permissions
1798 1798 Role.find(1).update_attribute :permissions, [:add_issues]
1799 1799 WorkflowTransition.delete_all :role_id => 1
1800 1800
1801 1801 @request.session[:user_id] = 2
1802 1802 get :new, :project_id => 1, :tracker_id => 1
1803 1803 assert_response :success
1804 1804 assert_template 'new'
1805 1805
1806 1806 assert_select 'form#issue-form' do
1807 1807 assert_select 'input[name=?]', 'issue[is_private]', 0
1808 1808 assert_select 'select[name=?]', 'issue[project_id]', 0
1809 1809 assert_select 'select[name=?]', 'issue[tracker_id]'
1810 1810 assert_select 'input[name=?]', 'issue[subject]'
1811 1811 assert_select 'textarea[name=?]', 'issue[description]'
1812 1812 assert_select 'select[name=?]', 'issue[status_id]'
1813 1813 assert_select 'select[name=?]', 'issue[priority_id]'
1814 1814 assert_select 'select[name=?]', 'issue[assigned_to_id]'
1815 1815 assert_select 'select[name=?]', 'issue[category_id]'
1816 1816 assert_select 'select[name=?]', 'issue[fixed_version_id]'
1817 1817 assert_select 'input[name=?]', 'issue[parent_issue_id]', 0
1818 1818 assert_select 'input[name=?]', 'issue[start_date]'
1819 1819 assert_select 'input[name=?]', 'issue[due_date]'
1820 1820 assert_select 'select[name=?]', 'issue[done_ratio]'
1821 1821 assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', 'Default string'
1822 1822 assert_select 'input[name=?]', 'issue[watcher_user_ids][]', 0
1823 1823 end
1824 1824 end
1825 1825
1826 1826 def test_new_without_project_id
1827 1827 @request.session[:user_id] = 2
1828 1828 get :new
1829 1829 assert_response :success
1830 1830 assert_template 'new'
1831 1831
1832 1832 assert_select 'form#issue-form[action=?]', '/issues'
1833 1833 assert_select 'form#issue-form' do
1834 1834 assert_select 'select[name=?]', 'issue[project_id]'
1835 1835 end
1836 1836
1837 1837 assert_nil assigns(:project)
1838 1838 assert_not_nil assigns(:issue)
1839 1839 end
1840 1840
1841 1841 def test_new_should_select_default_status
1842 1842 @request.session[:user_id] = 2
1843 1843
1844 1844 get :new, :project_id => 1
1845 1845 assert_response :success
1846 1846 assert_template 'new'
1847 1847 assert_select 'select[name=?]', 'issue[status_id]' do
1848 1848 assert_select 'option[value="1"][selected=selected]'
1849 1849 end
1850 1850 assert_select 'input[name=was_default_status][value="1"]'
1851 1851 end
1852 1852
1853 1853 def test_new_should_propose_allowed_statuses
1854 1854 WorkflowTransition.delete_all
1855 1855 WorkflowTransition.create!(:tracker_id => 1, :role_id => 1, :old_status_id => 0, :new_status_id => 1)
1856 1856 WorkflowTransition.create!(:tracker_id => 1, :role_id => 1, :old_status_id => 0, :new_status_id => 3)
1857 1857 @request.session[:user_id] = 2
1858 1858
1859 1859 get :new, :project_id => 1
1860 1860 assert_response :success
1861 1861 assert_select 'select[name=?]', 'issue[status_id]' do
1862 1862 assert_select 'option[value="1"]'
1863 1863 assert_select 'option[value="3"]'
1864 1864 assert_select 'option', 2
1865 1865 assert_select 'option[value="1"][selected=selected]'
1866 1866 end
1867 1867 end
1868 1868
1869 1869 def test_new_should_propose_allowed_statuses_without_default_status_allowed
1870 1870 WorkflowTransition.delete_all
1871 1871 WorkflowTransition.create!(:tracker_id => 1, :role_id => 1, :old_status_id => 0, :new_status_id => 2)
1872 1872 assert_equal 1, Tracker.find(1).default_status_id
1873 1873 @request.session[:user_id] = 2
1874 1874
1875 1875 get :new, :project_id => 1
1876 1876 assert_response :success
1877 1877 assert_select 'select[name=?]', 'issue[status_id]' do
1878 1878 assert_select 'option[value="2"]'
1879 1879 assert_select 'option', 1
1880 1880 assert_select 'option[value="2"][selected=selected]'
1881 1881 end
1882 1882 end
1883 1883
1884 1884 def test_new_should_propose_allowed_trackers
1885 1885 role = Role.find(1)
1886 1886 role.set_permission_trackers 'add_issues', [1, 3]
1887 1887 role.save!
1888 1888 @request.session[:user_id] = 2
1889 1889
1890 1890 get :new, :project_id => 1
1891 1891 assert_response :success
1892 1892 assert_select 'select[name=?]', 'issue[tracker_id]' do
1893 1893 assert_select 'option', 2
1894 1894 assert_select 'option[value="1"]'
1895 1895 assert_select 'option[value="3"]'
1896 1896 end
1897 1897 end
1898 1898
1899 1899 def test_new_without_allowed_trackers_should_respond_with_403
1900 1900 role = Role.find(1)
1901 1901 role.set_permission_trackers 'add_issues', []
1902 1902 role.save!
1903 1903 @request.session[:user_id] = 2
1904 1904
1905 1905 get :new, :project_id => 1
1906 1906 assert_response 403
1907 1907 end
1908 1908
1909 1909 def test_new_should_preselect_default_version
1910 1910 version = Version.generate!(:project_id => 1)
1911 1911 Project.find(1).update_attribute :default_version_id, version.id
1912 1912 @request.session[:user_id] = 2
1913 1913
1914 1914 get :new, :project_id => 1
1915 1915 assert_response :success
1916 1916 assert_equal version, assigns(:issue).fixed_version
1917 1917 assert_select 'select[name=?]', 'issue[fixed_version_id]' do
1918 1918 assert_select 'option[value=?][selected=selected]', version.id.to_s
1919 1919 end
1920 1920 end
1921 1921
1922 1922 def test_get_new_with_list_custom_field
1923 1923 @request.session[:user_id] = 2
1924 1924 get :new, :project_id => 1, :tracker_id => 1
1925 1925 assert_response :success
1926 1926 assert_template 'new'
1927 1927
1928 1928 assert_select 'select.list_cf[name=?]', 'issue[custom_field_values][1]' do
1929 1929 assert_select 'option', 4
1930 1930 assert_select 'option[value=MySQL]', :text => 'MySQL'
1931 1931 end
1932 1932 end
1933 1933
1934 1934 def test_get_new_with_multi_custom_field
1935 1935 field = IssueCustomField.find(1)
1936 1936 field.update_attribute :multiple, true
1937 1937
1938 1938 @request.session[:user_id] = 2
1939 1939 get :new, :project_id => 1, :tracker_id => 1
1940 1940 assert_response :success
1941 1941 assert_template 'new'
1942 1942
1943 1943 assert_select 'select[name=?][multiple=multiple]', 'issue[custom_field_values][1][]' do
1944 1944 assert_select 'option', 3
1945 1945 assert_select 'option[value=MySQL]', :text => 'MySQL'
1946 1946 end
1947 1947 assert_select 'input[name=?][type=hidden][value=?]', 'issue[custom_field_values][1][]', ''
1948 1948 end
1949 1949
1950 1950 def test_get_new_with_multi_user_custom_field
1951 1951 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
1952 1952 :tracker_ids => [1], :is_for_all => true)
1953 1953
1954 1954 @request.session[:user_id] = 2
1955 1955 get :new, :project_id => 1, :tracker_id => 1
1956 1956 assert_response :success
1957 1957 assert_template 'new'
1958 1958
1959 1959 assert_select 'select[name=?][multiple=multiple]', "issue[custom_field_values][#{field.id}][]" do
1960 1960 assert_select 'option', Project.find(1).users.count
1961 1961 assert_select 'option[value="2"]', :text => 'John Smith'
1962 1962 end
1963 1963 assert_select 'input[name=?][type=hidden][value=?]', "issue[custom_field_values][#{field.id}][]", ''
1964 1964 end
1965 1965
1966 1966 def test_get_new_with_date_custom_field
1967 1967 field = IssueCustomField.create!(:name => 'Date', :field_format => 'date', :tracker_ids => [1], :is_for_all => true)
1968 1968
1969 1969 @request.session[:user_id] = 2
1970 1970 get :new, :project_id => 1, :tracker_id => 1
1971 1971 assert_response :success
1972 1972
1973 1973 assert_select 'input[name=?]', "issue[custom_field_values][#{field.id}]"
1974 1974 end
1975 1975
1976 1976 def test_get_new_with_text_custom_field
1977 1977 field = IssueCustomField.create!(:name => 'Text', :field_format => 'text', :tracker_ids => [1], :is_for_all => true)
1978 1978
1979 1979 @request.session[:user_id] = 2
1980 1980 get :new, :project_id => 1, :tracker_id => 1
1981 1981 assert_response :success
1982 1982
1983 1983 assert_select 'textarea[name=?]', "issue[custom_field_values][#{field.id}]"
1984 1984 end
1985 1985
1986 1986 def test_get_new_without_default_start_date_is_creation_date
1987 1987 with_settings :default_issue_start_date_to_creation_date => 0 do
1988 1988 @request.session[:user_id] = 2
1989 1989 get :new, :project_id => 1, :tracker_id => 1
1990 1990 assert_response :success
1991 1991 assert_template 'new'
1992 1992 assert_select 'input[name=?]', 'issue[start_date]'
1993 1993 assert_select 'input[name=?][value]', 'issue[start_date]', 0
1994 1994 end
1995 1995 end
1996 1996
1997 1997 def test_get_new_with_default_start_date_is_creation_date
1998 1998 with_settings :default_issue_start_date_to_creation_date => 1 do
1999 1999 @request.session[:user_id] = 2
2000 2000 get :new, :project_id => 1, :tracker_id => 1
2001 2001 assert_response :success
2002 2002 assert_template 'new'
2003 2003 assert_select 'input[name=?][value=?]', 'issue[start_date]',
2004 2004 Date.today.to_s
2005 2005 end
2006 2006 end
2007 2007
2008 2008 def test_get_new_form_should_allow_attachment_upload
2009 2009 @request.session[:user_id] = 2
2010 2010 get :new, :project_id => 1, :tracker_id => 1
2011 2011
2012 2012 assert_select 'form[id=issue-form][method=post][enctype="multipart/form-data"]' do
2013 2013 assert_select 'input[name=?][type=file]', 'attachments[dummy][file]'
2014 2014 end
2015 2015 end
2016 2016
2017 2017 def test_get_new_should_prefill_the_form_from_params
2018 2018 @request.session[:user_id] = 2
2019 2019 get :new, :project_id => 1,
2020 2020 :issue => {:tracker_id => 3, :description => 'Prefilled', :custom_field_values => {'2' => 'Custom field value'}}
2021 2021
2022 2022 issue = assigns(:issue)
2023 2023 assert_equal 3, issue.tracker_id
2024 2024 assert_equal 'Prefilled', issue.description
2025 2025 assert_equal 'Custom field value', issue.custom_field_value(2)
2026 2026
2027 2027 assert_select 'select[name=?]', 'issue[tracker_id]' do
2028 2028 assert_select 'option[value="3"][selected=selected]'
2029 2029 end
2030 2030 assert_select 'textarea[name=?]', 'issue[description]', :text => /Prefilled/
2031 2031 assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', 'Custom field value'
2032 2032 end
2033 2033
2034 2034 def test_get_new_should_mark_required_fields
2035 2035 cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
2036 2036 cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
2037 2037 WorkflowPermission.delete_all
2038 2038 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'required')
2039 2039 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'required')
2040 2040 @request.session[:user_id] = 2
2041 2041
2042 2042 get :new, :project_id => 1
2043 2043 assert_response :success
2044 2044 assert_template 'new'
2045 2045
2046 2046 assert_select 'label[for=issue_start_date]' do
2047 2047 assert_select 'span[class=required]', 0
2048 2048 end
2049 2049 assert_select 'label[for=issue_due_date]' do
2050 2050 assert_select 'span[class=required]'
2051 2051 end
2052 2052 assert_select 'label[for=?]', "issue_custom_field_values_#{cf1.id}" do
2053 2053 assert_select 'span[class=required]', 0
2054 2054 end
2055 2055 assert_select 'label[for=?]', "issue_custom_field_values_#{cf2.id}" do
2056 2056 assert_select 'span[class=required]'
2057 2057 end
2058 2058 end
2059 2059
2060 2060 def test_get_new_should_not_display_readonly_fields
2061 2061 cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
2062 2062 cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
2063 2063 WorkflowPermission.delete_all
2064 2064 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'readonly')
2065 2065 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'readonly')
2066 2066 @request.session[:user_id] = 2
2067 2067
2068 2068 get :new, :project_id => 1
2069 2069 assert_response :success
2070 2070 assert_template 'new'
2071 2071
2072 2072 assert_select 'input[name=?]', 'issue[start_date]'
2073 2073 assert_select 'input[name=?]', 'issue[due_date]', 0
2074 2074 assert_select 'input[name=?]', "issue[custom_field_values][#{cf1.id}]"
2075 2075 assert_select 'input[name=?]', "issue[custom_field_values][#{cf2.id}]", 0
2076 2076 end
2077 2077
2078 2078 def test_new_with_tracker_set_as_readonly_should_accept_status
2079 2079 WorkflowPermission.delete_all
2080 2080 [1, 2].each do |status_id|
2081 2081 WorkflowPermission.create!(:tracker_id => 1, :old_status_id => status_id, :role_id => 1, :field_name => 'tracker_id', :rule => 'readonly')
2082 2082 end
2083 2083 @request.session[:user_id] = 2
2084 2084
2085 2085 get :new, :project_id => 1, :issue => {:status_id => 2}
2086 2086 assert_select 'select[name=?]', 'issue[tracker_id]', 0
2087 2087 assert_equal 2, assigns(:issue).status_id
2088 2088 end
2089 2089
2090 2090 def test_get_new_without_tracker_id
2091 2091 @request.session[:user_id] = 2
2092 2092 get :new, :project_id => 1
2093 2093 assert_response :success
2094 2094 assert_template 'new'
2095 2095
2096 2096 issue = assigns(:issue)
2097 2097 assert_not_nil issue
2098 2098 assert_equal Project.find(1).trackers.first, issue.tracker
2099 2099 end
2100 2100
2101 2101 def test_get_new_with_no_default_status_should_display_an_error
2102 2102 @request.session[:user_id] = 2
2103 2103 IssueStatus.delete_all
2104 2104
2105 2105 get :new, :project_id => 1
2106 2106 assert_response 500
2107 2107 assert_select_error /No default issue/
2108 2108 end
2109 2109
2110 2110 def test_get_new_with_no_tracker_should_display_an_error
2111 2111 @request.session[:user_id] = 2
2112 2112 Tracker.delete_all
2113 2113
2114 2114 get :new, :project_id => 1
2115 2115 assert_response 500
2116 2116 assert_select_error /No tracker/
2117 2117 end
2118 2118
2119 2119 def test_new_with_invalid_project_id
2120 2120 @request.session[:user_id] = 1
2121 2121 get :new, :project_id => 'invalid'
2122 2122 assert_response 404
2123 2123 end
2124 2124
2125 2125 def test_new_with_parent_id_should_only_propose_valid_trackers
2126 2126 @request.session[:user_id] = 2
2127 2127 t = Tracker.find(3)
2128 2128 assert !t.disabled_core_fields.include?('parent_issue_id')
2129 2129
2130 2130 get :new, :project_id => 1, issue: { parent_issue_id: 1 }
2131 2131 assert_response :success
2132 2132 assert_select 'option', text: /#{t.name}/, count: 1
2133 2133
2134 2134 t.core_fields = Tracker::CORE_FIELDS - ['parent_issue_id']
2135 2135 t.save!
2136 2136 assert t.disabled_core_fields.include?('parent_issue_id')
2137 2137 get :new, :project_id => 1, issue: { parent_issue_id: 1 }
2138 2138 assert_response :success
2139 2139 assert_select 'option', text: /#{t.name}/, count: 0
2140 2140 end
2141 2141
2142 2142 def test_update_form_for_new_issue
2143 2143 @request.session[:user_id] = 2
2144 2144 xhr :post, :new, :project_id => 1,
2145 2145 :issue => {:tracker_id => 2,
2146 2146 :subject => 'This is the test_new issue',
2147 2147 :description => 'This is the description',
2148 2148 :priority_id => 5}
2149 2149 assert_response :success
2150 2150 assert_template 'new'
2151 2151 assert_template :partial => '_form'
2152 2152 assert_equal 'text/javascript', response.content_type
2153 2153
2154 2154 issue = assigns(:issue)
2155 2155 assert_kind_of Issue, issue
2156 2156 assert_equal 1, issue.project_id
2157 2157 assert_equal 2, issue.tracker_id
2158 2158 assert_equal 'This is the test_new issue', issue.subject
2159 2159 end
2160 2160
2161 2161 def test_update_form_for_new_issue_should_propose_transitions_based_on_initial_status
2162 2162 @request.session[:user_id] = 2
2163 2163 WorkflowTransition.delete_all
2164 2164 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 0, :new_status_id => 2)
2165 2165 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 0, :new_status_id => 5)
2166 2166 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4)
2167 2167
2168 2168 xhr :post, :new, :project_id => 1,
2169 2169 :issue => {:tracker_id => 1,
2170 2170 :status_id => 5,
2171 2171 :subject => 'This is an issue'}
2172 2172
2173 2173 assert_equal 5, assigns(:issue).status_id
2174 2174 assert_equal [2,5], assigns(:allowed_statuses).map(&:id).sort
2175 2175 end
2176 2176
2177 2177 def test_update_form_with_default_status_should_ignore_submitted_status_id_if_equals
2178 2178 @request.session[:user_id] = 2
2179 2179 tracker = Tracker.find(2)
2180 2180 tracker.update! :default_status_id => 2
2181 2181 tracker.generate_transitions! 2, 1, :clear => true
2182 2182
2183 2183 xhr :post, :new, :project_id => 1,
2184 2184 :issue => {:tracker_id => 2,
2185 2185 :status_id => 1},
2186 2186 :was_default_status => 1
2187 2187
2188 2188 assert_equal 2, assigns(:issue).status_id
2189 2189 end
2190 2190
2191 2191 def test_update_form_for_new_issue_should_ignore_version_when_changing_project
2192 2192 version = Version.generate!(:project_id => 1)
2193 2193 Project.find(1).update_attribute :default_version_id, version.id
2194 2194 @request.session[:user_id] = 2
2195 2195
2196 2196 xhr :post, :new, :issue => {:project_id => 1,
2197 2197 :fixed_version_id => ''},
2198 2198 :form_update_triggered_by => 'issue_project_id'
2199 2199 assert_response :success
2200 2200 assert_template 'new'
2201 2201
2202 2202 issue = assigns(:issue)
2203 2203 assert_equal 1, issue.project_id
2204 2204 assert_equal version, issue.fixed_version
2205 2205 end
2206 2206
2207 2207 def test_post_create
2208 2208 @request.session[:user_id] = 2
2209 2209 assert_difference 'Issue.count' do
2210 2210 assert_no_difference 'Journal.count' do
2211 2211 post :create, :project_id => 1,
2212 2212 :issue => {:tracker_id => 3,
2213 2213 :status_id => 2,
2214 2214 :subject => 'This is the test_new issue',
2215 2215 :description => 'This is the description',
2216 2216 :priority_id => 5,
2217 2217 :start_date => '2010-11-07',
2218 2218 :estimated_hours => '',
2219 2219 :custom_field_values => {'2' => 'Value for field 2'}}
2220 2220 end
2221 2221 end
2222 2222 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
2223 2223
2224 2224 issue = Issue.find_by_subject('This is the test_new issue')
2225 2225 assert_not_nil issue
2226 2226 assert_equal 2, issue.author_id
2227 2227 assert_equal 3, issue.tracker_id
2228 2228 assert_equal 2, issue.status_id
2229 2229 assert_equal Date.parse('2010-11-07'), issue.start_date
2230 2230 assert_nil issue.estimated_hours
2231 2231 v = issue.custom_values.where(:custom_field_id => 2).first
2232 2232 assert_not_nil v
2233 2233 assert_equal 'Value for field 2', v.value
2234 2234 end
2235 2235
2236 2236 def test_post_new_with_group_assignment
2237 2237 group = Group.find(11)
2238 2238 project = Project.find(1)
2239 2239 project.members << Member.new(:principal => group, :roles => [Role.givable.first])
2240 2240
2241 2241 with_settings :issue_group_assignment => '1' do
2242 2242 @request.session[:user_id] = 2
2243 2243 assert_difference 'Issue.count' do
2244 2244 post :create, :project_id => project.id,
2245 2245 :issue => {:tracker_id => 3,
2246 2246 :status_id => 1,
2247 2247 :subject => 'This is the test_new_with_group_assignment issue',
2248 2248 :assigned_to_id => group.id}
2249 2249 end
2250 2250 end
2251 2251 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
2252 2252
2253 2253 issue = Issue.find_by_subject('This is the test_new_with_group_assignment issue')
2254 2254 assert_not_nil issue
2255 2255 assert_equal group, issue.assigned_to
2256 2256 end
2257 2257
2258 2258 def test_post_create_without_start_date_and_default_start_date_is_not_creation_date
2259 2259 with_settings :default_issue_start_date_to_creation_date => 0 do
2260 2260 @request.session[:user_id] = 2
2261 2261 assert_difference 'Issue.count' do
2262 2262 post :create, :project_id => 1,
2263 2263 :issue => {:tracker_id => 3,
2264 2264 :status_id => 2,
2265 2265 :subject => 'This is the test_new issue',
2266 2266 :description => 'This is the description',
2267 2267 :priority_id => 5,
2268 2268 :estimated_hours => '',
2269 2269 :custom_field_values => {'2' => 'Value for field 2'}}
2270 2270 end
2271 2271 assert_redirected_to :controller => 'issues', :action => 'show',
2272 2272 :id => Issue.last.id
2273 2273 issue = Issue.find_by_subject('This is the test_new issue')
2274 2274 assert_not_nil issue
2275 2275 assert_nil issue.start_date
2276 2276 end
2277 2277 end
2278 2278
2279 2279 def test_post_create_without_start_date_and_default_start_date_is_creation_date
2280 2280 with_settings :default_issue_start_date_to_creation_date => 1 do
2281 2281 @request.session[:user_id] = 2
2282 2282 assert_difference 'Issue.count' do
2283 2283 post :create, :project_id => 1,
2284 2284 :issue => {:tracker_id => 3,
2285 2285 :status_id => 2,
2286 2286 :subject => 'This is the test_new issue',
2287 2287 :description => 'This is the description',
2288 2288 :priority_id => 5,
2289 2289 :estimated_hours => '',
2290 2290 :custom_field_values => {'2' => 'Value for field 2'}}
2291 2291 end
2292 2292 assert_redirected_to :controller => 'issues', :action => 'show',
2293 2293 :id => Issue.last.id
2294 2294 issue = Issue.find_by_subject('This is the test_new issue')
2295 2295 assert_not_nil issue
2296 2296 assert_equal Date.today, issue.start_date
2297 2297 end
2298 2298 end
2299 2299
2300 2300 def test_post_create_and_continue
2301 2301 @request.session[:user_id] = 2
2302 2302 assert_difference 'Issue.count' do
2303 2303 post :create, :project_id => 1,
2304 2304 :issue => {:tracker_id => 3, :subject => 'This is first issue', :priority_id => 5},
2305 2305 :continue => ''
2306 2306 end
2307 2307
2308 2308 issue = Issue.order('id DESC').first
2309 2309 assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook', :issue => {:tracker_id => 3}
2310 2310 assert_not_nil flash[:notice], "flash was not set"
2311 2311 assert_select_in flash[:notice],
2312 2312 'a[href=?][title=?]', "/issues/#{issue.id}", "This is first issue", :text => "##{issue.id}"
2313 2313 end
2314 2314
2315 2315 def test_post_create_without_custom_fields_param
2316 2316 @request.session[:user_id] = 2
2317 2317 assert_difference 'Issue.count' do
2318 2318 post :create, :project_id => 1,
2319 2319 :issue => {:tracker_id => 1,
2320 2320 :subject => 'This is the test_new issue',
2321 2321 :description => 'This is the description',
2322 2322 :priority_id => 5}
2323 2323 end
2324 2324 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
2325 2325 end
2326 2326
2327 2327 def test_post_create_with_multi_custom_field
2328 2328 field = IssueCustomField.find_by_name('Database')
2329 2329 field.update_attribute(:multiple, true)
2330 2330
2331 2331 @request.session[:user_id] = 2
2332 2332 assert_difference 'Issue.count' do
2333 2333 post :create, :project_id => 1,
2334 2334 :issue => {:tracker_id => 1,
2335 2335 :subject => 'This is the test_new issue',
2336 2336 :description => 'This is the description',
2337 2337 :priority_id => 5,
2338 2338 :custom_field_values => {'1' => ['', 'MySQL', 'Oracle']}}
2339 2339 end
2340 2340 assert_response 302
2341 2341 issue = Issue.order('id DESC').first
2342 2342 assert_equal ['MySQL', 'Oracle'], issue.custom_field_value(1).sort
2343 2343 end
2344 2344
2345 2345 def test_post_create_with_empty_multi_custom_field
2346 2346 field = IssueCustomField.find_by_name('Database')
2347 2347 field.update_attribute(:multiple, true)
2348 2348
2349 2349 @request.session[:user_id] = 2
2350 2350 assert_difference 'Issue.count' do
2351 2351 post :create, :project_id => 1,
2352 2352 :issue => {:tracker_id => 1,
2353 2353 :subject => 'This is the test_new issue',
2354 2354 :description => 'This is the description',
2355 2355 :priority_id => 5,
2356 2356 :custom_field_values => {'1' => ['']}}
2357 2357 end
2358 2358 assert_response 302
2359 2359 issue = Issue.order('id DESC').first
2360 2360 assert_equal [''], issue.custom_field_value(1).sort
2361 2361 end
2362 2362
2363 2363 def test_post_create_with_multi_user_custom_field
2364 2364 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
2365 2365 :tracker_ids => [1], :is_for_all => true)
2366 2366
2367 2367 @request.session[:user_id] = 2
2368 2368 assert_difference 'Issue.count' do
2369 2369 post :create, :project_id => 1,
2370 2370 :issue => {:tracker_id => 1,
2371 2371 :subject => 'This is the test_new issue',
2372 2372 :description => 'This is the description',
2373 2373 :priority_id => 5,
2374 2374 :custom_field_values => {field.id.to_s => ['', '2', '3']}}
2375 2375 end
2376 2376 assert_response 302
2377 2377 issue = Issue.order('id DESC').first
2378 2378 assert_equal ['2', '3'], issue.custom_field_value(field).sort
2379 2379 end
2380 2380
2381 2381 def test_post_create_with_required_custom_field_and_without_custom_fields_param
2382 2382 field = IssueCustomField.find_by_name('Database')
2383 2383 field.update_attribute(:is_required, true)
2384 2384
2385 2385 @request.session[:user_id] = 2
2386 2386 assert_no_difference 'Issue.count' do
2387 2387 post :create, :project_id => 1,
2388 2388 :issue => {:tracker_id => 1,
2389 2389 :subject => 'This is the test_new issue',
2390 2390 :description => 'This is the description',
2391 2391 :priority_id => 5}
2392 2392 end
2393 2393 assert_response :success
2394 2394 assert_template 'new'
2395 2395 issue = assigns(:issue)
2396 2396 assert_not_nil issue
2397 2397 assert_select_error /Database cannot be blank/
2398 2398 end
2399 2399
2400 2400 def test_create_should_validate_required_fields
2401 2401 cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
2402 2402 cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
2403 2403 WorkflowPermission.delete_all
2404 2404 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => 'due_date', :rule => 'required')
2405 2405 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'required')
2406 2406 @request.session[:user_id] = 2
2407 2407
2408 2408 assert_no_difference 'Issue.count' do
2409 2409 post :create, :project_id => 1, :issue => {
2410 2410 :tracker_id => 2,
2411 2411 :status_id => 1,
2412 2412 :subject => 'Test',
2413 2413 :start_date => '',
2414 2414 :due_date => '',
2415 2415 :custom_field_values => {cf1.id.to_s => '', cf2.id.to_s => ''}
2416 2416 }
2417 2417 assert_response :success
2418 2418 assert_template 'new'
2419 2419 end
2420 2420
2421 2421 assert_select_error /Due date cannot be blank/i
2422 2422 assert_select_error /Bar cannot be blank/i
2423 2423 end
2424 2424
2425 2425 def test_create_should_validate_required_list_fields
2426 2426 cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'list', :is_for_all => true, :tracker_ids => [1, 2], :multiple => false, :possible_values => ['a', 'b'])
2427 2427 cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'list', :is_for_all => true, :tracker_ids => [1, 2], :multiple => true, :possible_values => ['a', 'b'])
2428 2428 WorkflowPermission.delete_all
2429 2429 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => cf1.id.to_s, :rule => 'required')
2430 2430 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'required')
2431 2431 @request.session[:user_id] = 2
2432 2432
2433 2433 assert_no_difference 'Issue.count' do
2434 2434 post :create, :project_id => 1, :issue => {
2435 2435 :tracker_id => 2,
2436 2436 :status_id => 1,
2437 2437 :subject => 'Test',
2438 2438 :start_date => '',
2439 2439 :due_date => '',
2440 2440 :custom_field_values => {cf1.id.to_s => '', cf2.id.to_s => ['']}
2441 2441 }
2442 2442 assert_response :success
2443 2443 assert_template 'new'
2444 2444 end
2445 2445
2446 2446 assert_select_error /Foo cannot be blank/i
2447 2447 assert_select_error /Bar cannot be blank/i
2448 2448 end
2449 2449
2450 2450 def test_create_should_ignore_readonly_fields
2451 2451 cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
2452 2452 cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
2453 2453 WorkflowPermission.delete_all
2454 2454 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => 'due_date', :rule => 'readonly')
2455 2455 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'readonly')
2456 2456 @request.session[:user_id] = 2
2457 2457
2458 2458 assert_difference 'Issue.count' do
2459 2459 post :create, :project_id => 1, :issue => {
2460 2460 :tracker_id => 2,
2461 2461 :status_id => 1,
2462 2462 :subject => 'Test',
2463 2463 :start_date => '2012-07-14',
2464 2464 :due_date => '2012-07-16',
2465 2465 :custom_field_values => {cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'}
2466 2466 }
2467 2467 assert_response 302
2468 2468 end
2469 2469
2470 2470 issue = Issue.order('id DESC').first
2471 2471 assert_equal Date.parse('2012-07-14'), issue.start_date
2472 2472 assert_nil issue.due_date
2473 2473 assert_equal 'value1', issue.custom_field_value(cf1)
2474 2474 assert_nil issue.custom_field_value(cf2)
2475 2475 end
2476 2476
2477 2477 def test_create_should_ignore_unallowed_trackers
2478 2478 role = Role.find(1)
2479 2479 role.set_permission_trackers :add_issues, [3]
2480 2480 role.save!
2481 2481 @request.session[:user_id] = 2
2482 2482
2483 2483 issue = new_record(Issue) do
2484 2484 post :create, :project_id => 1, :issue => {
2485 2485 :tracker_id => 1,
2486 2486 :status_id => 1,
2487 2487 :subject => 'Test'
2488 2488 }
2489 2489 assert_response 302
2490 2490 end
2491 2491 assert_equal 3, issue.tracker_id
2492 2492 end
2493 2493
2494 2494 def test_post_create_with_watchers
2495 2495 @request.session[:user_id] = 2
2496 2496 ActionMailer::Base.deliveries.clear
2497 2497
2498 2498 with_settings :notified_events => %w(issue_added) do
2499 2499 assert_difference 'Watcher.count', 2 do
2500 2500 post :create, :project_id => 1,
2501 2501 :issue => {:tracker_id => 1,
2502 2502 :subject => 'This is a new issue with watchers',
2503 2503 :description => 'This is the description',
2504 2504 :priority_id => 5,
2505 2505 :watcher_user_ids => ['2', '3']}
2506 2506 end
2507 2507 end
2508 2508 issue = Issue.find_by_subject('This is a new issue with watchers')
2509 2509 assert_not_nil issue
2510 2510 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
2511 2511
2512 2512 # Watchers added
2513 2513 assert_equal [2, 3], issue.watcher_user_ids.sort
2514 2514 assert issue.watched_by?(User.find(3))
2515 2515 # Watchers notified
2516 2516 mail = ActionMailer::Base.deliveries.last
2517 2517 assert_not_nil mail
2518 2518 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
2519 2519 end
2520 2520
2521 2521 def test_post_create_subissue
2522 2522 @request.session[:user_id] = 2
2523 2523
2524 2524 assert_difference 'Issue.count' do
2525 2525 post :create, :project_id => 1,
2526 2526 :issue => {:tracker_id => 1,
2527 2527 :subject => 'This is a child issue',
2528 2528 :parent_issue_id => '2'}
2529 2529 assert_response 302
2530 2530 end
2531 2531 issue = Issue.order('id DESC').first
2532 2532 assert_equal Issue.find(2), issue.parent
2533 2533 end
2534 2534
2535 2535 def test_post_create_subissue_with_sharp_parent_id
2536 2536 @request.session[:user_id] = 2
2537 2537
2538 2538 assert_difference 'Issue.count' do
2539 2539 post :create, :project_id => 1,
2540 2540 :issue => {:tracker_id => 1,
2541 2541 :subject => 'This is a child issue',
2542 2542 :parent_issue_id => '#2'}
2543 2543 assert_response 302
2544 2544 end
2545 2545 issue = Issue.order('id DESC').first
2546 2546 assert_equal Issue.find(2), issue.parent
2547 2547 end
2548 2548
2549 2549 def test_post_create_subissue_with_non_visible_parent_id_should_not_validate
2550 2550 @request.session[:user_id] = 2
2551 2551
2552 2552 assert_no_difference 'Issue.count' do
2553 2553 post :create, :project_id => 1,
2554 2554 :issue => {:tracker_id => 1,
2555 2555 :subject => 'This is a child issue',
2556 2556 :parent_issue_id => '4'}
2557 2557
2558 2558 assert_response :success
2559 2559 assert_select 'input[name=?][value=?]', 'issue[parent_issue_id]', '4'
2560 2560 assert_select_error /Parent task is invalid/i
2561 2561 end
2562 2562 end
2563 2563
2564 2564 def test_post_create_subissue_with_non_numeric_parent_id_should_not_validate
2565 2565 @request.session[:user_id] = 2
2566 2566
2567 2567 assert_no_difference 'Issue.count' do
2568 2568 post :create, :project_id => 1,
2569 2569 :issue => {:tracker_id => 1,
2570 2570 :subject => 'This is a child issue',
2571 2571 :parent_issue_id => '01ABC'}
2572 2572
2573 2573 assert_response :success
2574 2574 assert_select 'input[name=?][value=?]', 'issue[parent_issue_id]', '01ABC'
2575 2575 assert_select_error /Parent task is invalid/i
2576 2576 end
2577 2577 end
2578 2578
2579 2579 def test_post_create_private
2580 2580 @request.session[:user_id] = 2
2581 2581
2582 2582 assert_difference 'Issue.count' do
2583 2583 post :create, :project_id => 1,
2584 2584 :issue => {:tracker_id => 1,
2585 2585 :subject => 'This is a private issue',
2586 2586 :is_private => '1'}
2587 2587 end
2588 2588 issue = Issue.order('id DESC').first
2589 2589 assert issue.is_private?
2590 2590 end
2591 2591
2592 2592 def test_post_create_private_with_set_own_issues_private_permission
2593 2593 role = Role.find(1)
2594 2594 role.remove_permission! :set_issues_private
2595 2595 role.add_permission! :set_own_issues_private
2596 2596
2597 2597 @request.session[:user_id] = 2
2598 2598
2599 2599 assert_difference 'Issue.count' do
2600 2600 post :create, :project_id => 1,
2601 2601 :issue => {:tracker_id => 1,
2602 2602 :subject => 'This is a private issue',
2603 2603 :is_private => '1'}
2604 2604 end
2605 2605 issue = Issue.order('id DESC').first
2606 2606 assert issue.is_private?
2607 2607 end
2608 2608
2609 2609 def test_create_without_project_id
2610 2610 @request.session[:user_id] = 2
2611 2611
2612 2612 assert_difference 'Issue.count' do
2613 2613 post :create,
2614 2614 :issue => {:project_id => 3,
2615 2615 :tracker_id => 2,
2616 2616 :subject => 'Foo'}
2617 2617 assert_response 302
2618 2618 end
2619 2619 issue = Issue.order('id DESC').first
2620 2620 assert_equal 3, issue.project_id
2621 2621 assert_equal 2, issue.tracker_id
2622 2622 end
2623 2623
2624 2624 def test_create_without_project_id_and_continue_should_redirect_without_project_id
2625 2625 @request.session[:user_id] = 2
2626 2626
2627 2627 assert_difference 'Issue.count' do
2628 2628 post :create,
2629 2629 :issue => {:project_id => 3,
2630 2630 :tracker_id => 2,
2631 2631 :subject => 'Foo'},
2632 2632 :continue => '1'
2633 2633 assert_redirected_to '/issues/new?issue%5Bproject_id%5D=3&issue%5Btracker_id%5D=2'
2634 2634 end
2635 2635 end
2636 2636
2637 2637 def test_create_without_project_id_should_be_denied_without_permission
2638 2638 Role.non_member.remove_permission! :add_issues
2639 2639 Role.anonymous.remove_permission! :add_issues
2640 2640 @request.session[:user_id] = 2
2641 2641
2642 2642 assert_no_difference 'Issue.count' do
2643 2643 post :create,
2644 2644 :issue => {:project_id => 3,
2645 2645 :tracker_id => 2,
2646 2646 :subject => 'Foo'}
2647 2647 assert_response 422
2648 2648 end
2649 2649 end
2650 2650
2651 2651 def test_create_without_project_id_with_failure
2652 2652 @request.session[:user_id] = 2
2653 2653
2654 2654 post :create,
2655 2655 :issue => {:project_id => 3,
2656 2656 :tracker_id => 2,
2657 2657 :subject => ''}
2658 2658 assert_response :success
2659 2659 assert_nil assigns(:project)
2660 2660 end
2661 2661
2662 2662 def test_post_create_should_send_a_notification
2663 2663 ActionMailer::Base.deliveries.clear
2664 2664 @request.session[:user_id] = 2
2665 2665 with_settings :notified_events => %w(issue_added) do
2666 2666 assert_difference 'Issue.count' do
2667 2667 post :create, :project_id => 1,
2668 2668 :issue => {:tracker_id => 3,
2669 2669 :subject => 'This is the test_new issue',
2670 2670 :description => 'This is the description',
2671 2671 :priority_id => 5,
2672 2672 :estimated_hours => '',
2673 2673 :custom_field_values => {'2' => 'Value for field 2'}}
2674 2674 end
2675 2675 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
2676 2676
2677 2677 assert_equal 1, ActionMailer::Base.deliveries.size
2678 2678 end
2679 2679 end
2680 2680
2681 2681 def test_post_create_should_preserve_fields_values_on_validation_failure
2682 2682 @request.session[:user_id] = 2
2683 2683 post :create, :project_id => 1,
2684 2684 :issue => {:tracker_id => 1,
2685 2685 # empty subject
2686 2686 :subject => '',
2687 2687 :description => 'This is a description',
2688 2688 :priority_id => 6,
2689 2689 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
2690 2690 assert_response :success
2691 2691 assert_template 'new'
2692 2692
2693 2693 assert_select 'textarea[name=?]', 'issue[description]', :text => 'This is a description'
2694 2694 assert_select 'select[name=?]', 'issue[priority_id]' do
2695 2695 assert_select 'option[value="6"][selected=selected]', :text => 'High'
2696 2696 end
2697 2697 # Custom fields
2698 2698 assert_select 'select[name=?]', 'issue[custom_field_values][1]' do
2699 2699 assert_select 'option[value=Oracle][selected=selected]', :text => 'Oracle'
2700 2700 end
2701 2701 assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', 'Value for field 2'
2702 2702 end
2703 2703
2704 2704 def test_post_create_with_failure_should_preserve_watchers
2705 2705 assert !User.find(8).member_of?(Project.find(1))
2706 2706
2707 2707 @request.session[:user_id] = 2
2708 2708 post :create, :project_id => 1,
2709 2709 :issue => {:tracker_id => 1,
2710 2710 :watcher_user_ids => ['3', '8']}
2711 2711 assert_response :success
2712 2712 assert_template 'new'
2713 2713
2714 2714 assert_select 'input[name=?][value="2"]:not(checked)', 'issue[watcher_user_ids][]'
2715 2715 assert_select 'input[name=?][value="3"][checked=checked]', 'issue[watcher_user_ids][]'
2716 2716 assert_select 'input[name=?][value="8"][checked=checked]', 'issue[watcher_user_ids][]'
2717 2717 end
2718 2718
2719 2719 def test_post_create_should_ignore_non_safe_attributes
2720 2720 @request.session[:user_id] = 2
2721 2721 assert_nothing_raised do
2722 2722 post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
2723 2723 end
2724 2724 end
2725 2725
2726 2726 def test_post_create_with_attachment
2727 2727 set_tmp_attachments_directory
2728 2728 @request.session[:user_id] = 2
2729 2729
2730 2730 assert_difference 'Issue.count' do
2731 2731 assert_difference 'Attachment.count' do
2732 2732 assert_no_difference 'Journal.count' do
2733 2733 post :create, :project_id => 1,
2734 2734 :issue => { :tracker_id => '1', :subject => 'With attachment' },
2735 2735 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
2736 2736 end
2737 2737 end
2738 2738 end
2739 2739
2740 2740 issue = Issue.order('id DESC').first
2741 2741 attachment = Attachment.order('id DESC').first
2742 2742
2743 2743 assert_equal issue, attachment.container
2744 2744 assert_equal 2, attachment.author_id
2745 2745 assert_equal 'testfile.txt', attachment.filename
2746 2746 assert_equal 'text/plain', attachment.content_type
2747 2747 assert_equal 'test file', attachment.description
2748 2748 assert_equal 59, attachment.filesize
2749 2749 assert File.exists?(attachment.diskfile)
2750 2750 assert_equal 59, File.size(attachment.diskfile)
2751 2751 end
2752 2752
2753 2753 def test_post_create_with_attachment_should_notify_with_attachments
2754 2754 ActionMailer::Base.deliveries.clear
2755 2755 set_tmp_attachments_directory
2756 2756 @request.session[:user_id] = 2
2757 2757
2758 2758 with_settings :notified_events => %w(issue_added) do
2759 2759 assert_difference 'Issue.count' do
2760 2760 post :create, :project_id => 1,
2761 2761 :issue => { :tracker_id => '1', :subject => 'With attachment' },
2762 2762 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
2763 2763 end
2764 2764 end
2765 2765
2766 2766 assert_not_nil ActionMailer::Base.deliveries.last
2767 2767 assert_select_email do
2768 2768 assert_select 'a[href^=?]', 'http://localhost:3000/attachments/download', 'testfile.txt'
2769 2769 end
2770 2770 end
2771 2771
2772 2772 def test_post_create_with_failure_should_save_attachments
2773 2773 set_tmp_attachments_directory
2774 2774 @request.session[:user_id] = 2
2775 2775
2776 2776 assert_no_difference 'Issue.count' do
2777 2777 assert_difference 'Attachment.count' do
2778 2778 post :create, :project_id => 1,
2779 2779 :issue => { :tracker_id => '1', :subject => '' },
2780 2780 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
2781 2781 assert_response :success
2782 2782 assert_template 'new'
2783 2783 end
2784 2784 end
2785 2785
2786 2786 attachment = Attachment.order('id DESC').first
2787 2787 assert_equal 'testfile.txt', attachment.filename
2788 2788 assert File.exists?(attachment.diskfile)
2789 2789 assert_nil attachment.container
2790 2790
2791 2791 assert_select 'input[name=?][value=?]', 'attachments[p0][token]', attachment.token
2792 2792 assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'testfile.txt'
2793 2793 end
2794 2794
2795 2795 def test_post_create_with_failure_should_keep_saved_attachments
2796 2796 set_tmp_attachments_directory
2797 2797 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
2798 2798 @request.session[:user_id] = 2
2799 2799
2800 2800 assert_no_difference 'Issue.count' do
2801 2801 assert_no_difference 'Attachment.count' do
2802 2802 post :create, :project_id => 1,
2803 2803 :issue => { :tracker_id => '1', :subject => '' },
2804 2804 :attachments => {'p0' => {'token' => attachment.token}}
2805 2805 assert_response :success
2806 2806 assert_template 'new'
2807 2807 end
2808 2808 end
2809 2809
2810 2810 assert_select 'input[name=?][value=?]', 'attachments[p0][token]', attachment.token
2811 2811 assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'testfile.txt'
2812 2812 end
2813 2813
2814 2814 def test_post_create_should_attach_saved_attachments
2815 2815 set_tmp_attachments_directory
2816 2816 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
2817 2817 @request.session[:user_id] = 2
2818 2818
2819 2819 assert_difference 'Issue.count' do
2820 2820 assert_no_difference 'Attachment.count' do
2821 2821 post :create, :project_id => 1,
2822 2822 :issue => { :tracker_id => '1', :subject => 'Saved attachments' },
2823 2823 :attachments => {'p0' => {'token' => attachment.token}}
2824 2824 assert_response 302
2825 2825 end
2826 2826 end
2827 2827
2828 2828 issue = Issue.order('id DESC').first
2829 2829 assert_equal 1, issue.attachments.count
2830 2830
2831 2831 attachment.reload
2832 2832 assert_equal issue, attachment.container
2833 2833 end
2834 2834
2835 2835 def setup_without_workflow_privilege
2836 2836 WorkflowTransition.delete_all(["role_id = ?", Role.anonymous.id])
2837 2837 Role.anonymous.add_permission! :add_issues, :add_issue_notes
2838 2838 end
2839 2839 private :setup_without_workflow_privilege
2840 2840
2841 2841 test "without workflow privilege #new should propose default status only" do
2842 2842 setup_without_workflow_privilege
2843 2843 get :new, :project_id => 1
2844 2844 assert_response :success
2845 2845 assert_template 'new'
2846 2846
2847 2847 issue = assigns(:issue)
2848 2848 assert_not_nil issue.default_status
2849 2849
2850 2850 assert_select 'select[name=?]', 'issue[status_id]' do
2851 2851 assert_select 'option', 1
2852 2852 assert_select 'option[value=?]', issue.default_status.id.to_s
2853 2853 end
2854 2854 end
2855 2855
2856 2856 test "without workflow privilege #create should accept default status" do
2857 2857 setup_without_workflow_privilege
2858 2858 assert_difference 'Issue.count' do
2859 2859 post :create, :project_id => 1,
2860 2860 :issue => {:tracker_id => 1,
2861 2861 :subject => 'This is an issue',
2862 2862 :status_id => 1}
2863 2863 end
2864 2864 issue = Issue.order('id').last
2865 2865 assert_not_nil issue.default_status
2866 2866 assert_equal issue.default_status, issue.status
2867 2867 end
2868 2868
2869 2869 test "without workflow privilege #create should ignore unauthorized status" do
2870 2870 setup_without_workflow_privilege
2871 2871 assert_difference 'Issue.count' do
2872 2872 post :create, :project_id => 1,
2873 2873 :issue => {:tracker_id => 1,
2874 2874 :subject => 'This is an issue',
2875 2875 :status_id => 3}
2876 2876 end
2877 2877 issue = Issue.order('id').last
2878 2878 assert_not_nil issue.default_status
2879 2879 assert_equal issue.default_status, issue.status
2880 2880 end
2881 2881
2882 2882 test "without workflow privilege #update should ignore status change" do
2883 2883 setup_without_workflow_privilege
2884 2884 assert_difference 'Journal.count' do
2885 2885 put :update, :id => 1, :issue => {:status_id => 3, :notes => 'just trying'}
2886 2886 end
2887 2887 assert_equal 1, Issue.find(1).status_id
2888 2888 end
2889 2889
2890 2890 test "without workflow privilege #update ignore attributes changes" do
2891 2891 setup_without_workflow_privilege
2892 2892 assert_difference 'Journal.count' do
2893 2893 put :update, :id => 1,
2894 2894 :issue => {:subject => 'changed', :assigned_to_id => 2,
2895 2895 :notes => 'just trying'}
2896 2896 end
2897 2897 issue = Issue.find(1)
2898 2898 assert_equal "Cannot print recipes", issue.subject
2899 2899 assert_nil issue.assigned_to
2900 2900 end
2901 2901
2902 2902 def setup_with_workflow_privilege
2903 2903 WorkflowTransition.delete_all(["role_id = ?", Role.anonymous.id])
2904 2904 WorkflowTransition.create!(:role => Role.anonymous, :tracker_id => 1,
2905 2905 :old_status_id => 1, :new_status_id => 3)
2906 2906 WorkflowTransition.create!(:role => Role.anonymous, :tracker_id => 1,
2907 2907 :old_status_id => 1, :new_status_id => 4)
2908 2908 Role.anonymous.add_permission! :add_issues, :add_issue_notes
2909 2909 end
2910 2910 private :setup_with_workflow_privilege
2911 2911
2912 2912 def setup_with_workflow_privilege_and_edit_issues_permission
2913 2913 setup_with_workflow_privilege
2914 2914 Role.anonymous.add_permission! :add_issues, :edit_issues
2915 2915 end
2916 2916 private :setup_with_workflow_privilege_and_edit_issues_permission
2917 2917
2918 2918 test "with workflow privilege and :edit_issues permission should accept authorized status" do
2919 2919 setup_with_workflow_privilege_and_edit_issues_permission
2920 2920 assert_difference 'Journal.count' do
2921 2921 put :update, :id => 1, :issue => {:status_id => 3, :notes => 'just trying'}
2922 2922 end
2923 2923 assert_equal 3, Issue.find(1).status_id
2924 2924 end
2925 2925
2926 2926 test "with workflow privilege and :edit_issues permission should ignore unauthorized status" do
2927 2927 setup_with_workflow_privilege_and_edit_issues_permission
2928 2928 assert_difference 'Journal.count' do
2929 2929 put :update, :id => 1, :issue => {:status_id => 2, :notes => 'just trying'}
2930 2930 end
2931 2931 assert_equal 1, Issue.find(1).status_id
2932 2932 end
2933 2933
2934 2934 test "with workflow privilege and :edit_issues permission should accept authorized attributes changes" do
2935 2935 setup_with_workflow_privilege_and_edit_issues_permission
2936 2936 assert_difference 'Journal.count' do
2937 2937 put :update, :id => 1,
2938 2938 :issue => {:subject => 'changed', :assigned_to_id => 2,
2939 2939 :notes => 'just trying'}
2940 2940 end
2941 2941 issue = Issue.find(1)
2942 2942 assert_equal "changed", issue.subject
2943 2943 assert_equal 2, issue.assigned_to_id
2944 2944 end
2945 2945
2946 2946 def test_new_as_copy
2947 2947 @request.session[:user_id] = 2
2948 2948 get :new, :project_id => 1, :copy_from => 1
2949 2949
2950 2950 assert_response :success
2951 2951 assert_template 'new'
2952 2952
2953 2953 assert_not_nil assigns(:issue)
2954 2954 orig = Issue.find(1)
2955 2955 assert_equal 1, assigns(:issue).project_id
2956 2956 assert_equal orig.subject, assigns(:issue).subject
2957 2957 assert assigns(:issue).copy?
2958 2958
2959 2959 assert_select 'form[id=issue-form][action="/projects/ecookbook/issues"]' do
2960 2960 assert_select 'select[name=?]', 'issue[project_id]' do
2961 2961 assert_select 'option[value="1"][selected=selected]', :text => 'eCookbook'
2962 2962 assert_select 'option[value="2"]:not([selected])', :text => 'OnlineStore'
2963 2963 end
2964 2964 assert_select 'input[name=copy_from][value="1"]'
2965 2965 end
2966 2966 end
2967 2967
2968 2968 def test_new_as_copy_without_add_issues_permission_should_not_propose_current_project_as_target
2969 2969 user = setup_user_with_copy_but_not_add_permission
2970 2970
2971 2971 @request.session[:user_id] = user.id
2972 2972 get :new, :project_id => 1, :copy_from => 1
2973 2973
2974 2974 assert_response :success
2975 2975 assert_template 'new'
2976 2976 assert_select 'select[name=?]', 'issue[project_id]' do
2977 2977 assert_select 'option[value="1"]', 0
2978 2978 assert_select 'option[value="2"]', :text => 'OnlineStore'
2979 2979 end
2980 2980 end
2981 2981
2982 2982 def test_new_as_copy_with_attachments_should_show_copy_attachments_checkbox
2983 2983 @request.session[:user_id] = 2
2984 2984 issue = Issue.find(3)
2985 2985 assert issue.attachments.count > 0
2986 2986 get :new, :project_id => 1, :copy_from => 3
2987 2987
2988 2988 assert_select 'input[name=copy_attachments][type=checkbox][checked=checked][value="1"]'
2989 2989 end
2990 2990
2991 2991 def test_new_as_copy_without_attachments_should_not_show_copy_attachments_checkbox
2992 2992 @request.session[:user_id] = 2
2993 2993 issue = Issue.find(3)
2994 2994 issue.attachments.delete_all
2995 2995 get :new, :project_id => 1, :copy_from => 3
2996 2996
2997 2997 assert_select 'input[name=copy_attachments]', 0
2998 2998 end
2999 2999
3000 3000 def test_new_as_copy_should_preserve_parent_id
3001 3001 @request.session[:user_id] = 2
3002 3002 issue = Issue.generate!(:parent_issue_id => 2)
3003 3003 get :new, :project_id => 1, :copy_from => issue.id
3004 3004
3005 3005 assert_select 'input[name=?][value="2"]', 'issue[parent_issue_id]'
3006 3006 end
3007 3007
3008 3008 def test_new_as_copy_with_subtasks_should_show_copy_subtasks_checkbox
3009 3009 @request.session[:user_id] = 2
3010 3010 issue = Issue.generate_with_descendants!
3011 3011 get :new, :project_id => 1, :copy_from => issue.id
3012 3012
3013 3013 assert_select 'input[type=checkbox][name=copy_subtasks][checked=checked][value="1"]'
3014 3014 end
3015 3015
3016 3016 def test_new_as_copy_with_invalid_issue_should_respond_with_404
3017 3017 @request.session[:user_id] = 2
3018 3018 get :new, :project_id => 1, :copy_from => 99999
3019 3019 assert_response 404
3020 3020 end
3021 3021
3022 3022 def test_create_as_copy_on_different_project
3023 3023 @request.session[:user_id] = 2
3024 3024 assert_difference 'Issue.count' do
3025 3025 post :create, :project_id => 1, :copy_from => 1,
3026 3026 :issue => {:project_id => '2', :tracker_id => '3', :status_id => '1', :subject => 'Copy'}
3027 3027
3028 3028 assert_not_nil assigns(:issue)
3029 3029 assert assigns(:issue).copy?
3030 3030 end
3031 3031 issue = Issue.order('id DESC').first
3032 3032 assert_redirected_to "/issues/#{issue.id}"
3033 3033
3034 3034 assert_equal 2, issue.project_id
3035 3035 assert_equal 3, issue.tracker_id
3036 3036 assert_equal 'Copy', issue.subject
3037 3037 end
3038 3038
3039 3039 def test_create_as_copy_should_allow_status_to_be_set_to_default
3040 3040 copied = Issue.generate! :status_id => 2
3041 3041 assert_equal 2, copied.reload.status_id
3042 3042
3043 3043 @request.session[:user_id] = 2
3044 3044 assert_difference 'Issue.count' do
3045 3045 post :create, :project_id => 1, :copy_from => copied.id,
3046 3046 :issue => {:project_id => '1', :tracker_id => '1', :status_id => '1'},
3047 3047 :was_default_status => '1'
3048 3048 end
3049 3049 issue = Issue.order('id DESC').first
3050 3050 assert_equal 1, issue.status_id
3051 3051 end
3052 3052
3053 3053 def test_create_as_copy_should_copy_attachments
3054 3054 @request.session[:user_id] = 2
3055 3055 issue = Issue.find(3)
3056 3056 count = issue.attachments.count
3057 3057 assert count > 0
3058 3058 assert_difference 'Issue.count' do
3059 3059 assert_difference 'Attachment.count', count do
3060 3060 post :create, :project_id => 1, :copy_from => 3,
3061 3061 :issue => {:project_id => '1', :tracker_id => '3',
3062 3062 :status_id => '1', :subject => 'Copy with attachments'},
3063 3063 :copy_attachments => '1'
3064 3064 end
3065 3065 end
3066 3066 copy = Issue.order('id DESC').first
3067 3067 assert_equal count, copy.attachments.count
3068 3068 assert_equal issue.attachments.map(&:filename).sort, copy.attachments.map(&:filename).sort
3069 3069 end
3070 3070
3071 3071 def test_create_as_copy_without_copy_attachments_option_should_not_copy_attachments
3072 3072 @request.session[:user_id] = 2
3073 3073 issue = Issue.find(3)
3074 3074 count = issue.attachments.count
3075 3075 assert count > 0
3076 3076 assert_difference 'Issue.count' do
3077 3077 assert_no_difference 'Attachment.count' do
3078 3078 post :create, :project_id => 1, :copy_from => 3,
3079 3079 :issue => {:project_id => '1', :tracker_id => '3',
3080 3080 :status_id => '1', :subject => 'Copy with attachments'}
3081 3081 end
3082 3082 end
3083 3083 copy = Issue.order('id DESC').first
3084 3084 assert_equal 0, copy.attachments.count
3085 3085 end
3086 3086
3087 3087 def test_create_as_copy_with_attachments_should_also_add_new_files
3088 3088 @request.session[:user_id] = 2
3089 3089 issue = Issue.find(3)
3090 3090 count = issue.attachments.count
3091 3091 assert count > 0
3092 3092 assert_difference 'Issue.count' do
3093 3093 assert_difference 'Attachment.count', count + 1 do
3094 3094 post :create, :project_id => 1, :copy_from => 3,
3095 3095 :issue => {:project_id => '1', :tracker_id => '3',
3096 3096 :status_id => '1', :subject => 'Copy with attachments'},
3097 3097 :copy_attachments => '1',
3098 3098 :attachments => {'1' =>
3099 3099 {'file' => uploaded_test_file('testfile.txt', 'text/plain'),
3100 3100 'description' => 'test file'}}
3101 3101 end
3102 3102 end
3103 3103 copy = Issue.order('id DESC').first
3104 3104 assert_equal count + 1, copy.attachments.count
3105 3105 end
3106 3106
3107 3107 def test_create_as_copy_should_add_relation_with_copied_issue
3108 3108 @request.session[:user_id] = 2
3109 3109 assert_difference 'Issue.count' do
3110 3110 assert_difference 'IssueRelation.count' do
3111 3111 post :create, :project_id => 1, :copy_from => 1, :link_copy => '1',
3112 3112 :issue => {:project_id => '1', :tracker_id => '3',
3113 3113 :status_id => '1', :subject => 'Copy'}
3114 3114 end
3115 3115 end
3116 3116 copy = Issue.order('id DESC').first
3117 3117 assert_equal 1, copy.relations.size
3118 3118 end
3119 3119
3120 3120 def test_create_as_copy_should_allow_not_to_add_relation_with_copied_issue
3121 3121 @request.session[:user_id] = 2
3122 3122 assert_difference 'Issue.count' do
3123 3123 assert_no_difference 'IssueRelation.count' do
3124 3124 post :create, :project_id => 1, :copy_from => 1,
3125 3125 :issue => {:subject => 'Copy'}
3126 3126 end
3127 3127 end
3128 3128 end
3129 3129
3130 3130 def test_create_as_copy_should_always_add_relation_with_copied_issue_by_setting
3131 3131 with_settings :link_copied_issue => 'yes' do
3132 3132 @request.session[:user_id] = 2
3133 3133 assert_difference 'Issue.count' do
3134 3134 assert_difference 'IssueRelation.count' do
3135 3135 post :create, :project_id => 1, :copy_from => 1,
3136 3136 :issue => {:subject => 'Copy'}
3137 3137 end
3138 3138 end
3139 3139 end
3140 3140 end
3141 3141
3142 3142 def test_create_as_copy_should_never_add_relation_with_copied_issue_by_setting
3143 3143 with_settings :link_copied_issue => 'no' do
3144 3144 @request.session[:user_id] = 2
3145 3145 assert_difference 'Issue.count' do
3146 3146 assert_no_difference 'IssueRelation.count' do
3147 3147 post :create, :project_id => 1, :copy_from => 1, :link_copy => '1',
3148 3148 :issue => {:subject => 'Copy'}
3149 3149 end
3150 3150 end
3151 3151 end
3152 3152 end
3153 3153
3154 3154 def test_create_as_copy_should_copy_subtasks
3155 3155 @request.session[:user_id] = 2
3156 3156 issue = Issue.generate_with_descendants!
3157 3157 count = issue.descendants.count
3158 3158 assert_difference 'Issue.count', count + 1 do
3159 3159 post :create, :project_id => 1, :copy_from => issue.id,
3160 3160 :issue => {:project_id => '1', :tracker_id => '3',
3161 3161 :status_id => '1', :subject => 'Copy with subtasks'},
3162 3162 :copy_subtasks => '1'
3163 3163 end
3164 3164 copy = Issue.where(:parent_id => nil).order('id DESC').first
3165 3165 assert_equal count, copy.descendants.count
3166 3166 assert_equal issue.descendants.map(&:subject).sort, copy.descendants.map(&:subject).sort
3167 3167 end
3168 3168
3169 3169 def test_create_as_copy_to_a_different_project_should_copy_subtask_custom_fields
3170 3170 issue = Issue.generate! {|i| i.custom_field_values = {'2' => 'Foo'}}
3171 3171 child = Issue.generate!(:parent_issue_id => issue.id) {|i| i.custom_field_values = {'2' => 'Bar'}}
3172 3172 @request.session[:user_id] = 1
3173 3173
3174 3174 assert_difference 'Issue.count', 2 do
3175 3175 post :create, :project_id => 'ecookbook', :copy_from => issue.id,
3176 3176 :issue => {:project_id => '2', :tracker_id => 1, :status_id => '1',
3177 3177 :subject => 'Copy with subtasks', :custom_field_values => {'2' => 'Foo'}},
3178 3178 :copy_subtasks => '1'
3179 3179 end
3180 3180
3181 3181 child_copy, issue_copy = Issue.order(:id => :desc).limit(2).to_a
3182 3182 assert_equal 2, issue_copy.project_id
3183 3183 assert_equal 'Foo', issue_copy.custom_field_value(2)
3184 3184 assert_equal 'Bar', child_copy.custom_field_value(2)
3185 3185 end
3186 3186
3187 3187 def test_create_as_copy_without_copy_subtasks_option_should_not_copy_subtasks
3188 3188 @request.session[:user_id] = 2
3189 3189 issue = Issue.generate_with_descendants!
3190 3190 assert_difference 'Issue.count', 1 do
3191 3191 post :create, :project_id => 1, :copy_from => 3,
3192 3192 :issue => {:project_id => '1', :tracker_id => '3',
3193 3193 :status_id => '1', :subject => 'Copy with subtasks'}
3194 3194 end
3195 3195 copy = Issue.where(:parent_id => nil).order('id DESC').first
3196 3196 assert_equal 0, copy.descendants.count
3197 3197 end
3198 3198
3199 3199 def test_create_as_copy_with_failure
3200 3200 @request.session[:user_id] = 2
3201 3201 post :create, :project_id => 1, :copy_from => 1,
3202 3202 :issue => {:project_id => '2', :tracker_id => '3', :status_id => '1', :subject => ''}
3203 3203
3204 3204 assert_response :success
3205 3205 assert_template 'new'
3206 3206
3207 3207 assert_not_nil assigns(:issue)
3208 3208 assert assigns(:issue).copy?
3209 3209
3210 3210 assert_select 'form#issue-form[action="/projects/ecookbook/issues"]' do
3211 3211 assert_select 'select[name=?]', 'issue[project_id]' do
3212 3212 assert_select 'option[value="1"]:not([selected])', :text => 'eCookbook'
3213 3213 assert_select 'option[value="2"][selected=selected]', :text => 'OnlineStore'
3214 3214 end
3215 3215 assert_select 'input[name=copy_from][value="1"]'
3216 3216 end
3217 3217 end
3218 3218
3219 3219 def test_create_as_copy_on_project_without_permission_should_ignore_target_project
3220 3220 @request.session[:user_id] = 2
3221 3221 assert !User.find(2).member_of?(Project.find(4))
3222 3222
3223 3223 assert_difference 'Issue.count' do
3224 3224 post :create, :project_id => 1, :copy_from => 1,
3225 3225 :issue => {:project_id => '4', :tracker_id => '3', :status_id => '1', :subject => 'Copy'}
3226 3226 end
3227 3227 issue = Issue.order('id DESC').first
3228 3228 assert_equal 1, issue.project_id
3229 3229 end
3230 3230
3231 3231 def test_get_edit
3232 3232 @request.session[:user_id] = 2
3233 3233 get :edit, :id => 1
3234 3234 assert_response :success
3235 3235 assert_template 'edit'
3236 3236 assert_not_nil assigns(:issue)
3237 3237 assert_equal Issue.find(1), assigns(:issue)
3238 3238
3239 3239 # Be sure we don't display inactive IssuePriorities
3240 3240 assert ! IssuePriority.find(15).active?
3241 3241 assert_select 'select[name=?]', 'issue[priority_id]' do
3242 3242 assert_select 'option[value="15"]', 0
3243 3243 end
3244 3244 end
3245 3245
3246 3246 def test_get_edit_should_display_the_time_entry_form_with_log_time_permission
3247 3247 @request.session[:user_id] = 2
3248 3248 Role.find_by_name('Manager').update_attribute :permissions, [:view_issues, :edit_issues, :log_time]
3249 3249
3250 3250 get :edit, :id => 1
3251 3251 assert_select 'input[name=?]', 'time_entry[hours]'
3252 3252 end
3253 3253
3254 3254 def test_get_edit_should_not_display_the_time_entry_form_without_log_time_permission
3255 3255 @request.session[:user_id] = 2
3256 3256 Role.find_by_name('Manager').remove_permission! :log_time
3257 3257
3258 3258 get :edit, :id => 1
3259 3259 assert_select 'input[name=?]', 'time_entry[hours]', 0
3260 3260 end
3261 3261
3262 3262 def test_get_edit_with_params
3263 3263 @request.session[:user_id] = 2
3264 3264 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 },
3265 3265 :time_entry => { :hours => '2.5', :comments => 'test_get_edit_with_params', :activity_id => 10 }
3266 3266 assert_response :success
3267 3267 assert_template 'edit'
3268 3268
3269 3269 issue = assigns(:issue)
3270 3270 assert_not_nil issue
3271 3271
3272 3272 assert_equal 5, issue.status_id
3273 3273 assert_select 'select[name=?]', 'issue[status_id]' do
3274 3274 assert_select 'option[value="5"][selected=selected]', :text => 'Closed'
3275 3275 end
3276 3276
3277 3277 assert_equal 7, issue.priority_id
3278 3278 assert_select 'select[name=?]', 'issue[priority_id]' do
3279 3279 assert_select 'option[value="7"][selected=selected]', :text => 'Urgent'
3280 3280 end
3281 3281
3282 3282 assert_select 'input[name=?][value="2.5"]', 'time_entry[hours]'
3283 3283 assert_select 'select[name=?]', 'time_entry[activity_id]' do
3284 3284 assert_select 'option[value="10"][selected=selected]', :text => 'Development'
3285 3285 end
3286 3286 assert_select 'input[name=?][value=test_get_edit_with_params]', 'time_entry[comments]'
3287 3287 end
3288 3288
3289 3289 def test_get_edit_with_multi_custom_field
3290 3290 field = CustomField.find(1)
3291 3291 field.update_attribute :multiple, true
3292 3292 issue = Issue.find(1)
3293 3293 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
3294 3294 issue.save!
3295 3295
3296 3296 @request.session[:user_id] = 2
3297 3297 get :edit, :id => 1
3298 3298 assert_response :success
3299 3299 assert_template 'edit'
3300 3300
3301 3301 assert_select 'select[name=?][multiple=multiple]', 'issue[custom_field_values][1][]' do
3302 3302 assert_select 'option', 3
3303 3303 assert_select 'option[value=MySQL][selected=selected]'
3304 3304 assert_select 'option[value=Oracle][selected=selected]'
3305 3305 assert_select 'option[value=PostgreSQL]:not([selected])'
3306 3306 end
3307 3307 end
3308 3308
3309 3309 def test_update_form_for_existing_issue
3310 3310 @request.session[:user_id] = 2
3311 3311 xhr :patch, :edit, :id => 1,
3312 3312 :issue => {:tracker_id => 2,
3313 3313 :subject => 'This is the test_new issue',
3314 3314 :description => 'This is the description',
3315 3315 :priority_id => 5}
3316 3316 assert_response :success
3317 3317 assert_equal 'text/javascript', response.content_type
3318 3318 assert_template 'edit'
3319 3319 assert_template :partial => '_form'
3320 3320
3321 3321 issue = assigns(:issue)
3322 3322 assert_kind_of Issue, issue
3323 3323 assert_equal 1, issue.id
3324 3324 assert_equal 1, issue.project_id
3325 3325 assert_equal 2, issue.tracker_id
3326 3326 assert_equal 'This is the test_new issue', issue.subject
3327 3327 end
3328 3328
3329 3329 def test_update_form_for_existing_issue_should_keep_issue_author
3330 3330 @request.session[:user_id] = 3
3331 3331 xhr :patch, :edit, :id => 1, :issue => {:subject => 'Changed'}
3332 3332 assert_response :success
3333 3333 assert_equal 'text/javascript', response.content_type
3334 3334
3335 3335 issue = assigns(:issue)
3336 3336 assert_equal User.find(2), issue.author
3337 3337 assert_equal 2, issue.author_id
3338 3338 assert_not_equal User.current, issue.author
3339 3339 end
3340 3340
3341 3341 def test_update_form_for_existing_issue_should_propose_transitions_based_on_initial_status
3342 3342 @request.session[:user_id] = 2
3343 3343 WorkflowTransition.delete_all
3344 3344 WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 1)
3345 3345 WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 5)
3346 3346 WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 5, :new_status_id => 4)
3347 3347
3348 3348 xhr :patch, :edit, :id => 2,
3349 3349 :issue => {:tracker_id => 2,
3350 3350 :status_id => 5,
3351 3351 :subject => 'This is an issue'}
3352 3352
3353 3353 assert_equal 5, assigns(:issue).status_id
3354 3354 assert_equal [1,2,5], assigns(:allowed_statuses).map(&:id).sort
3355 3355 end
3356 3356
3357 3357 def test_update_form_for_existing_issue_with_project_change
3358 3358 @request.session[:user_id] = 2
3359 3359 xhr :patch, :edit, :id => 1,
3360 3360 :issue => {:project_id => 2,
3361 3361 :tracker_id => 2,
3362 3362 :subject => 'This is the test_new issue',
3363 3363 :description => 'This is the description',
3364 3364 :priority_id => 5}
3365 3365 assert_response :success
3366 3366 assert_template :partial => '_form'
3367 3367
3368 3368 issue = assigns(:issue)
3369 3369 assert_kind_of Issue, issue
3370 3370 assert_equal 1, issue.id
3371 3371 assert_equal 2, issue.project_id
3372 3372 assert_equal 2, issue.tracker_id
3373 3373 assert_equal 'This is the test_new issue', issue.subject
3374 3374 end
3375 3375
3376 3376 def test_update_form_should_keep_category_with_same_when_changing_project
3377 3377 source = Project.generate!
3378 3378 target = Project.generate!
3379 3379 source_category = IssueCategory.create!(:name => 'Foo', :project => source)
3380 3380 target_category = IssueCategory.create!(:name => 'Foo', :project => target)
3381 3381 issue = Issue.generate!(:project => source, :category => source_category)
3382 3382
3383 3383 @request.session[:user_id] = 1
3384 3384 patch :edit, :id => issue.id,
3385 3385 :issue => {:project_id => target.id, :category_id => source_category.id}
3386 3386 assert_response :success
3387 3387
3388 3388 issue = assigns(:issue)
3389 3389 assert_equal target_category, issue.category
3390 3390 end
3391 3391
3392 3392 def test_update_form_should_propose_default_status_for_existing_issue
3393 3393 @request.session[:user_id] = 2
3394 3394 WorkflowTransition.delete_all
3395 3395 WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 3)
3396 3396
3397 3397 xhr :patch, :edit, :id => 2
3398 3398 assert_response :success
3399 3399 assert_equal [2,3], assigns(:allowed_statuses).map(&:id).sort
3400 3400 end
3401 3401
3402 3402 def test_put_update_without_custom_fields_param
3403 3403 @request.session[:user_id] = 2
3404 3404
3405 3405 issue = Issue.find(1)
3406 3406 assert_equal '125', issue.custom_value_for(2).value
3407 3407
3408 3408 assert_difference('Journal.count') do
3409 3409 assert_difference('JournalDetail.count') do
3410 3410 put :update, :id => 1, :issue => {:subject => 'New subject'}
3411 3411 end
3412 3412 end
3413 3413 assert_redirected_to :action => 'show', :id => '1'
3414 3414 issue.reload
3415 3415 assert_equal 'New subject', issue.subject
3416 3416 # Make sure custom fields were not cleared
3417 3417 assert_equal '125', issue.custom_value_for(2).value
3418 3418 end
3419 3419
3420 3420 def test_put_update_with_project_change
3421 3421 @request.session[:user_id] = 2
3422 3422 ActionMailer::Base.deliveries.clear
3423 3423
3424 3424 with_settings :notified_events => %w(issue_updated) do
3425 3425 assert_difference('Journal.count') do
3426 3426 assert_difference('JournalDetail.count', 3) do
3427 3427 put :update, :id => 1, :issue => {:project_id => '2',
3428 3428 :tracker_id => '1', # no change
3429 3429 :priority_id => '6',
3430 3430 :category_id => '3'
3431 3431 }
3432 3432 end
3433 3433 end
3434 3434 end
3435 3435 assert_redirected_to :action => 'show', :id => '1'
3436 3436 issue = Issue.find(1)
3437 3437 assert_equal 2, issue.project_id
3438 3438 assert_equal 1, issue.tracker_id
3439 3439 assert_equal 6, issue.priority_id
3440 3440 assert_equal 3, issue.category_id
3441 3441
3442 3442 mail = ActionMailer::Base.deliveries.last
3443 3443 assert_not_nil mail
3444 3444 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
3445 3445 assert_mail_body_match "Project changed from eCookbook to OnlineStore", mail
3446 3446 end
3447 3447
3448 3448 def test_put_update_trying_to_move_issue_to_project_without_tracker_should_not_error
3449 3449 target = Project.generate!(:tracker_ids => [])
3450 3450 assert target.trackers.empty?
3451 3451 issue = Issue.generate!
3452 3452 @request.session[:user_id] = 1
3453 3453
3454 3454 put :update, :id => issue.id, :issue => {:project_id => target.id}
3455 3455 assert_response 302
3456 3456 end
3457 3457
3458 3458 def test_put_update_with_tracker_change
3459 3459 @request.session[:user_id] = 2
3460 3460 ActionMailer::Base.deliveries.clear
3461 3461
3462 3462 with_settings :notified_events => %w(issue_updated) do
3463 3463 assert_difference('Journal.count') do
3464 3464 assert_difference('JournalDetail.count', 2) do
3465 3465 put :update, :id => 1, :issue => {:project_id => '1',
3466 3466 :tracker_id => '2',
3467 3467 :priority_id => '6'
3468 3468 }
3469 3469 end
3470 3470 end
3471 3471 end
3472 3472 assert_redirected_to :action => 'show', :id => '1'
3473 3473 issue = Issue.find(1)
3474 3474 assert_equal 1, issue.project_id
3475 3475 assert_equal 2, issue.tracker_id
3476 3476 assert_equal 6, issue.priority_id
3477 3477 assert_equal 1, issue.category_id
3478 3478
3479 3479 mail = ActionMailer::Base.deliveries.last
3480 3480 assert_not_nil mail
3481 3481 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
3482 3482 assert_mail_body_match "Tracker changed from Bug to Feature request", mail
3483 3483 end
3484 3484
3485 3485 def test_put_update_with_custom_field_change
3486 3486 @request.session[:user_id] = 2
3487 3487 issue = Issue.find(1)
3488 3488 assert_equal '125', issue.custom_value_for(2).value
3489 3489
3490 3490 with_settings :notified_events => %w(issue_updated) do
3491 3491 assert_difference('Journal.count') do
3492 3492 assert_difference('JournalDetail.count', 3) do
3493 3493 put :update, :id => 1, :issue => {:subject => 'Custom field change',
3494 3494 :priority_id => '6',
3495 3495 :category_id => '1', # no change
3496 3496 :custom_field_values => { '2' => 'New custom value' }
3497 3497 }
3498 3498 end
3499 3499 end
3500 3500 end
3501 3501 assert_redirected_to :action => 'show', :id => '1'
3502 3502 issue.reload
3503 3503 assert_equal 'New custom value', issue.custom_value_for(2).value
3504 3504
3505 3505 mail = ActionMailer::Base.deliveries.last
3506 3506 assert_not_nil mail
3507 3507 assert_mail_body_match "Searchable field changed from 125 to New custom value", mail
3508 3508 end
3509 3509
3510 3510 def test_put_update_with_multi_custom_field_change
3511 3511 field = CustomField.find(1)
3512 3512 field.update_attribute :multiple, true
3513 3513 issue = Issue.find(1)
3514 3514 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
3515 3515 issue.save!
3516 3516
3517 3517 @request.session[:user_id] = 2
3518 3518 assert_difference('Journal.count') do
3519 3519 assert_difference('JournalDetail.count', 3) do
3520 3520 put :update, :id => 1,
3521 3521 :issue => {
3522 3522 :subject => 'Custom field change',
3523 3523 :custom_field_values => { '1' => ['', 'Oracle', 'PostgreSQL'] }
3524 3524 }
3525 3525 end
3526 3526 end
3527 3527 assert_redirected_to :action => 'show', :id => '1'
3528 3528 assert_equal ['Oracle', 'PostgreSQL'], Issue.find(1).custom_field_value(1).sort
3529 3529 end
3530 3530
3531 3531 def test_put_update_with_status_and_assignee_change
3532 3532 issue = Issue.find(1)
3533 3533 assert_equal 1, issue.status_id
3534 3534 @request.session[:user_id] = 2
3535 3535
3536 3536 with_settings :notified_events => %w(issue_updated) do
3537 3537 assert_difference('TimeEntry.count', 0) do
3538 3538 put :update,
3539 3539 :id => 1,
3540 3540 :issue => { :status_id => 2, :assigned_to_id => 3, :notes => 'Assigned to dlopper' },
3541 3541 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
3542 3542 end
3543 3543 end
3544 3544 assert_redirected_to :action => 'show', :id => '1'
3545 3545 issue.reload
3546 3546 assert_equal 2, issue.status_id
3547 3547 j = Journal.order('id DESC').first
3548 3548 assert_equal 'Assigned to dlopper', j.notes
3549 3549 assert_equal 2, j.details.size
3550 3550
3551 3551 mail = ActionMailer::Base.deliveries.last
3552 3552 assert_mail_body_match "Status changed from New to Assigned", mail
3553 3553 # subject should contain the new status
3554 3554 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
3555 3555 end
3556 3556
3557 3557 def test_put_update_with_note_only
3558 3558 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
3559 3559
3560 3560 with_settings :notified_events => %w(issue_updated) do
3561 3561 # anonymous user
3562 3562 put :update,
3563 3563 :id => 1,
3564 3564 :issue => { :notes => notes }
3565 3565 end
3566 3566 assert_redirected_to :action => 'show', :id => '1'
3567 3567 j = Journal.order('id DESC').first
3568 3568 assert_equal notes, j.notes
3569 3569 assert_equal 0, j.details.size
3570 3570 assert_equal User.anonymous, j.user
3571 3571
3572 3572 mail = ActionMailer::Base.deliveries.last
3573 3573 assert_mail_body_match notes, mail
3574 3574 end
3575 3575
3576 3576 def test_put_update_with_private_note_only
3577 3577 notes = 'Private note'
3578 3578 @request.session[:user_id] = 2
3579 3579
3580 3580 assert_difference 'Journal.count' do
3581 3581 put :update, :id => 1, :issue => {:notes => notes, :private_notes => '1'}
3582 3582 assert_redirected_to :action => 'show', :id => '1'
3583 3583 end
3584 3584
3585 3585 j = Journal.order('id DESC').first
3586 3586 assert_equal notes, j.notes
3587 3587 assert_equal true, j.private_notes
3588 3588 end
3589 3589
3590 3590 def test_put_update_with_private_note_and_changes
3591 3591 notes = 'Private note'
3592 3592 @request.session[:user_id] = 2
3593 3593
3594 3594 assert_difference 'Journal.count', 2 do
3595 3595 put :update, :id => 1, :issue => {:subject => 'New subject', :notes => notes, :private_notes => '1'}
3596 3596 assert_redirected_to :action => 'show', :id => '1'
3597 3597 end
3598 3598
3599 3599 j = Journal.order('id DESC').first
3600 3600 assert_equal notes, j.notes
3601 3601 assert_equal true, j.private_notes
3602 3602 assert_equal 0, j.details.count
3603 3603
3604 3604 j = Journal.order('id DESC').offset(1).first
3605 3605 assert_nil j.notes
3606 3606 assert_equal false, j.private_notes
3607 3607 assert_equal 1, j.details.count
3608 3608 end
3609 3609
3610 3610 def test_put_update_with_note_and_spent_time
3611 3611 @request.session[:user_id] = 2
3612 3612 spent_hours_before = Issue.find(1).spent_hours
3613 3613 assert_difference('TimeEntry.count') do
3614 3614 put :update,
3615 3615 :id => 1,
3616 3616 :issue => { :notes => '2.5 hours added' },
3617 3617 :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id }
3618 3618 end
3619 3619 assert_redirected_to :action => 'show', :id => '1'
3620 3620
3621 3621 issue = Issue.find(1)
3622 3622
3623 3623 j = Journal.order('id DESC').first
3624 3624 assert_equal '2.5 hours added', j.notes
3625 3625 assert_equal 0, j.details.size
3626 3626
3627 3627 t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time')
3628 3628 assert_not_nil t
3629 3629 assert_equal 2.5, t.hours
3630 3630 assert_equal spent_hours_before + 2.5, issue.spent_hours
3631 3631 end
3632 3632
3633 3633 def test_put_update_should_preserve_parent_issue_even_if_not_visible
3634 3634 parent = Issue.generate!(:project_id => 1, :is_private => true)
3635 3635 issue = Issue.generate!(:parent_issue_id => parent.id)
3636 3636 assert !parent.visible?(User.find(3))
3637 3637 @request.session[:user_id] = 3
3638 3638
3639 3639 get :edit, :id => issue.id
3640 3640 assert_select 'input[name=?][value=?]', 'issue[parent_issue_id]', parent.id.to_s
3641 3641
3642 3642 put :update, :id => issue.id, :issue => {:subject => 'New subject', :parent_issue_id => parent.id.to_s}
3643 3643 assert_response 302
3644 3644 assert_equal parent, issue.parent
3645 3645 end
3646 3646
3647 3647 def test_put_update_with_attachment_only
3648 3648 set_tmp_attachments_directory
3649 3649
3650 3650 # Delete all fixtured journals, a race condition can occur causing the wrong
3651 3651 # journal to get fetched in the next find.
3652 3652 Journal.delete_all
3653 3653
3654 3654 with_settings :notified_events => %w(issue_updated) do
3655 3655 # anonymous user
3656 3656 assert_difference 'Attachment.count' do
3657 3657 put :update, :id => 1,
3658 3658 :issue => {:notes => ''},
3659 3659 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
3660 3660 end
3661 3661 end
3662 3662
3663 3663 assert_redirected_to :action => 'show', :id => '1'
3664 3664 j = Issue.find(1).journals.reorder('id DESC').first
3665 3665 assert j.notes.blank?
3666 3666 assert_equal 1, j.details.size
3667 3667 assert_equal 'testfile.txt', j.details.first.value
3668 3668 assert_equal User.anonymous, j.user
3669 3669
3670 3670 attachment = Attachment.order('id DESC').first
3671 3671 assert_equal Issue.find(1), attachment.container
3672 3672 assert_equal User.anonymous, attachment.author
3673 3673 assert_equal 'testfile.txt', attachment.filename
3674 3674 assert_equal 'text/plain', attachment.content_type
3675 3675 assert_equal 'test file', attachment.description
3676 3676 assert_equal 59, attachment.filesize
3677 3677 assert File.exists?(attachment.diskfile)
3678 3678 assert_equal 59, File.size(attachment.diskfile)
3679 3679
3680 3680 mail = ActionMailer::Base.deliveries.last
3681 3681 assert_mail_body_match 'testfile.txt', mail
3682 3682 end
3683 3683
3684 3684 def test_put_update_with_failure_should_save_attachments
3685 3685 set_tmp_attachments_directory
3686 3686 @request.session[:user_id] = 2
3687 3687
3688 3688 assert_no_difference 'Journal.count' do
3689 3689 assert_difference 'Attachment.count' do
3690 3690 put :update, :id => 1,
3691 3691 :issue => { :subject => '' },
3692 3692 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
3693 3693 assert_response :success
3694 3694 assert_template 'edit'
3695 3695 end
3696 3696 end
3697 3697
3698 3698 attachment = Attachment.order('id DESC').first
3699 3699 assert_equal 'testfile.txt', attachment.filename
3700 3700 assert File.exists?(attachment.diskfile)
3701 3701 assert_nil attachment.container
3702 3702
3703 3703 assert_select 'input[name=?][value=?]', 'attachments[p0][token]', attachment.token
3704 3704 assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'testfile.txt'
3705 3705 end
3706 3706
3707 3707 def test_put_update_with_failure_should_keep_saved_attachments
3708 3708 set_tmp_attachments_directory
3709 3709 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
3710 3710 @request.session[:user_id] = 2
3711 3711
3712 3712 assert_no_difference 'Journal.count' do
3713 3713 assert_no_difference 'Attachment.count' do
3714 3714 put :update, :id => 1,
3715 3715 :issue => { :subject => '' },
3716 3716 :attachments => {'p0' => {'token' => attachment.token}}
3717 3717 assert_response :success
3718 3718 assert_template 'edit'
3719 3719 end
3720 3720 end
3721 3721
3722 3722 assert_select 'input[name=?][value=?]', 'attachments[p0][token]', attachment.token
3723 3723 assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'testfile.txt'
3724 3724 end
3725 3725
3726 3726 def test_put_update_should_attach_saved_attachments
3727 3727 set_tmp_attachments_directory
3728 3728 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
3729 3729 @request.session[:user_id] = 2
3730 3730
3731 3731 assert_difference 'Journal.count' do
3732 3732 assert_difference 'JournalDetail.count' do
3733 3733 assert_no_difference 'Attachment.count' do
3734 3734 put :update, :id => 1,
3735 3735 :issue => {:notes => 'Attachment added'},
3736 3736 :attachments => {'p0' => {'token' => attachment.token}}
3737 3737 assert_redirected_to '/issues/1'
3738 3738 end
3739 3739 end
3740 3740 end
3741 3741
3742 3742 attachment.reload
3743 3743 assert_equal Issue.find(1), attachment.container
3744 3744
3745 3745 journal = Journal.order('id DESC').first
3746 3746 assert_equal 1, journal.details.size
3747 3747 assert_equal 'testfile.txt', journal.details.first.value
3748 3748 end
3749 3749
3750 3750 def test_put_update_with_attachment_that_fails_to_save
3751 3751 set_tmp_attachments_directory
3752 3752
3753 3753 # anonymous user
3754 3754 with_settings :attachment_max_size => 0 do
3755 3755 put :update,
3756 3756 :id => 1,
3757 3757 :issue => {:notes => ''},
3758 3758 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
3759 3759 assert_redirected_to :action => 'show', :id => '1'
3760 3760 assert_equal '1 file(s) could not be saved.', flash[:warning]
3761 3761 end
3762 3762 end
3763 3763
3764 def test_put_update_with_attachment_deletion_should_create_a_single_journal
3765 set_tmp_attachments_directory
3766 @request.session[:user_id] = 2
3767
3768 journal = new_record(Journal) do
3769 assert_difference 'Attachment.count', -2 do
3770 put :update,
3771 :id => 3,
3772 :issue => {
3773 :notes => 'Removing attachments',
3774 :deleted_attachment_ids => ['1', '5']
3775 }
3776 end
3777 end
3778 assert_equal 'Removing attachments', journal.notes
3779 assert_equal 2, journal.details.count
3780 end
3781
3782 def test_put_update_with_attachment_deletion_and_failure_should_preserve_selected_attachments
3783 set_tmp_attachments_directory
3784 @request.session[:user_id] = 2
3785
3786 assert_no_difference 'Journal.count' do
3787 assert_no_difference 'Attachment.count' do
3788 put :update,
3789 :id => 3,
3790 :issue => {
3791 :subject => '',
3792 :notes => 'Removing attachments',
3793 :deleted_attachment_ids => ['1', '5']
3794 }
3795 end
3796 end
3797 assert_select 'input[name=?][value="1"][checked=checked]', 'issue[deleted_attachment_ids][]'
3798 assert_select 'input[name=?][value="5"][checked=checked]', 'issue[deleted_attachment_ids][]'
3799 assert_select 'input[name=?][value="6"]:not([checked])', 'issue[deleted_attachment_ids][]'
3800 end
3801
3764 3802 def test_put_update_with_no_change
3765 3803 issue = Issue.find(1)
3766 3804 issue.journals.clear
3767 3805 ActionMailer::Base.deliveries.clear
3768 3806
3769 3807 put :update,
3770 3808 :id => 1,
3771 3809 :issue => {:notes => ''}
3772 3810 assert_redirected_to :action => 'show', :id => '1'
3773 3811
3774 3812 issue.reload
3775 3813 assert issue.journals.empty?
3776 3814 # No email should be sent
3777 3815 assert ActionMailer::Base.deliveries.empty?
3778 3816 end
3779 3817
3780 3818 def test_put_update_should_send_a_notification
3781 3819 @request.session[:user_id] = 2
3782 3820 ActionMailer::Base.deliveries.clear
3783 3821 issue = Issue.find(1)
3784 3822 old_subject = issue.subject
3785 3823 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
3786 3824
3787 3825 with_settings :notified_events => %w(issue_updated) do
3788 3826 put :update, :id => 1, :issue => {:subject => new_subject,
3789 3827 :priority_id => '6',
3790 3828 :category_id => '1' # no change
3791 3829 }
3792 3830 assert_equal 1, ActionMailer::Base.deliveries.size
3793 3831 end
3794 3832 end
3795 3833
3796 3834 def test_put_update_with_invalid_spent_time_hours_only
3797 3835 @request.session[:user_id] = 2
3798 3836 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
3799 3837
3800 3838 assert_no_difference('Journal.count') do
3801 3839 put :update,
3802 3840 :id => 1,
3803 3841 :issue => {:notes => notes},
3804 3842 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
3805 3843 end
3806 3844 assert_response :success
3807 3845 assert_template 'edit'
3808 3846
3809 3847 assert_select_error /Activity cannot be blank/
3810 3848 assert_select 'textarea[name=?]', 'issue[notes]', :text => notes
3811 3849 assert_select 'input[name=?][value=?]', 'time_entry[hours]', '2z'
3812 3850 end
3813 3851
3814 3852 def test_put_update_with_invalid_spent_time_comments_only
3815 3853 @request.session[:user_id] = 2
3816 3854 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
3817 3855
3818 3856 assert_no_difference('Journal.count') do
3819 3857 put :update,
3820 3858 :id => 1,
3821 3859 :issue => {:notes => notes},
3822 3860 :time_entry => {"comments"=>"this is my comment", "activity_id"=>"", "hours"=>""}
3823 3861 end
3824 3862 assert_response :success
3825 3863 assert_template 'edit'
3826 3864
3827 3865 assert_select_error /Activity cannot be blank/
3828 3866 assert_select_error /Hours cannot be blank/
3829 3867 assert_select 'textarea[name=?]', 'issue[notes]', :text => notes
3830 3868 assert_select 'input[name=?][value=?]', 'time_entry[comments]', 'this is my comment'
3831 3869 end
3832 3870
3833 3871 def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject
3834 3872 issue = Issue.find(2)
3835 3873 @request.session[:user_id] = 2
3836 3874
3837 3875 put :update,
3838 3876 :id => issue.id,
3839 3877 :issue => {
3840 3878 :fixed_version_id => 4
3841 3879 }
3842 3880
3843 3881 assert_response :redirect
3844 3882 issue.reload
3845 3883 assert_equal 4, issue.fixed_version_id
3846 3884 assert_not_equal issue.project_id, issue.fixed_version.project_id
3847 3885 end
3848 3886
3849 3887 def test_put_update_should_redirect_back_using_the_back_url_parameter
3850 3888 issue = Issue.find(2)
3851 3889 @request.session[:user_id] = 2
3852 3890
3853 3891 put :update,
3854 3892 :id => issue.id,
3855 3893 :issue => {
3856 3894 :fixed_version_id => 4
3857 3895 },
3858 3896 :back_url => '/issues'
3859 3897
3860 3898 assert_response :redirect
3861 3899 assert_redirected_to '/issues'
3862 3900 end
3863 3901
3864 3902 def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
3865 3903 issue = Issue.find(2)
3866 3904 @request.session[:user_id] = 2
3867 3905
3868 3906 put :update,
3869 3907 :id => issue.id,
3870 3908 :issue => {
3871 3909 :fixed_version_id => 4
3872 3910 },
3873 3911 :back_url => 'http://google.com'
3874 3912
3875 3913 assert_response :redirect
3876 3914 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
3877 3915 end
3878 3916
3879 3917 def test_put_update_should_redirect_with_previous_and_next_issue_ids_params
3880 3918 @request.session[:user_id] = 2
3881 3919
3882 3920 put :update, :id => 11,
3883 3921 :issue => {:status_id => 6, :notes => 'Notes'},
3884 3922 :prev_issue_id => 8,
3885 3923 :next_issue_id => 12,
3886 3924 :issue_position => 2,
3887 3925 :issue_count => 3
3888 3926
3889 3927 assert_redirected_to '/issues/11?issue_count=3&issue_position=2&next_issue_id=12&prev_issue_id=8'
3890 3928 end
3891 3929
3892 3930 def test_update_with_permission_on_tracker_should_be_allowed
3893 3931 role = Role.find(1)
3894 3932 role.set_permission_trackers :edit_issues, [1]
3895 3933 role.save!
3896 3934 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :subject => 'Original subject')
3897 3935
3898 3936 @request.session[:user_id] = 2
3899 3937 put :update, :id => issue.id, :issue => {:subject => 'Changed subject'}
3900 3938 assert_response 302
3901 3939 assert_equal 'Changed subject', issue.reload.subject
3902 3940 end
3903 3941
3904 3942 def test_update_without_permission_on_tracker_should_be_denied
3905 3943 role = Role.find(1)
3906 3944 role.set_permission_trackers :edit_issues, [1]
3907 3945 role.save!
3908 3946 issue = Issue.generate!(:project_id => 1, :tracker_id => 2, :subject => 'Original subject')
3909 3947
3910 3948 @request.session[:user_id] = 2
3911 3949 put :update, :id => issue.id, :issue => {:subject => 'Changed subject'}
3912 3950 assert_response 302
3913 3951 assert_equal 'Original subject', issue.reload.subject
3914 3952 end
3915 3953
3916 3954 def test_get_bulk_edit
3917 3955 @request.session[:user_id] = 2
3918 3956 get :bulk_edit, :ids => [1, 3]
3919 3957 assert_response :success
3920 3958 assert_template 'bulk_edit'
3921 3959
3922 3960 assert_select 'ul#bulk-selection' do
3923 3961 assert_select 'li', 2
3924 3962 assert_select 'li a', :text => 'Bug #1'
3925 3963 end
3926 3964
3927 3965 assert_select 'form#bulk_edit_form[action=?]', '/issues/bulk_update' do
3928 3966 assert_select 'input[name=?]', 'ids[]', 2
3929 3967 assert_select 'input[name=?][value="1"][type=hidden]', 'ids[]'
3930 3968
3931 3969 assert_select 'select[name=?]', 'issue[project_id]'
3932 3970 assert_select 'input[name=?]', 'issue[parent_issue_id]'
3933 3971
3934 3972 # Project specific custom field, date type
3935 3973 field = CustomField.find(9)
3936 3974 assert !field.is_for_all?
3937 3975 assert_equal 'date', field.field_format
3938 3976 assert_select 'input[name=?]', 'issue[custom_field_values][9]'
3939 3977
3940 3978 # System wide custom field
3941 3979 assert CustomField.find(1).is_for_all?
3942 3980 assert_select 'select[name=?]', 'issue[custom_field_values][1]'
3943 3981
3944 3982 # Be sure we don't display inactive IssuePriorities
3945 3983 assert ! IssuePriority.find(15).active?
3946 3984 assert_select 'select[name=?]', 'issue[priority_id]' do
3947 3985 assert_select 'option[value="15"]', 0
3948 3986 end
3949 3987 end
3950 3988 end
3951 3989
3952 3990 def test_get_bulk_edit_on_different_projects
3953 3991 @request.session[:user_id] = 2
3954 3992 get :bulk_edit, :ids => [1, 2, 6]
3955 3993 assert_response :success
3956 3994 assert_template 'bulk_edit'
3957 3995
3958 3996 # Can not set issues from different projects as children of an issue
3959 3997 assert_select 'input[name=?]', 'issue[parent_issue_id]', 0
3960 3998
3961 3999 # Project specific custom field, date type
3962 4000 field = CustomField.find(9)
3963 4001 assert !field.is_for_all?
3964 4002 assert !field.project_ids.include?(Issue.find(6).project_id)
3965 4003 assert_select 'input[name=?]', 'issue[custom_field_values][9]', 0
3966 4004 end
3967 4005
3968 4006 def test_get_bulk_edit_with_user_custom_field
3969 4007 field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user', :is_for_all => true, :tracker_ids => [1,2,3])
3970 4008
3971 4009 @request.session[:user_id] = 2
3972 4010 get :bulk_edit, :ids => [1, 2]
3973 4011 assert_response :success
3974 4012 assert_template 'bulk_edit'
3975 4013
3976 4014 assert_select 'select.user_cf[name=?]', "issue[custom_field_values][#{field.id}]" do
3977 4015 assert_select 'option', Project.find(1).users.count + 2 # "no change" + "none" options
3978 4016 end
3979 4017 end
3980 4018
3981 4019 def test_get_bulk_edit_with_version_custom_field
3982 4020 field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true, :tracker_ids => [1,2,3])
3983 4021
3984 4022 @request.session[:user_id] = 2
3985 4023 get :bulk_edit, :ids => [1, 2]
3986 4024 assert_response :success
3987 4025 assert_template 'bulk_edit'
3988 4026
3989 4027 assert_select 'select.version_cf[name=?]', "issue[custom_field_values][#{field.id}]" do
3990 4028 assert_select 'option', Project.find(1).shared_versions.count + 2 # "no change" + "none" options
3991 4029 end
3992 4030 end
3993 4031
3994 4032 def test_get_bulk_edit_with_multi_custom_field
3995 4033 field = CustomField.find(1)
3996 4034 field.update_attribute :multiple, true
3997 4035
3998 4036 @request.session[:user_id] = 2
3999 4037 get :bulk_edit, :ids => [1, 3]
4000 4038 assert_response :success
4001 4039 assert_template 'bulk_edit'
4002 4040
4003 4041 assert_select 'select[name=?]', 'issue[custom_field_values][1][]' do
4004 4042 assert_select 'option', field.possible_values.size + 1 # "none" options
4005 4043 end
4006 4044 end
4007 4045
4008 4046 def test_bulk_edit_should_propose_to_clear_text_custom_fields
4009 4047 @request.session[:user_id] = 2
4010 4048 get :bulk_edit, :ids => [1, 3]
4011 4049 assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', '__none__'
4012 4050 end
4013 4051
4014 4052 def test_bulk_edit_should_only_propose_statuses_allowed_for_all_issues
4015 4053 WorkflowTransition.delete_all
4016 4054 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
4017 4055 :old_status_id => 1, :new_status_id => 1)
4018 4056 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
4019 4057 :old_status_id => 1, :new_status_id => 3)
4020 4058 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
4021 4059 :old_status_id => 1, :new_status_id => 4)
4022 4060 WorkflowTransition.create!(:role_id => 1, :tracker_id => 2,
4023 4061 :old_status_id => 2, :new_status_id => 1)
4024 4062 WorkflowTransition.create!(:role_id => 1, :tracker_id => 2,
4025 4063 :old_status_id => 2, :new_status_id => 3)
4026 4064 WorkflowTransition.create!(:role_id => 1, :tracker_id => 2,
4027 4065 :old_status_id => 2, :new_status_id => 5)
4028 4066 @request.session[:user_id] = 2
4029 4067 get :bulk_edit, :ids => [1, 2]
4030 4068
4031 4069 assert_response :success
4032 4070 statuses = assigns(:available_statuses)
4033 4071 assert_not_nil statuses
4034 4072 assert_equal [1, 3], statuses.map(&:id).sort
4035 4073
4036 4074 assert_select 'select[name=?]', 'issue[status_id]' do
4037 4075 assert_select 'option', 3 # 2 statuses + "no change" option
4038 4076 end
4039 4077 end
4040 4078
4041 4079 def test_bulk_edit_should_propose_target_project_open_shared_versions
4042 4080 @request.session[:user_id] = 2
4043 4081 post :bulk_edit, :ids => [1, 2, 6], :issue => {:project_id => 1}
4044 4082 assert_response :success
4045 4083 assert_template 'bulk_edit'
4046 4084 assert_equal Project.find(1).shared_versions.open.to_a.sort, assigns(:versions).sort
4047 4085
4048 4086 assert_select 'select[name=?]', 'issue[fixed_version_id]' do
4049 4087 assert_select 'option', :text => '2.0'
4050 4088 end
4051 4089 end
4052 4090
4053 4091 def test_bulk_edit_should_propose_target_project_categories
4054 4092 @request.session[:user_id] = 2
4055 4093 post :bulk_edit, :ids => [1, 2, 6], :issue => {:project_id => 1}
4056 4094 assert_response :success
4057 4095 assert_template 'bulk_edit'
4058 4096 assert_equal Project.find(1).issue_categories.sort, assigns(:categories).sort
4059 4097
4060 4098 assert_select 'select[name=?]', 'issue[category_id]' do
4061 4099 assert_select 'option', :text => 'Recipes'
4062 4100 end
4063 4101 end
4064 4102
4065 4103 def test_bulk_edit_should_only_propose_issues_trackers_custom_fields
4066 4104 IssueCustomField.delete_all
4067 4105 field = IssueCustomField.generate!(:tracker_ids => [1], :is_for_all => true)
4068 4106 IssueCustomField.generate!(:tracker_ids => [2], :is_for_all => true)
4069 4107 @request.session[:user_id] = 2
4070 4108
4071 4109 issue_ids = Issue.where(:project_id => 1, :tracker_id => 1).limit(2).ids
4072 4110 get :bulk_edit, :ids => issue_ids
4073 4111 assert_equal [field], assigns(:custom_fields)
4074 4112 end
4075 4113
4076 4114 def test_bulk_update
4077 4115 @request.session[:user_id] = 2
4078 4116 # update issues priority
4079 4117 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
4080 4118 :issue => {:priority_id => 7,
4081 4119 :assigned_to_id => '',
4082 4120 :custom_field_values => {'2' => ''}}
4083 4121
4084 4122 assert_response 302
4085 4123 # check that the issues were updated
4086 4124 assert_equal [7, 7], Issue.where(:id =>[1, 2]).collect {|i| i.priority.id}
4087 4125
4088 4126 issue = Issue.find(1)
4089 4127 journal = issue.journals.reorder('created_on DESC').first
4090 4128 assert_equal '125', issue.custom_value_for(2).value
4091 4129 assert_equal 'Bulk editing', journal.notes
4092 4130 assert_equal 1, journal.details.size
4093 4131 end
4094 4132
4095 4133 def test_bulk_update_with_group_assignee
4096 4134 group = Group.find(11)
4097 4135 project = Project.find(1)
4098 4136 project.members << Member.new(:principal => group, :roles => [Role.givable.first])
4099 4137
4100 4138 @request.session[:user_id] = 2
4101 4139 # update issues assignee
4102 4140 with_settings :issue_group_assignment => '1' do
4103 4141 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
4104 4142 :issue => {:priority_id => '',
4105 4143 :assigned_to_id => group.id,
4106 4144 :custom_field_values => {'2' => ''}}
4107 4145
4108 4146 assert_response 302
4109 4147 assert_equal [group, group], Issue.where(:id => [1, 2]).collect {|i| i.assigned_to}
4110 4148 end
4111 4149 end
4112 4150
4113 4151 def test_bulk_update_on_different_projects
4114 4152 @request.session[:user_id] = 2
4115 4153 # update issues priority
4116 4154 post :bulk_update, :ids => [1, 2, 6], :notes => 'Bulk editing',
4117 4155 :issue => {:priority_id => 7,
4118 4156 :assigned_to_id => '',
4119 4157 :custom_field_values => {'2' => ''}}
4120 4158
4121 4159 assert_response 302
4122 4160 # check that the issues were updated
4123 4161 assert_equal [7, 7, 7], Issue.find([1,2,6]).map(&:priority_id)
4124 4162
4125 4163 issue = Issue.find(1)
4126 4164 journal = issue.journals.reorder('created_on DESC').first
4127 4165 assert_equal '125', issue.custom_value_for(2).value
4128 4166 assert_equal 'Bulk editing', journal.notes
4129 4167 assert_equal 1, journal.details.size
4130 4168 end
4131 4169
4132 4170 def test_bulk_update_on_different_projects_without_rights
4133 4171 @request.session[:user_id] = 3
4134 4172 user = User.find(3)
4135 4173 action = { :controller => "issues", :action => "bulk_update" }
4136 4174 assert user.allowed_to?(action, Issue.find(1).project)
4137 4175 assert ! user.allowed_to?(action, Issue.find(6).project)
4138 4176 post :bulk_update, :ids => [1, 6], :notes => 'Bulk should fail',
4139 4177 :issue => {:priority_id => 7,
4140 4178 :assigned_to_id => '',
4141 4179 :custom_field_values => {'2' => ''}}
4142 4180 assert_response 403
4143 4181 assert_not_equal "Bulk should fail", Journal.last.notes
4144 4182 end
4145 4183
4146 4184 def test_bullk_update_should_send_a_notification
4147 4185 @request.session[:user_id] = 2
4148 4186 ActionMailer::Base.deliveries.clear
4149 4187 with_settings :notified_events => %w(issue_updated) do
4150 4188 post(:bulk_update,
4151 4189 {
4152 4190 :ids => [1, 2],
4153 4191 :notes => 'Bulk editing',
4154 4192 :issue => {
4155 4193 :priority_id => 7,
4156 4194 :assigned_to_id => '',
4157 4195 :custom_field_values => {'2' => ''}
4158 4196 }
4159 4197 })
4160 4198 assert_response 302
4161 4199 assert_equal 2, ActionMailer::Base.deliveries.size
4162 4200 end
4163 4201 end
4164 4202
4165 4203 def test_bulk_update_project
4166 4204 @request.session[:user_id] = 2
4167 4205 post :bulk_update, :ids => [1, 2], :issue => {:project_id => '2'}
4168 4206 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
4169 4207 # Issues moved to project 2
4170 4208 assert_equal 2, Issue.find(1).project_id
4171 4209 assert_equal 2, Issue.find(2).project_id
4172 4210 # No tracker change
4173 4211 assert_equal 1, Issue.find(1).tracker_id
4174 4212 assert_equal 2, Issue.find(2).tracker_id
4175 4213 end
4176 4214
4177 4215 def test_bulk_update_project_on_single_issue_should_follow_when_needed
4178 4216 @request.session[:user_id] = 2
4179 4217 post :bulk_update, :id => 1, :issue => {:project_id => '2'}, :follow => '1'
4180 4218 assert_redirected_to '/issues/1'
4181 4219 end
4182 4220
4183 4221 def test_bulk_update_project_on_multiple_issues_should_follow_when_needed
4184 4222 @request.session[:user_id] = 2
4185 4223 post :bulk_update, :id => [1, 2], :issue => {:project_id => '2'}, :follow => '1'
4186 4224 assert_redirected_to '/projects/onlinestore/issues'
4187 4225 end
4188 4226
4189 4227 def test_bulk_update_tracker
4190 4228 @request.session[:user_id] = 2
4191 4229 post :bulk_update, :ids => [1, 2], :issue => {:tracker_id => '2'}
4192 4230 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
4193 4231 assert_equal 2, Issue.find(1).tracker_id
4194 4232 assert_equal 2, Issue.find(2).tracker_id
4195 4233 end
4196 4234
4197 4235 def test_bulk_update_status
4198 4236 @request.session[:user_id] = 2
4199 4237 # update issues priority
4200 4238 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status',
4201 4239 :issue => {:priority_id => '',
4202 4240 :assigned_to_id => '',
4203 4241 :status_id => '5'}
4204 4242
4205 4243 assert_response 302
4206 4244 issue = Issue.find(1)
4207 4245 assert issue.closed?
4208 4246 end
4209 4247
4210 4248 def test_bulk_update_priority
4211 4249 @request.session[:user_id] = 2
4212 4250 post :bulk_update, :ids => [1, 2], :issue => {:priority_id => 6}
4213 4251
4214 4252 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
4215 4253 assert_equal 6, Issue.find(1).priority_id
4216 4254 assert_equal 6, Issue.find(2).priority_id
4217 4255 end
4218 4256
4219 4257 def test_bulk_update_with_notes
4220 4258 @request.session[:user_id] = 2
4221 4259 post :bulk_update, :ids => [1, 2], :notes => 'Moving two issues'
4222 4260
4223 4261 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
4224 4262 assert_equal 'Moving two issues', Issue.find(1).journals.sort_by(&:id).last.notes
4225 4263 assert_equal 'Moving two issues', Issue.find(2).journals.sort_by(&:id).last.notes
4226 4264 end
4227 4265
4228 4266 def test_bulk_update_parent_id
4229 4267 IssueRelation.delete_all
4230 4268 @request.session[:user_id] = 2
4231 4269 post :bulk_update, :ids => [1, 3],
4232 4270 :notes => 'Bulk editing parent',
4233 4271 :issue => {:priority_id => '', :assigned_to_id => '',
4234 4272 :status_id => '', :parent_issue_id => '2'}
4235 4273 assert_response 302
4236 4274 parent = Issue.find(2)
4237 4275 assert_equal parent.id, Issue.find(1).parent_id
4238 4276 assert_equal parent.id, Issue.find(3).parent_id
4239 4277 assert_equal [1, 3], parent.children.collect(&:id).sort
4240 4278 end
4241 4279
4242 4280 def test_bulk_update_estimated_hours
4243 4281 @request.session[:user_id] = 2
4244 4282 post :bulk_update, :ids => [1, 2], :issue => {:estimated_hours => 4.25}
4245 4283
4246 4284 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
4247 4285 assert_equal 4.25, Issue.find(1).estimated_hours
4248 4286 assert_equal 4.25, Issue.find(2).estimated_hours
4249 4287 end
4250 4288
4251 4289 def test_bulk_update_custom_field
4252 4290 @request.session[:user_id] = 2
4253 4291 # update issues priority
4254 4292 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field',
4255 4293 :issue => {:priority_id => '',
4256 4294 :assigned_to_id => '',
4257 4295 :custom_field_values => {'2' => '777'}}
4258 4296
4259 4297 assert_response 302
4260 4298
4261 4299 issue = Issue.find(1)
4262 4300 journal = issue.journals.reorder('created_on DESC').first
4263 4301 assert_equal '777', issue.custom_value_for(2).value
4264 4302 assert_equal 1, journal.details.size
4265 4303 assert_equal '125', journal.details.first.old_value
4266 4304 assert_equal '777', journal.details.first.value
4267 4305 end
4268 4306
4269 4307 def test_bulk_update_custom_field_to_blank
4270 4308 @request.session[:user_id] = 2
4271 4309 post :bulk_update, :ids => [1, 3], :notes => 'Bulk editing custom field',
4272 4310 :issue => {:priority_id => '',
4273 4311 :assigned_to_id => '',
4274 4312 :custom_field_values => {'1' => '__none__'}}
4275 4313 assert_response 302
4276 4314 assert_equal '', Issue.find(1).custom_field_value(1)
4277 4315 assert_equal '', Issue.find(3).custom_field_value(1)
4278 4316 end
4279 4317
4280 4318 def test_bulk_update_multi_custom_field
4281 4319 field = CustomField.find(1)
4282 4320 field.update_attribute :multiple, true
4283 4321
4284 4322 @request.session[:user_id] = 2
4285 4323 post :bulk_update, :ids => [1, 2, 3], :notes => 'Bulk editing multi custom field',
4286 4324 :issue => {:priority_id => '',
4287 4325 :assigned_to_id => '',
4288 4326 :custom_field_values => {'1' => ['MySQL', 'Oracle']}}
4289 4327
4290 4328 assert_response 302
4291 4329
4292 4330 assert_equal ['MySQL', 'Oracle'], Issue.find(1).custom_field_value(1).sort
4293 4331 assert_equal ['MySQL', 'Oracle'], Issue.find(3).custom_field_value(1).sort
4294 4332 # the custom field is not associated with the issue tracker
4295 4333 assert_nil Issue.find(2).custom_field_value(1)
4296 4334 end
4297 4335
4298 4336 def test_bulk_update_multi_custom_field_to_blank
4299 4337 field = CustomField.find(1)
4300 4338 field.update_attribute :multiple, true
4301 4339
4302 4340 @request.session[:user_id] = 2
4303 4341 post :bulk_update, :ids => [1, 3], :notes => 'Bulk editing multi custom field',
4304 4342 :issue => {:priority_id => '',
4305 4343 :assigned_to_id => '',
4306 4344 :custom_field_values => {'1' => ['__none__']}}
4307 4345 assert_response 302
4308 4346 assert_equal [''], Issue.find(1).custom_field_value(1)
4309 4347 assert_equal [''], Issue.find(3).custom_field_value(1)
4310 4348 end
4311 4349
4312 4350 def test_bulk_update_unassign
4313 4351 assert_not_nil Issue.find(2).assigned_to
4314 4352 @request.session[:user_id] = 2
4315 4353 # unassign issues
4316 4354 post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'}
4317 4355 assert_response 302
4318 4356 # check that the issues were updated
4319 4357 assert_nil Issue.find(2).assigned_to
4320 4358 end
4321 4359
4322 4360 def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject
4323 4361 @request.session[:user_id] = 2
4324 4362
4325 4363 post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4}
4326 4364
4327 4365 assert_response :redirect
4328 4366 issues = Issue.find([1,2])
4329 4367 issues.each do |issue|
4330 4368 assert_equal 4, issue.fixed_version_id
4331 4369 assert_not_equal issue.project_id, issue.fixed_version.project_id
4332 4370 end
4333 4371 end
4334 4372
4335 4373 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
4336 4374 @request.session[:user_id] = 2
4337 4375 post :bulk_update, :ids => [1,2], :back_url => '/issues'
4338 4376
4339 4377 assert_response :redirect
4340 4378 assert_redirected_to '/issues'
4341 4379 end
4342 4380
4343 4381 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
4344 4382 @request.session[:user_id] = 2
4345 4383 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
4346 4384
4347 4385 assert_response :redirect
4348 4386 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
4349 4387 end
4350 4388
4351 4389 def test_bulk_update_with_all_failures_should_show_errors
4352 4390 @request.session[:user_id] = 2
4353 4391 post :bulk_update, :ids => [1, 2], :issue => {:start_date => 'foo'}
4354 4392
4355 4393 assert_response :success
4356 4394 assert_template 'bulk_edit'
4357 4395 assert_select '#errorExplanation span', :text => 'Failed to save 2 issue(s) on 2 selected: #1, #2.'
4358 4396 assert_select '#errorExplanation ul li', :text => 'Start date is not a valid date: #1, #2'
4359 4397
4360 4398 assert_equal [1, 2], assigns[:issues].map(&:id)
4361 4399 end
4362 4400
4363 4401 def test_bulk_update_with_some_failures_should_show_errors
4364 4402 issue1 = Issue.generate!(:start_date => '2013-05-12')
4365 4403 issue2 = Issue.generate!(:start_date => '2013-05-15')
4366 4404 issue3 = Issue.generate!
4367 4405 @request.session[:user_id] = 2
4368 4406 post :bulk_update, :ids => [issue1.id, issue2.id, issue3.id],
4369 4407 :issue => {:due_date => '2013-05-01'}
4370 4408 assert_response :success
4371 4409 assert_template 'bulk_edit'
4372 4410 assert_select '#errorExplanation span',
4373 4411 :text => "Failed to save 2 issue(s) on 3 selected: ##{issue1.id}, ##{issue2.id}."
4374 4412 assert_select '#errorExplanation ul li',
4375 4413 :text => "Due date must be greater than start date: ##{issue1.id}, ##{issue2.id}"
4376 4414 assert_equal [issue1.id, issue2.id], assigns[:issues].map(&:id)
4377 4415 end
4378 4416
4379 4417 def test_bulk_update_with_failure_should_preserved_form_values
4380 4418 @request.session[:user_id] = 2
4381 4419 post :bulk_update, :ids => [1, 2], :issue => {:tracker_id => '2', :start_date => 'foo'}
4382 4420
4383 4421 assert_response :success
4384 4422 assert_template 'bulk_edit'
4385 4423 assert_select 'select[name=?]', 'issue[tracker_id]' do
4386 4424 assert_select 'option[value="2"][selected=selected]'
4387 4425 end
4388 4426 assert_select 'input[name=?][value=?]', 'issue[start_date]', 'foo'
4389 4427 end
4390 4428
4391 4429 def test_get_bulk_copy
4392 4430 @request.session[:user_id] = 2
4393 4431 get :bulk_edit, :ids => [1, 2, 3], :copy => '1'
4394 4432 assert_response :success
4395 4433 assert_template 'bulk_edit'
4396 4434
4397 4435 issues = assigns(:issues)
4398 4436 assert_not_nil issues
4399 4437 assert_equal [1, 2, 3], issues.map(&:id).sort
4400 4438
4401 4439 assert_select 'select[name=?]', 'issue[project_id]' do
4402 4440 assert_select 'option[value=""]'
4403 4441 end
4404 4442 assert_select 'input[name=copy_attachments]'
4405 4443 end
4406 4444
4407 4445 def test_get_bulk_copy_without_add_issues_permission_should_not_propose_current_project_as_target
4408 4446 user = setup_user_with_copy_but_not_add_permission
4409 4447 @request.session[:user_id] = user.id
4410 4448
4411 4449 get :bulk_edit, :ids => [1, 2, 3], :copy => '1'
4412 4450 assert_response :success
4413 4451 assert_template 'bulk_edit'
4414 4452
4415 4453 assert_select 'select[name=?]', 'issue[project_id]' do
4416 4454 assert_select 'option[value=""]', 0
4417 4455 assert_select 'option[value="2"]'
4418 4456 end
4419 4457 end
4420 4458
4421 4459 def test_bulk_copy_to_another_project
4422 4460 @request.session[:user_id] = 2
4423 4461 assert_difference 'Issue.count', 2 do
4424 4462 assert_no_difference 'Project.find(1).issues.count' do
4425 4463 post :bulk_update, :ids => [1, 2], :issue => {:project_id => '2'}, :copy => '1'
4426 4464 end
4427 4465 end
4428 4466 assert_redirected_to '/projects/ecookbook/issues'
4429 4467
4430 4468 copies = Issue.order('id DESC').limit(issues.size)
4431 4469 copies.each do |copy|
4432 4470 assert_equal 2, copy.project_id
4433 4471 end
4434 4472 end
4435 4473
4436 4474 def test_bulk_copy_without_add_issues_permission_should_be_allowed_on_project_with_permission
4437 4475 user = setup_user_with_copy_but_not_add_permission
4438 4476 @request.session[:user_id] = user.id
4439 4477
4440 4478 assert_difference 'Issue.count', 3 do
4441 4479 post :bulk_update, :ids => [1, 2, 3], :issue => {:project_id => '2'}, :copy => '1'
4442 4480 assert_response 302
4443 4481 end
4444 4482 end
4445 4483
4446 4484 def test_bulk_copy_on_same_project_without_add_issues_permission_should_be_denied
4447 4485 user = setup_user_with_copy_but_not_add_permission
4448 4486 @request.session[:user_id] = user.id
4449 4487
4450 4488 post :bulk_update, :ids => [1, 2, 3], :issue => {:project_id => ''}, :copy => '1'
4451 4489 assert_response 403
4452 4490 end
4453 4491
4454 4492 def test_bulk_copy_on_different_project_without_add_issues_permission_should_be_denied
4455 4493 user = setup_user_with_copy_but_not_add_permission
4456 4494 @request.session[:user_id] = user.id
4457 4495
4458 4496 post :bulk_update, :ids => [1, 2, 3], :issue => {:project_id => '1'}, :copy => '1'
4459 4497 assert_response 403
4460 4498 end
4461 4499
4462 4500 def test_bulk_copy_should_allow_not_changing_the_issue_attributes
4463 4501 @request.session[:user_id] = 2
4464 4502 issues = [
4465 4503 Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 1,
4466 4504 :priority_id => 2, :subject => 'issue 1', :author_id => 1,
4467 4505 :assigned_to_id => nil),
4468 4506 Issue.create!(:project_id => 2, :tracker_id => 3, :status_id => 2,
4469 4507 :priority_id => 1, :subject => 'issue 2', :author_id => 2,
4470 4508 :assigned_to_id => 3)
4471 4509 ]
4472 4510 assert_difference 'Issue.count', issues.size do
4473 4511 post :bulk_update, :ids => issues.map(&:id), :copy => '1',
4474 4512 :issue => {
4475 4513 :project_id => '', :tracker_id => '', :assigned_to_id => '',
4476 4514 :status_id => '', :start_date => '', :due_date => ''
4477 4515 }
4478 4516 end
4479 4517
4480 4518 copies = Issue.order('id DESC').limit(issues.size)
4481 4519 issues.each do |orig|
4482 4520 copy = copies.detect {|c| c.subject == orig.subject}
4483 4521 assert_not_nil copy
4484 4522 assert_equal orig.project_id, copy.project_id
4485 4523 assert_equal orig.tracker_id, copy.tracker_id
4486 4524 assert_equal orig.status_id, copy.status_id
4487 4525 assert_equal orig.assigned_to_id, copy.assigned_to_id
4488 4526 assert_equal orig.priority_id, copy.priority_id
4489 4527 end
4490 4528 end
4491 4529
4492 4530 def test_bulk_copy_should_allow_changing_the_issue_attributes
4493 4531 # Fixes random test failure with Mysql
4494 4532 # where Issue.where(:project_id => 2).limit(2).order('id desc')
4495 4533 # doesn't return the expected results
4496 4534 Issue.delete_all("project_id=2")
4497 4535
4498 4536 @request.session[:user_id] = 2
4499 4537 assert_difference 'Issue.count', 2 do
4500 4538 assert_no_difference 'Project.find(1).issues.count' do
4501 4539 post :bulk_update, :ids => [1, 2], :copy => '1',
4502 4540 :issue => {
4503 4541 :project_id => '2', :tracker_id => '', :assigned_to_id => '2',
4504 4542 :status_id => '1', :start_date => '2009-12-01', :due_date => '2009-12-31'
4505 4543 }
4506 4544 end
4507 4545 end
4508 4546
4509 4547 copied_issues = Issue.where(:project_id => 2).limit(2).order('id desc').to_a
4510 4548 assert_equal 2, copied_issues.size
4511 4549 copied_issues.each do |issue|
4512 4550 assert_equal 2, issue.project_id, "Project is incorrect"
4513 4551 assert_equal 2, issue.assigned_to_id, "Assigned to is incorrect"
4514 4552 assert_equal 1, issue.status_id, "Status is incorrect"
4515 4553 assert_equal '2009-12-01', issue.start_date.to_s, "Start date is incorrect"
4516 4554 assert_equal '2009-12-31', issue.due_date.to_s, "Due date is incorrect"
4517 4555 end
4518 4556 end
4519 4557
4520 4558 def test_bulk_copy_should_allow_adding_a_note
4521 4559 @request.session[:user_id] = 2
4522 4560 assert_difference 'Issue.count', 1 do
4523 4561 post :bulk_update, :ids => [1], :copy => '1',
4524 4562 :notes => 'Copying one issue',
4525 4563 :issue => {
4526 4564 :project_id => '', :tracker_id => '', :assigned_to_id => '4',
4527 4565 :status_id => '3', :start_date => '2009-12-01', :due_date => '2009-12-31'
4528 4566 }
4529 4567 end
4530 4568 issue = Issue.order('id DESC').first
4531 4569 assert_equal 1, issue.journals.size
4532 4570 journal = issue.journals.first
4533 4571 assert_equal 'Copying one issue', journal.notes
4534 4572 end
4535 4573
4536 4574 def test_bulk_copy_should_allow_not_copying_the_attachments
4537 4575 attachment_count = Issue.find(3).attachments.size
4538 4576 assert attachment_count > 0
4539 4577 @request.session[:user_id] = 2
4540 4578
4541 4579 assert_difference 'Issue.count', 1 do
4542 4580 assert_no_difference 'Attachment.count' do
4543 4581 post :bulk_update, :ids => [3], :copy => '1', :copy_attachments => '0',
4544 4582 :issue => {
4545 4583 :project_id => ''
4546 4584 }
4547 4585 end
4548 4586 end
4549 4587 end
4550 4588
4551 4589 def test_bulk_copy_should_allow_copying_the_attachments
4552 4590 attachment_count = Issue.find(3).attachments.size
4553 4591 assert attachment_count > 0
4554 4592 @request.session[:user_id] = 2
4555 4593
4556 4594 assert_difference 'Issue.count', 1 do
4557 4595 assert_difference 'Attachment.count', attachment_count do
4558 4596 post :bulk_update, :ids => [3], :copy => '1', :copy_attachments => '1',
4559 4597 :issue => {
4560 4598 :project_id => ''
4561 4599 }
4562 4600 end
4563 4601 end
4564 4602 end
4565 4603
4566 4604 def test_bulk_copy_should_add_relations_with_copied_issues
4567 4605 @request.session[:user_id] = 2
4568 4606
4569 4607 assert_difference 'Issue.count', 2 do
4570 4608 assert_difference 'IssueRelation.count', 2 do
4571 4609 post :bulk_update, :ids => [1, 3], :copy => '1', :link_copy => '1',
4572 4610 :issue => {
4573 4611 :project_id => '1'
4574 4612 }
4575 4613 end
4576 4614 end
4577 4615 end
4578 4616
4579 4617 def test_bulk_copy_should_allow_not_copying_the_subtasks
4580 4618 issue = Issue.generate_with_descendants!
4581 4619 @request.session[:user_id] = 2
4582 4620
4583 4621 assert_difference 'Issue.count', 1 do
4584 4622 post :bulk_update, :ids => [issue.id], :copy => '1', :copy_subtasks => '0',
4585 4623 :issue => {
4586 4624 :project_id => ''
4587 4625 }
4588 4626 end
4589 4627 end
4590 4628
4591 4629 def test_bulk_copy_should_allow_copying_the_subtasks
4592 4630 issue = Issue.generate_with_descendants!
4593 4631 count = issue.descendants.count
4594 4632 @request.session[:user_id] = 2
4595 4633
4596 4634 assert_difference 'Issue.count', count+1 do
4597 4635 post :bulk_update, :ids => [issue.id], :copy => '1', :copy_subtasks => '1',
4598 4636 :issue => {
4599 4637 :project_id => ''
4600 4638 }
4601 4639 end
4602 4640 copy = Issue.where(:parent_id => nil).order("id DESC").first
4603 4641 assert_equal count, copy.descendants.count
4604 4642 end
4605 4643
4606 4644 def test_bulk_copy_should_not_copy_selected_subtasks_twice
4607 4645 issue = Issue.generate_with_descendants!
4608 4646 count = issue.descendants.count
4609 4647 @request.session[:user_id] = 2
4610 4648
4611 4649 assert_difference 'Issue.count', count+1 do
4612 4650 post :bulk_update, :ids => issue.self_and_descendants.map(&:id), :copy => '1', :copy_subtasks => '1',
4613 4651 :issue => {
4614 4652 :project_id => ''
4615 4653 }
4616 4654 end
4617 4655 copy = Issue.where(:parent_id => nil).order("id DESC").first
4618 4656 assert_equal count, copy.descendants.count
4619 4657 end
4620 4658
4621 4659 def test_bulk_copy_to_another_project_should_follow_when_needed
4622 4660 @request.session[:user_id] = 2
4623 4661 post :bulk_update, :ids => [1], :copy => '1', :issue => {:project_id => 2}, :follow => '1'
4624 4662 issue = Issue.order('id DESC').first
4625 4663 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
4626 4664 end
4627 4665
4628 4666 def test_bulk_copy_with_all_failures_should_display_errors
4629 4667 @request.session[:user_id] = 2
4630 4668 post :bulk_update, :ids => [1, 2], :copy => '1', :issue => {:start_date => 'foo'}
4631 4669
4632 4670 assert_response :success
4633 4671 end
4634 4672
4635 4673 def test_destroy_issue_with_no_time_entries
4636 4674 assert_nil TimeEntry.find_by_issue_id(2)
4637 4675 @request.session[:user_id] = 2
4638 4676
4639 4677 assert_difference 'Issue.count', -1 do
4640 4678 delete :destroy, :id => 2
4641 4679 end
4642 4680 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
4643 4681 assert_nil Issue.find_by_id(2)
4644 4682 end
4645 4683
4646 4684 def test_destroy_issues_with_time_entries
4647 4685 @request.session[:user_id] = 2
4648 4686
4649 4687 assert_no_difference 'Issue.count' do
4650 4688 delete :destroy, :ids => [1, 3]
4651 4689 end
4652 4690 assert_response :success
4653 4691 assert_template 'destroy'
4654 4692 assert_not_nil assigns(:hours)
4655 4693 assert Issue.find_by_id(1) && Issue.find_by_id(3)
4656 4694
4657 4695 assert_select 'form' do
4658 4696 assert_select 'input[name=_method][value=delete]'
4659 4697 end
4660 4698 end
4661 4699
4662 4700 def test_destroy_issues_and_destroy_time_entries
4663 4701 @request.session[:user_id] = 2
4664 4702
4665 4703 assert_difference 'Issue.count', -2 do
4666 4704 assert_difference 'TimeEntry.count', -3 do
4667 4705 delete :destroy, :ids => [1, 3], :todo => 'destroy'
4668 4706 end
4669 4707 end
4670 4708 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
4671 4709 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
4672 4710 assert_nil TimeEntry.find_by_id([1, 2])
4673 4711 end
4674 4712
4675 4713 def test_destroy_issues_and_assign_time_entries_to_project
4676 4714 @request.session[:user_id] = 2
4677 4715
4678 4716 assert_difference 'Issue.count', -2 do
4679 4717 assert_no_difference 'TimeEntry.count' do
4680 4718 delete :destroy, :ids => [1, 3], :todo => 'nullify'
4681 4719 end
4682 4720 end
4683 4721 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
4684 4722 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
4685 4723 assert_nil TimeEntry.find(1).issue_id
4686 4724 assert_nil TimeEntry.find(2).issue_id
4687 4725 end
4688 4726
4689 4727 def test_destroy_issues_and_reassign_time_entries_to_another_issue
4690 4728 @request.session[:user_id] = 2
4691 4729
4692 4730 assert_difference 'Issue.count', -2 do
4693 4731 assert_no_difference 'TimeEntry.count' do
4694 4732 delete :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
4695 4733 end
4696 4734 end
4697 4735 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
4698 4736 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
4699 4737 assert_equal 2, TimeEntry.find(1).issue_id
4700 4738 assert_equal 2, TimeEntry.find(2).issue_id
4701 4739 end
4702 4740
4703 4741 def test_destroy_issues_and_reassign_time_entries_to_an_invalid_issue_should_fail
4704 4742 @request.session[:user_id] = 2
4705 4743
4706 4744 assert_no_difference 'Issue.count' do
4707 4745 assert_no_difference 'TimeEntry.count' do
4708 4746 # try to reassign time to an issue of another project
4709 4747 delete :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 4
4710 4748 end
4711 4749 end
4712 4750 assert_response :success
4713 4751 assert_template 'destroy'
4714 4752 end
4715 4753
4716 4754 def test_destroy_issues_from_different_projects
4717 4755 @request.session[:user_id] = 2
4718 4756
4719 4757 assert_difference 'Issue.count', -3 do
4720 4758 delete :destroy, :ids => [1, 2, 6], :todo => 'destroy'
4721 4759 end
4722 4760 assert_redirected_to :controller => 'issues', :action => 'index'
4723 4761 assert !(Issue.find_by_id(1) || Issue.find_by_id(2) || Issue.find_by_id(6))
4724 4762 end
4725 4763
4726 4764 def test_destroy_parent_and_child_issues
4727 4765 parent = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'Parent Issue')
4728 4766 child = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'Child Issue', :parent_issue_id => parent.id)
4729 4767 assert child.is_descendant_of?(parent.reload)
4730 4768
4731 4769 @request.session[:user_id] = 2
4732 4770 assert_difference 'Issue.count', -2 do
4733 4771 delete :destroy, :ids => [parent.id, child.id], :todo => 'destroy'
4734 4772 end
4735 4773 assert_response 302
4736 4774 end
4737 4775
4738 4776 def test_destroy_invalid_should_respond_with_404
4739 4777 @request.session[:user_id] = 2
4740 4778 assert_no_difference 'Issue.count' do
4741 4779 delete :destroy, :id => 999
4742 4780 end
4743 4781 assert_response 404
4744 4782 end
4745 4783
4746 4784 def test_destroy_with_permission_on_tracker_should_be_allowed
4747 4785 role = Role.find(1)
4748 4786 role.set_permission_trackers :delete_issues, [1]
4749 4787 role.save!
4750 4788 issue = Issue.generate!(:project_id => 1, :tracker_id => 1)
4751 4789
4752 4790 @request.session[:user_id] = 2
4753 4791 assert_difference 'Issue.count', -1 do
4754 4792 delete :destroy, :id => issue.id
4755 4793 end
4756 4794 assert_response 302
4757 4795 end
4758 4796
4759 4797 def test_destroy_without_permission_on_tracker_should_be_denied
4760 4798 role = Role.find(1)
4761 4799 role.set_permission_trackers :delete_issues, [2]
4762 4800 role.save!
4763 4801 issue = Issue.generate!(:project_id => 1, :tracker_id => 1)
4764 4802
4765 4803 @request.session[:user_id] = 2
4766 4804 assert_no_difference 'Issue.count' do
4767 4805 delete :destroy, :id => issue.id
4768 4806 end
4769 4807 assert_response 403
4770 4808 end
4771 4809
4772 4810 def test_default_search_scope
4773 4811 get :index
4774 4812
4775 4813 assert_select 'div#quick-search form' do
4776 4814 assert_select 'input[name=issues][value="1"][type=hidden]'
4777 4815 end
4778 4816 end
4779 4817
4780 4818 def setup_user_with_copy_but_not_add_permission
4781 4819 Role.all.each {|r| r.remove_permission! :add_issues}
4782 4820 Role.find_by_name('Manager').add_permission! :add_issues
4783 4821 user = User.generate!
4784 4822 User.add_to_project(user, Project.find(1), Role.find_by_name('Developer'))
4785 4823 User.add_to_project(user, Project.find(2), Role.find_by_name('Manager'))
4786 4824 user
4787 4825 end
4788 4826 end
General Comments 0
You need to be logged in to leave comments. Login now