##// END OF EJS Templates
Fixed: User with groups may not see issues assigned to him or to its groups (#9478)....
Jean-Philippe Lang -
r7651:857cf5db38c2
parent child
Show More
@@ -1,960 +1,960
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Issue < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20
21 21 belongs_to :project
22 22 belongs_to :tracker
23 23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
24 24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
25 25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
26 26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
27 27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
28 28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
29 29
30 30 has_many :journals, :as => :journalized, :dependent => :destroy
31 31 has_many :time_entries, :dependent => :delete_all
32 32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
33 33
34 34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
35 35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36 36
37 37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
38 38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
39 39 acts_as_customizable
40 40 acts_as_watchable
41 41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
42 42 :include => [:project, :journals],
43 43 # sort by id so that limited eager loading doesn't break with postgresql
44 44 :order_column => "#{table_name}.id"
45 45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
46 46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
47 47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
48 48
49 49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
50 50 :author_key => :author_id
51 51
52 52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
53 53
54 54 attr_reader :current_journal
55 55
56 56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
57 57
58 58 validates_length_of :subject, :maximum => 255
59 59 validates_inclusion_of :done_ratio, :in => 0..100
60 60 validates_numericality_of :estimated_hours, :allow_nil => true
61 61 validate :validate_issue
62 62
63 63 named_scope :visible, lambda {|*args| { :include => :project,
64 64 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
65 65
66 66 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
67 67
68 68 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
69 69 named_scope :with_limit, lambda { |limit| { :limit => limit} }
70 70 named_scope :on_active_project, :include => [:status, :project, :tracker],
71 71 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
72 72
73 73 named_scope :without_version, lambda {
74 74 {
75 75 :conditions => { :fixed_version_id => nil}
76 76 }
77 77 }
78 78
79 79 named_scope :with_query, lambda {|query|
80 80 {
81 81 :conditions => Query.merge_conditions(query.statement)
82 82 }
83 83 }
84 84
85 85 before_create :default_assign
86 86 before_save :close_duplicates, :update_done_ratio_from_issue_status
87 87 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
88 88 after_destroy :update_parent_attributes
89 89
90 90 # Returns a SQL conditions string used to find all issues visible by the specified user
91 91 def self.visible_condition(user, options={})
92 92 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
93 93 case role.issues_visibility
94 94 when 'all'
95 95 nil
96 96 when 'default'
97 97 user_ids = [user.id] + user.groups.map(&:id)
98 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids}))"
98 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
99 99 when 'own'
100 100 user_ids = [user.id] + user.groups.map(&:id)
101 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids}))"
101 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
102 102 else
103 103 '1=0'
104 104 end
105 105 end
106 106 end
107 107
108 108 # Returns true if usr or current user is allowed to view the issue
109 109 def visible?(usr=nil)
110 110 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
111 111 case role.issues_visibility
112 112 when 'all'
113 113 true
114 114 when 'default'
115 115 !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
116 116 when 'own'
117 117 self.author == user || user.is_or_belongs_to?(assigned_to)
118 118 else
119 119 false
120 120 end
121 121 end
122 122 end
123 123
124 124 def after_initialize
125 125 if new_record?
126 126 # set default values for new records only
127 127 self.status ||= IssueStatus.default
128 128 self.priority ||= IssuePriority.default
129 129 end
130 130 end
131 131
132 132 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
133 133 def available_custom_fields
134 134 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
135 135 end
136 136
137 137 def copy_from(arg)
138 138 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
139 139 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
140 140 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
141 141 self.status = issue.status
142 142 self
143 143 end
144 144
145 145 # Moves/copies an issue to a new project and tracker
146 146 # Returns the moved/copied issue on success, false on failure
147 147 def move_to_project(*args)
148 148 ret = Issue.transaction do
149 149 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
150 150 end || false
151 151 end
152 152
153 153 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
154 154 options ||= {}
155 155 issue = options[:copy] ? self.class.new.copy_from(self) : self
156 156
157 157 if new_project && issue.project_id != new_project.id
158 158 # delete issue relations
159 159 unless Setting.cross_project_issue_relations?
160 160 issue.relations_from.clear
161 161 issue.relations_to.clear
162 162 end
163 163 # issue is moved to another project
164 164 # reassign to the category with same name if any
165 165 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
166 166 issue.category = new_category
167 167 # Keep the fixed_version if it's still valid in the new_project
168 168 unless new_project.shared_versions.include?(issue.fixed_version)
169 169 issue.fixed_version = nil
170 170 end
171 171 issue.project = new_project
172 172 if issue.parent && issue.parent.project_id != issue.project_id
173 173 issue.parent_issue_id = nil
174 174 end
175 175 end
176 176 if new_tracker
177 177 issue.tracker = new_tracker
178 178 issue.reset_custom_values!
179 179 end
180 180 if options[:copy]
181 181 issue.author = User.current
182 182 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
183 183 issue.status = if options[:attributes] && options[:attributes][:status_id]
184 184 IssueStatus.find_by_id(options[:attributes][:status_id])
185 185 else
186 186 self.status
187 187 end
188 188 end
189 189 # Allow bulk setting of attributes on the issue
190 190 if options[:attributes]
191 191 issue.attributes = options[:attributes]
192 192 end
193 193 if issue.save
194 194 if options[:copy]
195 195 if current_journal && current_journal.notes.present?
196 196 issue.init_journal(current_journal.user, current_journal.notes)
197 197 issue.current_journal.notify = false
198 198 issue.save
199 199 end
200 200 else
201 201 # Manually update project_id on related time entries
202 202 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
203 203
204 204 issue.children.each do |child|
205 205 unless child.move_to_project_without_transaction(new_project)
206 206 # Move failed and transaction was rollback'd
207 207 return false
208 208 end
209 209 end
210 210 end
211 211 else
212 212 return false
213 213 end
214 214 issue
215 215 end
216 216
217 217 def status_id=(sid)
218 218 self.status = nil
219 219 write_attribute(:status_id, sid)
220 220 end
221 221
222 222 def priority_id=(pid)
223 223 self.priority = nil
224 224 write_attribute(:priority_id, pid)
225 225 end
226 226
227 227 def tracker_id=(tid)
228 228 self.tracker = nil
229 229 result = write_attribute(:tracker_id, tid)
230 230 @custom_field_values = nil
231 231 result
232 232 end
233 233
234 234 def description=(arg)
235 235 if arg.is_a?(String)
236 236 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
237 237 end
238 238 write_attribute(:description, arg)
239 239 end
240 240
241 241 # Overrides attributes= so that tracker_id gets assigned first
242 242 def attributes_with_tracker_first=(new_attributes, *args)
243 243 return if new_attributes.nil?
244 244 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
245 245 if new_tracker_id
246 246 self.tracker_id = new_tracker_id
247 247 end
248 248 send :attributes_without_tracker_first=, new_attributes, *args
249 249 end
250 250 # Do not redefine alias chain on reload (see #4838)
251 251 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
252 252
253 253 def estimated_hours=(h)
254 254 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
255 255 end
256 256
257 257 safe_attributes 'tracker_id',
258 258 'status_id',
259 259 'parent_issue_id',
260 260 'category_id',
261 261 'assigned_to_id',
262 262 'priority_id',
263 263 'fixed_version_id',
264 264 'subject',
265 265 'description',
266 266 'start_date',
267 267 'due_date',
268 268 'done_ratio',
269 269 'estimated_hours',
270 270 'custom_field_values',
271 271 'custom_fields',
272 272 'lock_version',
273 273 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
274 274
275 275 safe_attributes 'status_id',
276 276 'assigned_to_id',
277 277 'fixed_version_id',
278 278 'done_ratio',
279 279 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
280 280
281 281 safe_attributes 'is_private',
282 282 :if => lambda {|issue, user|
283 283 user.allowed_to?(:set_issues_private, issue.project) ||
284 284 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
285 285 }
286 286
287 287 # Safely sets attributes
288 288 # Should be called from controllers instead of #attributes=
289 289 # attr_accessible is too rough because we still want things like
290 290 # Issue.new(:project => foo) to work
291 291 # TODO: move workflow/permission checks from controllers to here
292 292 def safe_attributes=(attrs, user=User.current)
293 293 return unless attrs.is_a?(Hash)
294 294
295 295 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
296 296 attrs = delete_unsafe_attributes(attrs, user)
297 297 return if attrs.empty?
298 298
299 299 # Tracker must be set before since new_statuses_allowed_to depends on it.
300 300 if t = attrs.delete('tracker_id')
301 301 self.tracker_id = t
302 302 end
303 303
304 304 if attrs['status_id']
305 305 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
306 306 attrs.delete('status_id')
307 307 end
308 308 end
309 309
310 310 unless leaf?
311 311 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
312 312 end
313 313
314 314 if attrs.has_key?('parent_issue_id')
315 315 if !user.allowed_to?(:manage_subtasks, project)
316 316 attrs.delete('parent_issue_id')
317 317 elsif !attrs['parent_issue_id'].blank?
318 318 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
319 319 end
320 320 end
321 321
322 322 self.attributes = attrs
323 323 end
324 324
325 325 def done_ratio
326 326 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
327 327 status.default_done_ratio
328 328 else
329 329 read_attribute(:done_ratio)
330 330 end
331 331 end
332 332
333 333 def self.use_status_for_done_ratio?
334 334 Setting.issue_done_ratio == 'issue_status'
335 335 end
336 336
337 337 def self.use_field_for_done_ratio?
338 338 Setting.issue_done_ratio == 'issue_field'
339 339 end
340 340
341 341 def validate_issue
342 342 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
343 343 errors.add :due_date, :not_a_date
344 344 end
345 345
346 346 if self.due_date and self.start_date and self.due_date < self.start_date
347 347 errors.add :due_date, :greater_than_start_date
348 348 end
349 349
350 350 if start_date && soonest_start && start_date < soonest_start
351 351 errors.add :start_date, :invalid
352 352 end
353 353
354 354 if fixed_version
355 355 if !assignable_versions.include?(fixed_version)
356 356 errors.add :fixed_version_id, :inclusion
357 357 elsif reopened? && fixed_version.closed?
358 358 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
359 359 end
360 360 end
361 361
362 362 # Checks that the issue can not be added/moved to a disabled tracker
363 363 if project && (tracker_id_changed? || project_id_changed?)
364 364 unless project.trackers.include?(tracker)
365 365 errors.add :tracker_id, :inclusion
366 366 end
367 367 end
368 368
369 369 # Checks parent issue assignment
370 370 if @parent_issue
371 371 if @parent_issue.project_id != project_id
372 372 errors.add :parent_issue_id, :not_same_project
373 373 elsif !new_record?
374 374 # moving an existing issue
375 375 if @parent_issue.root_id != root_id
376 376 # we can always move to another tree
377 377 elsif move_possible?(@parent_issue)
378 378 # move accepted inside tree
379 379 else
380 380 errors.add :parent_issue_id, :not_a_valid_parent
381 381 end
382 382 end
383 383 end
384 384 end
385 385
386 386 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
387 387 # even if the user turns off the setting later
388 388 def update_done_ratio_from_issue_status
389 389 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
390 390 self.done_ratio = status.default_done_ratio
391 391 end
392 392 end
393 393
394 394 def init_journal(user, notes = "")
395 395 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
396 396 @issue_before_change = self.clone
397 397 @issue_before_change.status = self.status
398 398 @custom_values_before_change = {}
399 399 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
400 400 # Make sure updated_on is updated when adding a note.
401 401 updated_on_will_change!
402 402 @current_journal
403 403 end
404 404
405 405 # Return true if the issue is closed, otherwise false
406 406 def closed?
407 407 self.status.is_closed?
408 408 end
409 409
410 410 # Return true if the issue is being reopened
411 411 def reopened?
412 412 if !new_record? && status_id_changed?
413 413 status_was = IssueStatus.find_by_id(status_id_was)
414 414 status_new = IssueStatus.find_by_id(status_id)
415 415 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
416 416 return true
417 417 end
418 418 end
419 419 false
420 420 end
421 421
422 422 # Return true if the issue is being closed
423 423 def closing?
424 424 if !new_record? && status_id_changed?
425 425 status_was = IssueStatus.find_by_id(status_id_was)
426 426 status_new = IssueStatus.find_by_id(status_id)
427 427 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
428 428 return true
429 429 end
430 430 end
431 431 false
432 432 end
433 433
434 434 # Returns true if the issue is overdue
435 435 def overdue?
436 436 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
437 437 end
438 438
439 439 # Is the amount of work done less than it should for the due date
440 440 def behind_schedule?
441 441 return false if start_date.nil? || due_date.nil?
442 442 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
443 443 return done_date <= Date.today
444 444 end
445 445
446 446 # Does this issue have children?
447 447 def children?
448 448 !leaf?
449 449 end
450 450
451 451 # Users the issue can be assigned to
452 452 def assignable_users
453 453 users = project.assignable_users
454 454 users << author if author
455 455 users << assigned_to if assigned_to
456 456 users.uniq.sort
457 457 end
458 458
459 459 # Versions that the issue can be assigned to
460 460 def assignable_versions
461 461 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
462 462 end
463 463
464 464 # Returns true if this issue is blocked by another issue that is still open
465 465 def blocked?
466 466 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
467 467 end
468 468
469 469 # Returns an array of status that user is able to apply
470 470 def new_statuses_allowed_to(user, include_default=false)
471 471 statuses = status.find_new_statuses_allowed_to(
472 472 user.roles_for_project(project),
473 473 tracker,
474 474 author == user,
475 475 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
476 476 )
477 477 statuses << status unless statuses.empty?
478 478 statuses << IssueStatus.default if include_default
479 479 statuses = statuses.uniq.sort
480 480 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
481 481 end
482 482
483 483 # Returns the mail adresses of users that should be notified
484 484 def recipients
485 485 notified = project.notified_users
486 486 # Author and assignee are always notified unless they have been
487 487 # locked or don't want to be notified
488 488 notified << author if author && author.active? && author.notify_about?(self)
489 489 if assigned_to
490 490 if assigned_to.is_a?(Group)
491 491 notified += assigned_to.users.select {|u| u.active? && u.notify_about?(self)}
492 492 else
493 493 notified << assigned_to if assigned_to.active? && assigned_to.notify_about?(self)
494 494 end
495 495 end
496 496 notified.uniq!
497 497 # Remove users that can not view the issue
498 498 notified.reject! {|user| !visible?(user)}
499 499 notified.collect(&:mail)
500 500 end
501 501
502 502 # Returns the total number of hours spent on this issue and its descendants
503 503 #
504 504 # Example:
505 505 # spent_hours => 0.0
506 506 # spent_hours => 50.2
507 507 def spent_hours
508 508 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
509 509 end
510 510
511 511 def relations
512 512 @relations ||= (relations_from + relations_to).sort
513 513 end
514 514
515 515 # Preloads relations for a collection of issues
516 516 def self.load_relations(issues)
517 517 if issues.any?
518 518 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
519 519 issues.each do |issue|
520 520 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
521 521 end
522 522 end
523 523 end
524 524
525 525 # Finds an issue relation given its id.
526 526 def find_relation(relation_id)
527 527 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
528 528 end
529 529
530 530 def all_dependent_issues(except=[])
531 531 except << self
532 532 dependencies = []
533 533 relations_from.each do |relation|
534 534 if relation.issue_to && !except.include?(relation.issue_to)
535 535 dependencies << relation.issue_to
536 536 dependencies += relation.issue_to.all_dependent_issues(except)
537 537 end
538 538 end
539 539 dependencies
540 540 end
541 541
542 542 # Returns an array of issues that duplicate this one
543 543 def duplicates
544 544 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
545 545 end
546 546
547 547 # Returns the due date or the target due date if any
548 548 # Used on gantt chart
549 549 def due_before
550 550 due_date || (fixed_version ? fixed_version.effective_date : nil)
551 551 end
552 552
553 553 # Returns the time scheduled for this issue.
554 554 #
555 555 # Example:
556 556 # Start Date: 2/26/09, End Date: 3/04/09
557 557 # duration => 6
558 558 def duration
559 559 (start_date && due_date) ? due_date - start_date : 0
560 560 end
561 561
562 562 def soonest_start
563 563 @soonest_start ||= (
564 564 relations_to.collect{|relation| relation.successor_soonest_start} +
565 565 ancestors.collect(&:soonest_start)
566 566 ).compact.max
567 567 end
568 568
569 569 def reschedule_after(date)
570 570 return if date.nil?
571 571 if leaf?
572 572 if start_date.nil? || start_date < date
573 573 self.start_date, self.due_date = date, date + duration
574 574 save
575 575 end
576 576 else
577 577 leaves.each do |leaf|
578 578 leaf.reschedule_after(date)
579 579 end
580 580 end
581 581 end
582 582
583 583 def <=>(issue)
584 584 if issue.nil?
585 585 -1
586 586 elsif root_id != issue.root_id
587 587 (root_id || 0) <=> (issue.root_id || 0)
588 588 else
589 589 (lft || 0) <=> (issue.lft || 0)
590 590 end
591 591 end
592 592
593 593 def to_s
594 594 "#{tracker} ##{id}: #{subject}"
595 595 end
596 596
597 597 # Returns a string of css classes that apply to the issue
598 598 def css_classes
599 599 s = "issue status-#{status.position} priority-#{priority.position}"
600 600 s << ' closed' if closed?
601 601 s << ' overdue' if overdue?
602 602 s << ' child' if child?
603 603 s << ' parent' unless leaf?
604 604 s << ' private' if is_private?
605 605 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
606 606 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
607 607 s
608 608 end
609 609
610 610 # Saves an issue, time_entry, attachments, and a journal from the parameters
611 611 # Returns false if save fails
612 612 def save_issue_with_child_records(params, existing_time_entry=nil)
613 613 Issue.transaction do
614 614 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
615 615 @time_entry = existing_time_entry || TimeEntry.new
616 616 @time_entry.project = project
617 617 @time_entry.issue = self
618 618 @time_entry.user = User.current
619 619 @time_entry.spent_on = Date.today
620 620 @time_entry.attributes = params[:time_entry]
621 621 self.time_entries << @time_entry
622 622 end
623 623
624 624 if valid?
625 625 attachments = Attachment.attach_files(self, params[:attachments])
626 626 # TODO: Rename hook
627 627 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
628 628 begin
629 629 if save
630 630 # TODO: Rename hook
631 631 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
632 632 else
633 633 raise ActiveRecord::Rollback
634 634 end
635 635 rescue ActiveRecord::StaleObjectError
636 636 attachments[:files].each(&:destroy)
637 637 errors.add :base, l(:notice_locking_conflict)
638 638 raise ActiveRecord::Rollback
639 639 end
640 640 end
641 641 end
642 642 end
643 643
644 644 # Unassigns issues from +version+ if it's no longer shared with issue's project
645 645 def self.update_versions_from_sharing_change(version)
646 646 # Update issues assigned to the version
647 647 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
648 648 end
649 649
650 650 # Unassigns issues from versions that are no longer shared
651 651 # after +project+ was moved
652 652 def self.update_versions_from_hierarchy_change(project)
653 653 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
654 654 # Update issues of the moved projects and issues assigned to a version of a moved project
655 655 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
656 656 end
657 657
658 658 def parent_issue_id=(arg)
659 659 parent_issue_id = arg.blank? ? nil : arg.to_i
660 660 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
661 661 @parent_issue.id
662 662 else
663 663 @parent_issue = nil
664 664 nil
665 665 end
666 666 end
667 667
668 668 def parent_issue_id
669 669 if instance_variable_defined? :@parent_issue
670 670 @parent_issue.nil? ? nil : @parent_issue.id
671 671 else
672 672 parent_id
673 673 end
674 674 end
675 675
676 676 # Extracted from the ReportsController.
677 677 def self.by_tracker(project)
678 678 count_and_group_by(:project => project,
679 679 :field => 'tracker_id',
680 680 :joins => Tracker.table_name)
681 681 end
682 682
683 683 def self.by_version(project)
684 684 count_and_group_by(:project => project,
685 685 :field => 'fixed_version_id',
686 686 :joins => Version.table_name)
687 687 end
688 688
689 689 def self.by_priority(project)
690 690 count_and_group_by(:project => project,
691 691 :field => 'priority_id',
692 692 :joins => IssuePriority.table_name)
693 693 end
694 694
695 695 def self.by_category(project)
696 696 count_and_group_by(:project => project,
697 697 :field => 'category_id',
698 698 :joins => IssueCategory.table_name)
699 699 end
700 700
701 701 def self.by_assigned_to(project)
702 702 count_and_group_by(:project => project,
703 703 :field => 'assigned_to_id',
704 704 :joins => User.table_name)
705 705 end
706 706
707 707 def self.by_author(project)
708 708 count_and_group_by(:project => project,
709 709 :field => 'author_id',
710 710 :joins => User.table_name)
711 711 end
712 712
713 713 def self.by_subproject(project)
714 714 ActiveRecord::Base.connection.select_all("select s.id as status_id,
715 715 s.is_closed as closed,
716 716 #{Issue.table_name}.project_id as project_id,
717 717 count(#{Issue.table_name}.id) as total
718 718 from
719 719 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
720 720 where
721 721 #{Issue.table_name}.status_id=s.id
722 722 and #{Issue.table_name}.project_id = #{Project.table_name}.id
723 723 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
724 724 and #{Issue.table_name}.project_id <> #{project.id}
725 725 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
726 726 end
727 727 # End ReportsController extraction
728 728
729 729 # Returns an array of projects that current user can move issues to
730 730 def self.allowed_target_projects_on_move
731 731 projects = []
732 732 if User.current.admin?
733 733 # admin is allowed to move issues to any active (visible) project
734 734 projects = Project.visible.all
735 735 elsif User.current.logged?
736 736 if Role.non_member.allowed_to?(:move_issues)
737 737 projects = Project.visible.all
738 738 else
739 739 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
740 740 end
741 741 end
742 742 projects
743 743 end
744 744
745 745 private
746 746
747 747 def update_nested_set_attributes
748 748 if root_id.nil?
749 749 # issue was just created
750 750 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
751 751 set_default_left_and_right
752 752 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
753 753 if @parent_issue
754 754 move_to_child_of(@parent_issue)
755 755 end
756 756 reload
757 757 elsif parent_issue_id != parent_id
758 758 former_parent_id = parent_id
759 759 # moving an existing issue
760 760 if @parent_issue && @parent_issue.root_id == root_id
761 761 # inside the same tree
762 762 move_to_child_of(@parent_issue)
763 763 else
764 764 # to another tree
765 765 unless root?
766 766 move_to_right_of(root)
767 767 reload
768 768 end
769 769 old_root_id = root_id
770 770 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
771 771 target_maxright = nested_set_scope.maximum(right_column_name) || 0
772 772 offset = target_maxright + 1 - lft
773 773 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
774 774 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
775 775 self[left_column_name] = lft + offset
776 776 self[right_column_name] = rgt + offset
777 777 if @parent_issue
778 778 move_to_child_of(@parent_issue)
779 779 end
780 780 end
781 781 reload
782 782 # delete invalid relations of all descendants
783 783 self_and_descendants.each do |issue|
784 784 issue.relations.each do |relation|
785 785 relation.destroy unless relation.valid?
786 786 end
787 787 end
788 788 # update former parent
789 789 recalculate_attributes_for(former_parent_id) if former_parent_id
790 790 end
791 791 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
792 792 end
793 793
794 794 def update_parent_attributes
795 795 recalculate_attributes_for(parent_id) if parent_id
796 796 end
797 797
798 798 def recalculate_attributes_for(issue_id)
799 799 if issue_id && p = Issue.find_by_id(issue_id)
800 800 # priority = highest priority of children
801 801 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
802 802 p.priority = IssuePriority.find_by_position(priority_position)
803 803 end
804 804
805 805 # start/due dates = lowest/highest dates of children
806 806 p.start_date = p.children.minimum(:start_date)
807 807 p.due_date = p.children.maximum(:due_date)
808 808 if p.start_date && p.due_date && p.due_date < p.start_date
809 809 p.start_date, p.due_date = p.due_date, p.start_date
810 810 end
811 811
812 812 # done ratio = weighted average ratio of leaves
813 813 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
814 814 leaves_count = p.leaves.count
815 815 if leaves_count > 0
816 816 average = p.leaves.average(:estimated_hours).to_f
817 817 if average == 0
818 818 average = 1
819 819 end
820 820 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f
821 821 progress = done / (average * leaves_count)
822 822 p.done_ratio = progress.round
823 823 end
824 824 end
825 825
826 826 # estimate = sum of leaves estimates
827 827 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
828 828 p.estimated_hours = nil if p.estimated_hours == 0.0
829 829
830 830 # ancestors will be recursively updated
831 831 p.save(false)
832 832 end
833 833 end
834 834
835 835 # Update issues so their versions are not pointing to a
836 836 # fixed_version that is not shared with the issue's project
837 837 def self.update_versions(conditions=nil)
838 838 # Only need to update issues with a fixed_version from
839 839 # a different project and that is not systemwide shared
840 840 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
841 841 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
842 842 " AND #{Version.table_name}.sharing <> 'system'",
843 843 conditions),
844 844 :include => [:project, :fixed_version]
845 845 ).each do |issue|
846 846 next if issue.project.nil? || issue.fixed_version.nil?
847 847 unless issue.project.shared_versions.include?(issue.fixed_version)
848 848 issue.init_journal(User.current)
849 849 issue.fixed_version = nil
850 850 issue.save
851 851 end
852 852 end
853 853 end
854 854
855 855 # Callback on attachment deletion
856 856 def attachment_added(obj)
857 857 if @current_journal && !obj.new_record?
858 858 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
859 859 end
860 860 end
861 861
862 862 # Callback on attachment deletion
863 863 def attachment_removed(obj)
864 864 journal = init_journal(User.current)
865 865 journal.details << JournalDetail.new(:property => 'attachment',
866 866 :prop_key => obj.id,
867 867 :old_value => obj.filename)
868 868 journal.save
869 869 end
870 870
871 871 # Default assignment based on category
872 872 def default_assign
873 873 if assigned_to.nil? && category && category.assigned_to
874 874 self.assigned_to = category.assigned_to
875 875 end
876 876 end
877 877
878 878 # Updates start/due dates of following issues
879 879 def reschedule_following_issues
880 880 if start_date_changed? || due_date_changed?
881 881 relations_from.each do |relation|
882 882 relation.set_issue_to_dates
883 883 end
884 884 end
885 885 end
886 886
887 887 # Closes duplicates if the issue is being closed
888 888 def close_duplicates
889 889 if closing?
890 890 duplicates.each do |duplicate|
891 891 # Reload is need in case the duplicate was updated by a previous duplicate
892 892 duplicate.reload
893 893 # Don't re-close it if it's already closed
894 894 next if duplicate.closed?
895 895 # Same user and notes
896 896 if @current_journal
897 897 duplicate.init_journal(@current_journal.user, @current_journal.notes)
898 898 end
899 899 duplicate.update_attribute :status, self.status
900 900 end
901 901 end
902 902 end
903 903
904 904 # Saves the changes in a Journal
905 905 # Called after_save
906 906 def create_journal
907 907 if @current_journal
908 908 # attributes changes
909 909 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
910 910 before = @issue_before_change.send(c)
911 911 after = send(c)
912 912 next if before == after || (before.blank? && after.blank?)
913 913 @current_journal.details << JournalDetail.new(:property => 'attr',
914 914 :prop_key => c,
915 915 :old_value => @issue_before_change.send(c),
916 916 :value => send(c))
917 917 }
918 918 # custom fields changes
919 919 custom_values.each {|c|
920 920 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
921 921 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
922 922 @current_journal.details << JournalDetail.new(:property => 'cf',
923 923 :prop_key => c.custom_field_id,
924 924 :old_value => @custom_values_before_change[c.custom_field_id],
925 925 :value => c.value)
926 926 }
927 927 @current_journal.save
928 928 # reset current journal
929 929 init_journal @current_journal.user, @current_journal.notes
930 930 end
931 931 end
932 932
933 933 # Query generator for selecting groups of issue counts for a project
934 934 # based on specific criteria
935 935 #
936 936 # Options
937 937 # * project - Project to search in.
938 938 # * field - String. Issue field to key off of in the grouping.
939 939 # * joins - String. The table name to join against.
940 940 def self.count_and_group_by(options)
941 941 project = options.delete(:project)
942 942 select_field = options.delete(:field)
943 943 joins = options.delete(:joins)
944 944
945 945 where = "#{Issue.table_name}.#{select_field}=j.id"
946 946
947 947 ActiveRecord::Base.connection.select_all("select s.id as status_id,
948 948 s.is_closed as closed,
949 949 j.id as #{select_field},
950 950 count(#{Issue.table_name}.id) as total
951 951 from
952 952 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
953 953 where
954 954 #{Issue.table_name}.status_id=s.id
955 955 and #{where}
956 956 and #{Issue.table_name}.project_id=#{Project.table_name}.id
957 957 and #{visible_condition(User.current, :project => project)}
958 958 group by s.id, s.is_closed, j.id")
959 959 end
960 960 end
@@ -1,1111 +1,1134
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class IssueTest < ActiveSupport::TestCase
21 21 fixtures :projects, :users, :members, :member_roles, :roles,
22 22 :trackers, :projects_trackers,
23 23 :enabled_modules,
24 24 :versions,
25 25 :issue_statuses, :issue_categories, :issue_relations, :workflows,
26 26 :enumerations,
27 27 :issues,
28 28 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
29 29 :time_entries
30 30
31 31 def test_create
32 32 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
33 33 :status_id => 1, :priority => IssuePriority.all.first,
34 34 :subject => 'test_create',
35 35 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
36 36 assert issue.save
37 37 issue.reload
38 38 assert_equal 1.5, issue.estimated_hours
39 39 end
40 40
41 41 def test_create_minimal
42 42 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
43 43 :status_id => 1, :priority => IssuePriority.all.first,
44 44 :subject => 'test_create')
45 45 assert issue.save
46 46 assert issue.description.nil?
47 47 end
48 48
49 49 def test_create_with_required_custom_field
50 50 field = IssueCustomField.find_by_name('Database')
51 51 field.update_attribute(:is_required, true)
52 52
53 53 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
54 54 :status_id => 1, :subject => 'test_create',
55 55 :description => 'IssueTest#test_create_with_required_custom_field')
56 56 assert issue.available_custom_fields.include?(field)
57 57 # No value for the custom field
58 58 assert !issue.save
59 59 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
60 60 # Blank value
61 61 issue.custom_field_values = { field.id => '' }
62 62 assert !issue.save
63 63 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
64 64 # Invalid value
65 65 issue.custom_field_values = { field.id => 'SQLServer' }
66 66 assert !issue.save
67 67 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
68 68 # Valid value
69 69 issue.custom_field_values = { field.id => 'PostgreSQL' }
70 70 assert issue.save
71 71 issue.reload
72 72 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
73 73 end
74 74
75 75 def test_create_with_group_assignment
76 76 with_settings :issue_group_assignment => '1' do
77 77 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
78 78 :subject => 'Group assignment',
79 79 :assigned_to_id => 11).save
80 80 issue = Issue.first(:order => 'id DESC')
81 81 assert_kind_of Group, issue.assigned_to
82 82 assert_equal Group.find(11), issue.assigned_to
83 83 end
84 84 end
85 85
86 86 def assert_visibility_match(user, issues)
87 87 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
88 88 end
89 89
90 90 def test_visible_scope_for_anonymous
91 91 # Anonymous user should see issues of public projects only
92 92 issues = Issue.visible(User.anonymous).all
93 93 assert issues.any?
94 94 assert_nil issues.detect {|issue| !issue.project.is_public?}
95 95 assert_nil issues.detect {|issue| issue.is_private?}
96 96 assert_visibility_match User.anonymous, issues
97 97 end
98 98
99 99 def test_visible_scope_for_anonymous_with_own_issues_visibility
100 100 Role.anonymous.update_attribute :issues_visibility, 'own'
101 101 Issue.create!(:project_id => 1, :tracker_id => 1,
102 102 :author_id => User.anonymous.id,
103 103 :subject => 'Issue by anonymous')
104 104
105 105 issues = Issue.visible(User.anonymous).all
106 106 assert issues.any?
107 107 assert_nil issues.detect {|issue| issue.author != User.anonymous}
108 108 assert_visibility_match User.anonymous, issues
109 109 end
110 110
111 111 def test_visible_scope_for_anonymous_without_view_issues_permissions
112 112 # Anonymous user should not see issues without permission
113 113 Role.anonymous.remove_permission!(:view_issues)
114 114 issues = Issue.visible(User.anonymous).all
115 115 assert issues.empty?
116 116 assert_visibility_match User.anonymous, issues
117 117 end
118 118
119 119 def test_visible_scope_for_non_member
120 120 user = User.find(9)
121 121 assert user.projects.empty?
122 122 # Non member user should see issues of public projects only
123 123 issues = Issue.visible(user).all
124 124 assert issues.any?
125 125 assert_nil issues.detect {|issue| !issue.project.is_public?}
126 126 assert_nil issues.detect {|issue| issue.is_private?}
127 127 assert_visibility_match user, issues
128 128 end
129 129
130 130 def test_visible_scope_for_non_member_with_own_issues_visibility
131 131 Role.non_member.update_attribute :issues_visibility, 'own'
132 132 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
133 133 user = User.find(9)
134 134
135 135 issues = Issue.visible(user).all
136 136 assert issues.any?
137 137 assert_nil issues.detect {|issue| issue.author != user}
138 138 assert_visibility_match user, issues
139 139 end
140 140
141 141 def test_visible_scope_for_non_member_without_view_issues_permissions
142 142 # Non member user should not see issues without permission
143 143 Role.non_member.remove_permission!(:view_issues)
144 144 user = User.find(9)
145 145 assert user.projects.empty?
146 146 issues = Issue.visible(user).all
147 147 assert issues.empty?
148 148 assert_visibility_match user, issues
149 149 end
150 150
151 151 def test_visible_scope_for_member
152 152 user = User.find(9)
153 153 # User should see issues of projects for which he has view_issues permissions only
154 154 Role.non_member.remove_permission!(:view_issues)
155 155 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
156 156 issues = Issue.visible(user).all
157 157 assert issues.any?
158 158 assert_nil issues.detect {|issue| issue.project_id != 3}
159 159 assert_nil issues.detect {|issue| issue.is_private?}
160 160 assert_visibility_match user, issues
161 161 end
162 162
163 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
164 user = User.find(8)
165 assert user.groups.any?
166 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
167 Role.non_member.remove_permission!(:view_issues)
168
169 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
170 :status_id => 1, :priority => IssuePriority.all.first,
171 :subject => 'Assignment test',
172 :assigned_to => user.groups.first,
173 :is_private => true)
174
175 Role.find(2).update_attribute :issues_visibility, 'default'
176 issues = Issue.visible(User.find(8)).all
177 assert issues.any?
178 assert issues.include?(issue)
179
180 Role.find(2).update_attribute :issues_visibility, 'own'
181 issues = Issue.visible(User.find(8)).all
182 assert issues.any?
183 assert issues.include?(issue)
184 end
185
163 186 def test_visible_scope_for_admin
164 187 user = User.find(1)
165 188 user.members.each(&:destroy)
166 189 assert user.projects.empty?
167 190 issues = Issue.visible(user).all
168 191 assert issues.any?
169 192 # Admin should see issues on private projects that he does not belong to
170 193 assert issues.detect {|issue| !issue.project.is_public?}
171 194 # Admin should see private issues of other users
172 195 assert issues.detect {|issue| issue.is_private? && issue.author != user}
173 196 assert_visibility_match user, issues
174 197 end
175 198
176 199 def test_visible_scope_with_project
177 200 project = Project.find(1)
178 201 issues = Issue.visible(User.find(2), :project => project).all
179 202 projects = issues.collect(&:project).uniq
180 203 assert_equal 1, projects.size
181 204 assert_equal project, projects.first
182 205 end
183 206
184 207 def test_visible_scope_with_project_and_subprojects
185 208 project = Project.find(1)
186 209 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
187 210 projects = issues.collect(&:project).uniq
188 211 assert projects.size > 1
189 212 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
190 213 end
191 214
192 215 def test_visible_and_nested_set_scopes
193 216 assert_equal 0, Issue.find(1).descendants.visible.all.size
194 217 end
195 218
196 219 def test_errors_full_messages_should_include_custom_fields_errors
197 220 field = IssueCustomField.find_by_name('Database')
198 221
199 222 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
200 223 :status_id => 1, :subject => 'test_create',
201 224 :description => 'IssueTest#test_create_with_required_custom_field')
202 225 assert issue.available_custom_fields.include?(field)
203 226 # Invalid value
204 227 issue.custom_field_values = { field.id => 'SQLServer' }
205 228
206 229 assert !issue.valid?
207 230 assert_equal 1, issue.errors.full_messages.size
208 231 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
209 232 issue.errors.full_messages.first
210 233 end
211 234
212 235 def test_update_issue_with_required_custom_field
213 236 field = IssueCustomField.find_by_name('Database')
214 237 field.update_attribute(:is_required, true)
215 238
216 239 issue = Issue.find(1)
217 240 assert_nil issue.custom_value_for(field)
218 241 assert issue.available_custom_fields.include?(field)
219 242 # No change to custom values, issue can be saved
220 243 assert issue.save
221 244 # Blank value
222 245 issue.custom_field_values = { field.id => '' }
223 246 assert !issue.save
224 247 # Valid value
225 248 issue.custom_field_values = { field.id => 'PostgreSQL' }
226 249 assert issue.save
227 250 issue.reload
228 251 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
229 252 end
230 253
231 254 def test_should_not_update_attributes_if_custom_fields_validation_fails
232 255 issue = Issue.find(1)
233 256 field = IssueCustomField.find_by_name('Database')
234 257 assert issue.available_custom_fields.include?(field)
235 258
236 259 issue.custom_field_values = { field.id => 'Invalid' }
237 260 issue.subject = 'Should be not be saved'
238 261 assert !issue.save
239 262
240 263 issue.reload
241 264 assert_equal "Can't print recipes", issue.subject
242 265 end
243 266
244 267 def test_should_not_recreate_custom_values_objects_on_update
245 268 field = IssueCustomField.find_by_name('Database')
246 269
247 270 issue = Issue.find(1)
248 271 issue.custom_field_values = { field.id => 'PostgreSQL' }
249 272 assert issue.save
250 273 custom_value = issue.custom_value_for(field)
251 274 issue.reload
252 275 issue.custom_field_values = { field.id => 'MySQL' }
253 276 assert issue.save
254 277 issue.reload
255 278 assert_equal custom_value.id, issue.custom_value_for(field).id
256 279 end
257 280
258 281 def test_assigning_tracker_id_should_reload_custom_fields_values
259 282 issue = Issue.new(:project => Project.find(1))
260 283 assert issue.custom_field_values.empty?
261 284 issue.tracker_id = 1
262 285 assert issue.custom_field_values.any?
263 286 end
264 287
265 288 def test_assigning_attributes_should_assign_tracker_id_first
266 289 attributes = ActiveSupport::OrderedHash.new
267 290 attributes['custom_field_values'] = { '1' => 'MySQL' }
268 291 attributes['tracker_id'] = '1'
269 292 issue = Issue.new(:project => Project.find(1))
270 293 issue.attributes = attributes
271 294 assert_not_nil issue.custom_value_for(1)
272 295 assert_equal 'MySQL', issue.custom_value_for(1).value
273 296 end
274 297
275 298 def test_should_update_issue_with_disabled_tracker
276 299 p = Project.find(1)
277 300 issue = Issue.find(1)
278 301
279 302 p.trackers.delete(issue.tracker)
280 303 assert !p.trackers.include?(issue.tracker)
281 304
282 305 issue.reload
283 306 issue.subject = 'New subject'
284 307 assert issue.save
285 308 end
286 309
287 310 def test_should_not_set_a_disabled_tracker
288 311 p = Project.find(1)
289 312 p.trackers.delete(Tracker.find(2))
290 313
291 314 issue = Issue.find(1)
292 315 issue.tracker_id = 2
293 316 issue.subject = 'New subject'
294 317 assert !issue.save
295 318 assert_not_nil issue.errors[:tracker_id]
296 319 end
297 320
298 321 def test_category_based_assignment
299 322 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
300 323 :status_id => 1, :priority => IssuePriority.all.first,
301 324 :subject => 'Assignment test',
302 325 :description => 'Assignment test', :category_id => 1)
303 326 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
304 327 end
305 328
306 329 def test_new_statuses_allowed_to
307 330 Workflow.delete_all
308 331
309 332 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
310 333 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
311 334 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
312 335 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
313 336 status = IssueStatus.find(1)
314 337 role = Role.find(1)
315 338 tracker = Tracker.find(1)
316 339 user = User.find(2)
317 340
318 341 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1)
319 342 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
320 343
321 344 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user)
322 345 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
323 346
324 347 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :assigned_to => user)
325 348 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
326 349
327 350 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user, :assigned_to => user)
328 351 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
329 352 end
330 353
331 354 def test_copy
332 355 issue = Issue.new.copy_from(1)
333 356 assert issue.save
334 357 issue.reload
335 358 orig = Issue.find(1)
336 359 assert_equal orig.subject, issue.subject
337 360 assert_equal orig.tracker, issue.tracker
338 361 assert_equal "125", issue.custom_value_for(2).value
339 362 end
340 363
341 364 def test_copy_should_copy_status
342 365 orig = Issue.find(8)
343 366 assert orig.status != IssueStatus.default
344 367
345 368 issue = Issue.new.copy_from(orig)
346 369 assert issue.save
347 370 issue.reload
348 371 assert_equal orig.status, issue.status
349 372 end
350 373
351 374 def test_should_close_duplicates
352 375 # Create 3 issues
353 376 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
354 377 :status_id => 1, :priority => IssuePriority.all.first,
355 378 :subject => 'Duplicates test', :description => 'Duplicates test')
356 379 assert issue1.save
357 380 issue2 = issue1.clone
358 381 assert issue2.save
359 382 issue3 = issue1.clone
360 383 assert issue3.save
361 384
362 385 # 2 is a dupe of 1
363 386 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
364 387 # And 3 is a dupe of 2
365 388 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
366 389 # And 3 is a dupe of 1 (circular duplicates)
367 390 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
368 391
369 392 assert issue1.reload.duplicates.include?(issue2)
370 393
371 394 # Closing issue 1
372 395 issue1.init_journal(User.find(:first), "Closing issue1")
373 396 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
374 397 assert issue1.save
375 398 # 2 and 3 should be also closed
376 399 assert issue2.reload.closed?
377 400 assert issue3.reload.closed?
378 401 end
379 402
380 403 def test_should_not_close_duplicated_issue
381 404 # Create 3 issues
382 405 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
383 406 :status_id => 1, :priority => IssuePriority.all.first,
384 407 :subject => 'Duplicates test', :description => 'Duplicates test')
385 408 assert issue1.save
386 409 issue2 = issue1.clone
387 410 assert issue2.save
388 411
389 412 # 2 is a dupe of 1
390 413 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
391 414 # 2 is a dup of 1 but 1 is not a duplicate of 2
392 415 assert !issue2.reload.duplicates.include?(issue1)
393 416
394 417 # Closing issue 2
395 418 issue2.init_journal(User.find(:first), "Closing issue2")
396 419 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
397 420 assert issue2.save
398 421 # 1 should not be also closed
399 422 assert !issue1.reload.closed?
400 423 end
401 424
402 425 def test_assignable_versions
403 426 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
404 427 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
405 428 end
406 429
407 430 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
408 431 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
409 432 assert !issue.save
410 433 assert_not_nil issue.errors[:fixed_version_id]
411 434 end
412 435
413 436 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
414 437 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
415 438 assert !issue.save
416 439 assert_not_nil issue.errors[:fixed_version_id]
417 440 end
418 441
419 442 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
420 443 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
421 444 assert issue.save
422 445 end
423 446
424 447 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
425 448 issue = Issue.find(11)
426 449 assert_equal 'closed', issue.fixed_version.status
427 450 issue.subject = 'Subject changed'
428 451 assert issue.save
429 452 end
430 453
431 454 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
432 455 issue = Issue.find(11)
433 456 issue.status_id = 1
434 457 assert !issue.save
435 458 assert_not_nil issue.errors[:base]
436 459 end
437 460
438 461 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
439 462 issue = Issue.find(11)
440 463 issue.status_id = 1
441 464 issue.fixed_version_id = 3
442 465 assert issue.save
443 466 end
444 467
445 468 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
446 469 issue = Issue.find(12)
447 470 assert_equal 'locked', issue.fixed_version.status
448 471 issue.status_id = 1
449 472 assert issue.save
450 473 end
451 474
452 475 def test_move_to_another_project_with_same_category
453 476 issue = Issue.find(1)
454 477 assert issue.move_to_project(Project.find(2))
455 478 issue.reload
456 479 assert_equal 2, issue.project_id
457 480 # Category changes
458 481 assert_equal 4, issue.category_id
459 482 # Make sure time entries were move to the target project
460 483 assert_equal 2, issue.time_entries.first.project_id
461 484 end
462 485
463 486 def test_move_to_another_project_without_same_category
464 487 issue = Issue.find(2)
465 488 assert issue.move_to_project(Project.find(2))
466 489 issue.reload
467 490 assert_equal 2, issue.project_id
468 491 # Category cleared
469 492 assert_nil issue.category_id
470 493 end
471 494
472 495 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
473 496 issue = Issue.find(1)
474 497 issue.update_attribute(:fixed_version_id, 1)
475 498 assert issue.move_to_project(Project.find(2))
476 499 issue.reload
477 500 assert_equal 2, issue.project_id
478 501 # Cleared fixed_version
479 502 assert_equal nil, issue.fixed_version
480 503 end
481 504
482 505 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
483 506 issue = Issue.find(1)
484 507 issue.update_attribute(:fixed_version_id, 4)
485 508 assert issue.move_to_project(Project.find(5))
486 509 issue.reload
487 510 assert_equal 5, issue.project_id
488 511 # Keep fixed_version
489 512 assert_equal 4, issue.fixed_version_id
490 513 end
491 514
492 515 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
493 516 issue = Issue.find(1)
494 517 issue.update_attribute(:fixed_version_id, 1)
495 518 assert issue.move_to_project(Project.find(5))
496 519 issue.reload
497 520 assert_equal 5, issue.project_id
498 521 # Cleared fixed_version
499 522 assert_equal nil, issue.fixed_version
500 523 end
501 524
502 525 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
503 526 issue = Issue.find(1)
504 527 issue.update_attribute(:fixed_version_id, 7)
505 528 assert issue.move_to_project(Project.find(2))
506 529 issue.reload
507 530 assert_equal 2, issue.project_id
508 531 # Keep fixed_version
509 532 assert_equal 7, issue.fixed_version_id
510 533 end
511 534
512 535 def test_move_to_another_project_with_disabled_tracker
513 536 issue = Issue.find(1)
514 537 target = Project.find(2)
515 538 target.tracker_ids = [3]
516 539 target.save
517 540 assert_equal false, issue.move_to_project(target)
518 541 issue.reload
519 542 assert_equal 1, issue.project_id
520 543 end
521 544
522 545 def test_copy_to_the_same_project
523 546 issue = Issue.find(1)
524 547 copy = nil
525 548 assert_difference 'Issue.count' do
526 549 copy = issue.move_to_project(issue.project, nil, :copy => true)
527 550 end
528 551 assert_kind_of Issue, copy
529 552 assert_equal issue.project, copy.project
530 553 assert_equal "125", copy.custom_value_for(2).value
531 554 end
532 555
533 556 def test_copy_to_another_project_and_tracker
534 557 issue = Issue.find(1)
535 558 copy = nil
536 559 assert_difference 'Issue.count' do
537 560 copy = issue.move_to_project(Project.find(3), Tracker.find(2), :copy => true)
538 561 end
539 562 copy.reload
540 563 assert_kind_of Issue, copy
541 564 assert_equal Project.find(3), copy.project
542 565 assert_equal Tracker.find(2), copy.tracker
543 566 # Custom field #2 is not associated with target tracker
544 567 assert_nil copy.custom_value_for(2)
545 568 end
546 569
547 570 context "#move_to_project" do
548 571 context "as a copy" do
549 572 setup do
550 573 @issue = Issue.find(1)
551 574 @copy = nil
552 575 end
553 576
554 577 should "not create a journal" do
555 578 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
556 579 assert_equal 0, @copy.reload.journals.size
557 580 end
558 581
559 582 should "allow assigned_to changes" do
560 583 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
561 584 assert_equal 3, @copy.assigned_to_id
562 585 end
563 586
564 587 should "allow status changes" do
565 588 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:status_id => 2}})
566 589 assert_equal 2, @copy.status_id
567 590 end
568 591
569 592 should "allow start date changes" do
570 593 date = Date.today
571 594 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
572 595 assert_equal date, @copy.start_date
573 596 end
574 597
575 598 should "allow due date changes" do
576 599 date = Date.today
577 600 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:due_date => date}})
578 601
579 602 assert_equal date, @copy.due_date
580 603 end
581 604
582 605 should "set current user as author" do
583 606 User.current = User.find(9)
584 607 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {}})
585 608
586 609 assert_equal User.current, @copy.author
587 610 end
588 611
589 612 should "keep journal notes" do
590 613 date = Date.today
591 614 notes = "Notes added when copying"
592 615 User.current = User.find(9)
593 616 @issue.init_journal(User.current, notes)
594 617 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
595 618
596 619 assert_equal 1, @copy.journals.size
597 620 journal = @copy.journals.first
598 621 assert_equal 0, journal.details.size
599 622 assert_equal notes, journal.notes
600 623 end
601 624 end
602 625 end
603 626
604 627 def test_recipients_should_not_include_users_that_cannot_view_the_issue
605 628 issue = Issue.find(12)
606 629 assert issue.recipients.include?(issue.author.mail)
607 630 # move the issue to a private project
608 631 copy = issue.move_to_project(Project.find(5), Tracker.find(2), :copy => true)
609 632 # author is not a member of project anymore
610 633 assert !copy.recipients.include?(copy.author.mail)
611 634 end
612 635
613 636 def test_recipients_should_include_the_assigned_group_members
614 637 group_member = User.generate_with_protected!
615 638 group = Group.generate!
616 639 group.users << group_member
617 640
618 641 issue = Issue.find(12)
619 642 issue.assigned_to = group
620 643 assert issue.recipients.include?(group_member.mail)
621 644 end
622 645
623 646 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
624 647 user = User.find(3)
625 648 issue = Issue.find(9)
626 649 Watcher.create!(:user => user, :watchable => issue)
627 650 assert issue.watched_by?(user)
628 651 assert !issue.watcher_recipients.include?(user.mail)
629 652 end
630 653
631 654 def test_issue_destroy
632 655 Issue.find(1).destroy
633 656 assert_nil Issue.find_by_id(1)
634 657 assert_nil TimeEntry.find_by_issue_id(1)
635 658 end
636 659
637 660 def test_blocked
638 661 blocked_issue = Issue.find(9)
639 662 blocking_issue = Issue.find(10)
640 663
641 664 assert blocked_issue.blocked?
642 665 assert !blocking_issue.blocked?
643 666 end
644 667
645 668 def test_blocked_issues_dont_allow_closed_statuses
646 669 blocked_issue = Issue.find(9)
647 670
648 671 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
649 672 assert !allowed_statuses.empty?
650 673 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
651 674 assert closed_statuses.empty?
652 675 end
653 676
654 677 def test_unblocked_issues_allow_closed_statuses
655 678 blocking_issue = Issue.find(10)
656 679
657 680 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
658 681 assert !allowed_statuses.empty?
659 682 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
660 683 assert !closed_statuses.empty?
661 684 end
662 685
663 686 def test_rescheduling_an_issue_should_reschedule_following_issue
664 687 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
665 688 issue2 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
666 689 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
667 690 assert_equal issue1.due_date + 1, issue2.reload.start_date
668 691
669 692 issue1.due_date = Date.today + 5
670 693 issue1.save!
671 694 assert_equal issue1.due_date + 1, issue2.reload.start_date
672 695 end
673 696
674 697 def test_overdue
675 698 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
676 699 assert !Issue.new(:due_date => Date.today).overdue?
677 700 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
678 701 assert !Issue.new(:due_date => nil).overdue?
679 702 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
680 703 end
681 704
682 705 context "#behind_schedule?" do
683 706 should "be false if the issue has no start_date" do
684 707 assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
685 708 end
686 709
687 710 should "be false if the issue has no end_date" do
688 711 assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
689 712 end
690 713
691 714 should "be false if the issue has more done than it's calendar time" do
692 715 assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
693 716 end
694 717
695 718 should "be true if the issue hasn't been started at all" do
696 719 assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
697 720 end
698 721
699 722 should "be true if the issue has used more calendar time than it's done ratio" do
700 723 assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
701 724 end
702 725 end
703 726
704 727 context "#assignable_users" do
705 728 should "be Users" do
706 729 assert_kind_of User, Issue.find(1).assignable_users.first
707 730 end
708 731
709 732 should "include the issue author" do
710 733 project = Project.find(1)
711 734 non_project_member = User.generate!
712 735 issue = Issue.generate_for_project!(project, :author => non_project_member)
713 736
714 737 assert issue.assignable_users.include?(non_project_member)
715 738 end
716 739
717 740 should "include the current assignee" do
718 741 project = Project.find(1)
719 742 user = User.generate!
720 743 issue = Issue.generate_for_project!(project, :assigned_to => user)
721 744 user.lock!
722 745
723 746 assert Issue.find(issue.id).assignable_users.include?(user)
724 747 end
725 748
726 749 should "not show the issue author twice" do
727 750 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
728 751 assert_equal 2, assignable_user_ids.length
729 752
730 753 assignable_user_ids.each do |user_id|
731 754 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, "User #{user_id} appears more or less than once"
732 755 end
733 756 end
734 757
735 758 context "with issue_group_assignment" do
736 759 should "include groups" do
737 760 issue = Issue.new(:project => Project.find(2))
738 761
739 762 with_settings :issue_group_assignment => '1' do
740 763 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
741 764 assert issue.assignable_users.include?(Group.find(11))
742 765 end
743 766 end
744 767 end
745 768
746 769 context "without issue_group_assignment" do
747 770 should "not include groups" do
748 771 issue = Issue.new(:project => Project.find(2))
749 772
750 773 with_settings :issue_group_assignment => '0' do
751 774 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
752 775 assert !issue.assignable_users.include?(Group.find(11))
753 776 end
754 777 end
755 778 end
756 779 end
757 780
758 781 def test_create_should_send_email_notification
759 782 ActionMailer::Base.deliveries.clear
760 783 issue = Issue.new(:project_id => 1, :tracker_id => 1,
761 784 :author_id => 3, :status_id => 1,
762 785 :priority => IssuePriority.all.first,
763 786 :subject => 'test_create', :estimated_hours => '1:30')
764 787
765 788 assert issue.save
766 789 assert_equal 1, ActionMailer::Base.deliveries.size
767 790 end
768 791
769 792 def test_stale_issue_should_not_send_email_notification
770 793 ActionMailer::Base.deliveries.clear
771 794 issue = Issue.find(1)
772 795 stale = Issue.find(1)
773 796
774 797 issue.init_journal(User.find(1))
775 798 issue.subject = 'Subjet update'
776 799 assert issue.save
777 800 assert_equal 1, ActionMailer::Base.deliveries.size
778 801 ActionMailer::Base.deliveries.clear
779 802
780 803 stale.init_journal(User.find(1))
781 804 stale.subject = 'Another subjet update'
782 805 assert_raise ActiveRecord::StaleObjectError do
783 806 stale.save
784 807 end
785 808 assert ActionMailer::Base.deliveries.empty?
786 809 end
787 810
788 811 def test_journalized_description
789 812 IssueCustomField.delete_all
790 813
791 814 i = Issue.first
792 815 old_description = i.description
793 816 new_description = "This is the new description"
794 817
795 818 i.init_journal(User.find(2))
796 819 i.description = new_description
797 820 assert_difference 'Journal.count', 1 do
798 821 assert_difference 'JournalDetail.count', 1 do
799 822 i.save!
800 823 end
801 824 end
802 825
803 826 detail = JournalDetail.first(:order => 'id DESC')
804 827 assert_equal i, detail.journal.journalized
805 828 assert_equal 'attr', detail.property
806 829 assert_equal 'description', detail.prop_key
807 830 assert_equal old_description, detail.old_value
808 831 assert_equal new_description, detail.value
809 832 end
810 833
811 834 def test_blank_descriptions_should_not_be_journalized
812 835 IssueCustomField.delete_all
813 836 Issue.update_all("description = NULL", "id=1")
814 837
815 838 i = Issue.find(1)
816 839 i.init_journal(User.find(2))
817 840 i.subject = "blank description"
818 841 i.description = "\r\n"
819 842
820 843 assert_difference 'Journal.count', 1 do
821 844 assert_difference 'JournalDetail.count', 1 do
822 845 i.save!
823 846 end
824 847 end
825 848 end
826 849
827 850 def test_description_eol_should_be_normalized
828 851 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
829 852 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
830 853 end
831 854
832 855 def test_saving_twice_should_not_duplicate_journal_details
833 856 i = Issue.find(:first)
834 857 i.init_journal(User.find(2), 'Some notes')
835 858 # initial changes
836 859 i.subject = 'New subject'
837 860 i.done_ratio = i.done_ratio + 10
838 861 assert_difference 'Journal.count' do
839 862 assert i.save
840 863 end
841 864 # 1 more change
842 865 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
843 866 assert_no_difference 'Journal.count' do
844 867 assert_difference 'JournalDetail.count', 1 do
845 868 i.save
846 869 end
847 870 end
848 871 # no more change
849 872 assert_no_difference 'Journal.count' do
850 873 assert_no_difference 'JournalDetail.count' do
851 874 i.save
852 875 end
853 876 end
854 877 end
855 878
856 879 def test_all_dependent_issues
857 880 IssueRelation.delete_all
858 881 assert IssueRelation.create!(:issue_from => Issue.find(1),
859 882 :issue_to => Issue.find(2),
860 883 :relation_type => IssueRelation::TYPE_PRECEDES)
861 884 assert IssueRelation.create!(:issue_from => Issue.find(2),
862 885 :issue_to => Issue.find(3),
863 886 :relation_type => IssueRelation::TYPE_PRECEDES)
864 887 assert IssueRelation.create!(:issue_from => Issue.find(3),
865 888 :issue_to => Issue.find(8),
866 889 :relation_type => IssueRelation::TYPE_PRECEDES)
867 890
868 891 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
869 892 end
870 893
871 894 def test_all_dependent_issues_with_persistent_circular_dependency
872 895 IssueRelation.delete_all
873 896 assert IssueRelation.create!(:issue_from => Issue.find(1),
874 897 :issue_to => Issue.find(2),
875 898 :relation_type => IssueRelation::TYPE_PRECEDES)
876 899 assert IssueRelation.create!(:issue_from => Issue.find(2),
877 900 :issue_to => Issue.find(3),
878 901 :relation_type => IssueRelation::TYPE_PRECEDES)
879 902 # Validation skipping
880 903 assert IssueRelation.new(:issue_from => Issue.find(3),
881 904 :issue_to => Issue.find(1),
882 905 :relation_type => IssueRelation::TYPE_PRECEDES).save(false)
883 906
884 907 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
885 908 end
886 909
887 910 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
888 911 IssueRelation.delete_all
889 912 assert IssueRelation.create!(:issue_from => Issue.find(1),
890 913 :issue_to => Issue.find(2),
891 914 :relation_type => IssueRelation::TYPE_RELATES)
892 915 assert IssueRelation.create!(:issue_from => Issue.find(2),
893 916 :issue_to => Issue.find(3),
894 917 :relation_type => IssueRelation::TYPE_RELATES)
895 918 assert IssueRelation.create!(:issue_from => Issue.find(3),
896 919 :issue_to => Issue.find(8),
897 920 :relation_type => IssueRelation::TYPE_RELATES)
898 921 # Validation skipping
899 922 assert IssueRelation.new(:issue_from => Issue.find(8),
900 923 :issue_to => Issue.find(2),
901 924 :relation_type => IssueRelation::TYPE_RELATES).save(false)
902 925 assert IssueRelation.new(:issue_from => Issue.find(3),
903 926 :issue_to => Issue.find(1),
904 927 :relation_type => IssueRelation::TYPE_RELATES).save(false)
905 928
906 929 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
907 930 end
908 931
909 932 context "#done_ratio" do
910 933 setup do
911 934 @issue = Issue.find(1)
912 935 @issue_status = IssueStatus.find(1)
913 936 @issue_status.update_attribute(:default_done_ratio, 50)
914 937 @issue2 = Issue.find(2)
915 938 @issue_status2 = IssueStatus.find(2)
916 939 @issue_status2.update_attribute(:default_done_ratio, 0)
917 940 end
918 941
919 942 context "with Setting.issue_done_ratio using the issue_field" do
920 943 setup do
921 944 Setting.issue_done_ratio = 'issue_field'
922 945 end
923 946
924 947 should "read the issue's field" do
925 948 assert_equal 0, @issue.done_ratio
926 949 assert_equal 30, @issue2.done_ratio
927 950 end
928 951 end
929 952
930 953 context "with Setting.issue_done_ratio using the issue_status" do
931 954 setup do
932 955 Setting.issue_done_ratio = 'issue_status'
933 956 end
934 957
935 958 should "read the Issue Status's default done ratio" do
936 959 assert_equal 50, @issue.done_ratio
937 960 assert_equal 0, @issue2.done_ratio
938 961 end
939 962 end
940 963 end
941 964
942 965 context "#update_done_ratio_from_issue_status" do
943 966 setup do
944 967 @issue = Issue.find(1)
945 968 @issue_status = IssueStatus.find(1)
946 969 @issue_status.update_attribute(:default_done_ratio, 50)
947 970 @issue2 = Issue.find(2)
948 971 @issue_status2 = IssueStatus.find(2)
949 972 @issue_status2.update_attribute(:default_done_ratio, 0)
950 973 end
951 974
952 975 context "with Setting.issue_done_ratio using the issue_field" do
953 976 setup do
954 977 Setting.issue_done_ratio = 'issue_field'
955 978 end
956 979
957 980 should "not change the issue" do
958 981 @issue.update_done_ratio_from_issue_status
959 982 @issue2.update_done_ratio_from_issue_status
960 983
961 984 assert_equal 0, @issue.read_attribute(:done_ratio)
962 985 assert_equal 30, @issue2.read_attribute(:done_ratio)
963 986 end
964 987 end
965 988
966 989 context "with Setting.issue_done_ratio using the issue_status" do
967 990 setup do
968 991 Setting.issue_done_ratio = 'issue_status'
969 992 end
970 993
971 994 should "change the issue's done ratio" do
972 995 @issue.update_done_ratio_from_issue_status
973 996 @issue2.update_done_ratio_from_issue_status
974 997
975 998 assert_equal 50, @issue.read_attribute(:done_ratio)
976 999 assert_equal 0, @issue2.read_attribute(:done_ratio)
977 1000 end
978 1001 end
979 1002 end
980 1003
981 1004 test "#by_tracker" do
982 1005 User.current = User.anonymous
983 1006 groups = Issue.by_tracker(Project.find(1))
984 1007 assert_equal 3, groups.size
985 1008 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
986 1009 end
987 1010
988 1011 test "#by_version" do
989 1012 User.current = User.anonymous
990 1013 groups = Issue.by_version(Project.find(1))
991 1014 assert_equal 3, groups.size
992 1015 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
993 1016 end
994 1017
995 1018 test "#by_priority" do
996 1019 User.current = User.anonymous
997 1020 groups = Issue.by_priority(Project.find(1))
998 1021 assert_equal 4, groups.size
999 1022 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1000 1023 end
1001 1024
1002 1025 test "#by_category" do
1003 1026 User.current = User.anonymous
1004 1027 groups = Issue.by_category(Project.find(1))
1005 1028 assert_equal 2, groups.size
1006 1029 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1007 1030 end
1008 1031
1009 1032 test "#by_assigned_to" do
1010 1033 User.current = User.anonymous
1011 1034 groups = Issue.by_assigned_to(Project.find(1))
1012 1035 assert_equal 2, groups.size
1013 1036 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1014 1037 end
1015 1038
1016 1039 test "#by_author" do
1017 1040 User.current = User.anonymous
1018 1041 groups = Issue.by_author(Project.find(1))
1019 1042 assert_equal 4, groups.size
1020 1043 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1021 1044 end
1022 1045
1023 1046 test "#by_subproject" do
1024 1047 User.current = User.anonymous
1025 1048 groups = Issue.by_subproject(Project.find(1))
1026 1049 # Private descendant not visible
1027 1050 assert_equal 1, groups.size
1028 1051 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1029 1052 end
1030 1053
1031 1054 context ".allowed_target_projects_on_move" do
1032 1055 should "return all active projects for admin users" do
1033 1056 User.current = User.find(1)
1034 1057 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
1035 1058 end
1036 1059
1037 1060 should "return allowed projects for non admin users" do
1038 1061 User.current = User.find(2)
1039 1062 Role.non_member.remove_permission! :move_issues
1040 1063 assert_equal 3, Issue.allowed_target_projects_on_move.size
1041 1064
1042 1065 Role.non_member.add_permission! :move_issues
1043 1066 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
1044 1067 end
1045 1068 end
1046 1069
1047 1070 def test_recently_updated_with_limit_scopes
1048 1071 #should return the last updated issue
1049 1072 assert_equal 1, Issue.recently_updated.with_limit(1).length
1050 1073 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
1051 1074 end
1052 1075
1053 1076 def test_on_active_projects_scope
1054 1077 assert Project.find(2).archive
1055 1078
1056 1079 before = Issue.on_active_project.length
1057 1080 # test inclusion to results
1058 1081 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
1059 1082 assert_equal before + 1, Issue.on_active_project.length
1060 1083
1061 1084 # Move to an archived project
1062 1085 issue.project = Project.find(2)
1063 1086 assert issue.save
1064 1087 assert_equal before, Issue.on_active_project.length
1065 1088 end
1066 1089
1067 1090 context "Issue#recipients" do
1068 1091 setup do
1069 1092 @project = Project.find(1)
1070 1093 @author = User.generate_with_protected!
1071 1094 @assignee = User.generate_with_protected!
1072 1095 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
1073 1096 end
1074 1097
1075 1098 should "include project recipients" do
1076 1099 assert @project.recipients.present?
1077 1100 @project.recipients.each do |project_recipient|
1078 1101 assert @issue.recipients.include?(project_recipient)
1079 1102 end
1080 1103 end
1081 1104
1082 1105 should "include the author if the author is active" do
1083 1106 assert @issue.author, "No author set for Issue"
1084 1107 assert @issue.recipients.include?(@issue.author.mail)
1085 1108 end
1086 1109
1087 1110 should "include the assigned to user if the assigned to user is active" do
1088 1111 assert @issue.assigned_to, "No assigned_to set for Issue"
1089 1112 assert @issue.recipients.include?(@issue.assigned_to.mail)
1090 1113 end
1091 1114
1092 1115 should "not include users who opt out of all email" do
1093 1116 @author.update_attribute(:mail_notification, :none)
1094 1117
1095 1118 assert !@issue.recipients.include?(@issue.author.mail)
1096 1119 end
1097 1120
1098 1121 should "not include the issue author if they are only notified of assigned issues" do
1099 1122 @author.update_attribute(:mail_notification, :only_assigned)
1100 1123
1101 1124 assert !@issue.recipients.include?(@issue.author.mail)
1102 1125 end
1103 1126
1104 1127 should "not include the assigned user if they are only notified of owned issues" do
1105 1128 @assignee.update_attribute(:mail_notification, :only_owner)
1106 1129
1107 1130 assert !@issue.recipients.include?(@issue.assigned_to.mail)
1108 1131 end
1109 1132
1110 1133 end
1111 1134 end
General Comments 0
You need to be logged in to leave comments. Login now