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